History

Revert the outbound MCP SDK v2 migration (#1697)

* Revert the outbound MCP SDK v2 migration * Drop the v2 MCP packages from the lockfile

Rhys Sullivan committed 2 months ago

Commit a5800a11 parent 54cde5e91 files · +5838 −10032Browse files

Showing the changes to e2e (9 of 91 files). Show all

Unified · Split

9 files
  1. e2e/cloud/mcp-modern-protocol.test.ts +0 −235
  2. e2e/local/cli-mcp-protocol.test.ts +0 −104
  3. e2e/package.json +0 −3
  4. e2e/scenarios/mcp-modern-only-server.test.ts +0 −131
  5. e2e/selfhost/mcp-modern-protocol.test.ts +0 −52
  6. e2e/setup/cloud.boot.ts +0 −1
  7. e2e/setup/cloudflare.boot.ts +0 −2
  8. e2e/src/fixtures/modern-only-mcp.ts +0 −83
  9. e2e/src/surfaces/modern-mcp.ts +0 −112
e2e/cloud/mcp-modern-protocol.test.ts+0−235
@@ -1,235 +0,0 @@
1import { randomBytes, randomUUID } from "node:crypto";
2
3import { expect } from "@effect/vitest";
4import { Effect } from "effect";
5import { composePluginApi } from "@executor-js/api/server";
6import { isInputRequiredResult } from "@modelcontextprotocol/client";
7
8import { scenario } from "../src/scenario";
9import { Api, Mcp, Target } from "../src/services";
10import {
11 callModernToolWithInputRequired,
12 connectModernMcpClient,
13 MODERN_MCP_PROTOCOL_VERSION,
14 modernToolText,
15 readModernMcpAuthChallenge,
16} from "../src/surfaces/modern-mcp";
17import type { Identity } from "../src/target";
18
19const coreApi = composePluginApi([] as const);
20const LEGACY_PROTOCOL_VERSION = "2025-03-26";
21const JSON_AND_SSE = "application/json, text/event-stream";
22
23const emailOf = (identity: Identity): string => identity.credentials?.email ?? identity.label;
24
25const legacyInitialize = {
26 jsonrpc: "2.0" as const,
27 id: 1,
28 method: "initialize",
29 params: {
30 protocolVersion: LEGACY_PROTOCOL_VERSION,
31 capabilities: {},
32 clientInfo: { name: "executor-modern-e2e-control", version: "1.0.0" },
33 },
34};
35
36const postLegacy = (
37 url: string,
38 body: unknown,
39 options?: { readonly bearer?: string; readonly sessionId?: string },
40): Promise<Response> =>
41 fetch(url, {
42 method: "POST",
43 headers: {
44 accept: JSON_AND_SSE,
45 "content-type": "application/json",
46 "mcp-protocol-version": LEGACY_PROTOCOL_VERSION,
47 ...(options?.bearer ? { authorization: `Bearer ${options.bearer}` } : {}),
48 ...(options?.sessionId ? { "mcp-session-id": options.sessionId } : {}),
49 },
50 body: JSON.stringify(body),
51 });
52
53scenario(
54 "MCP modern protocol · a pinned 2026 client discovers, lists, and executes while legacy isolation stays intact",
55 {},
56 Effect.scoped(
57 Effect.gen(function* () {
58 const target = yield* Target;
59 const mcp = yield* Mcp;
60 const identity = yield* target.newIdentity();
61 const bearer = yield* mcp.mintBearer(emailOf(identity));
62 const client = yield* connectModernMcpClient({
63 url: target.mcpUrl,
64 bearer,
65 mode: { pin: MODERN_MCP_PROTOCOL_VERSION },
66 });
67
68 expect(client.getProtocolEra(), "the pinned client selected the modern era").toBe("modern");
69 expect(client.getNegotiatedProtocolVersion(), "the exact revision was selected").toBe(
70 MODERN_MCP_PROTOCOL_VERSION,
71 );
72 expect(
73 client.getDiscoverResult()?.supportedVersions,
74 "server/discover advertises the pinned revision",
75 ).toContain(MODERN_MCP_PROTOCOL_VERSION);
76
77 const tools = yield* Effect.promise(() => client.listTools());
78 expect(
79 tools.tools.map((tool) => tool.name),
80 "the modern catalog advertises Executor's execute tool",
81 ).toContain("execute");
82
83 const result = yield* Effect.promise(() =>
84 client.callTool({ name: "execute", arguments: { code: "return 6 * 7;" } }),
85 );
86 expect(result.isError, "the modern execute call completes successfully").not.toBe(true);
87 expect(modernToolText(result), "the sandbox result crosses the modern wire").toBe("42");
88
89 const foreignSession = yield* Effect.promise(() =>
90 postLegacy(
91 target.mcpUrl,
92 { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} },
93 { bearer, sessionId: `foreign-${randomUUID()}` },
94 ),
95 );
96 expect(
97 foreignSession.status,
98 "a foreign legacy session id remains a clean not-found response",
99 ).toBe(404);
100 const foreignBody = (yield* Effect.promise(() => foreignSession.json())) as {
101 readonly error?: { readonly code?: number; readonly message?: string };
102 };
103 expect(foreignBody.error?.code, "the legacy rejection remains a JSON-RPC error").toBe(-32001);
104 expect(
105 foreignBody.error?.message,
106 "the unknown legacy session stays a clean not-found error",
107 ).toBe("Session not found");
108
109 const [modernChallenge, legacyChallenge] = yield* Effect.all([
110 readModernMcpAuthChallenge(target.mcpUrl),
111 Effect.promise(() => postLegacy(target.mcpUrl, legacyInitialize)),
112 ]);
113 expect(modernChallenge.status, "the unauthenticated modern probe is challenged").toBe(401);
114 expect(legacyChallenge.status, "the unauthenticated legacy initialize is challenged").toBe(
115 401,
116 );
117 expect(
118 modernChallenge.wwwAuthenticate,
119 "modern and legacy entry paths publish the same Bearer challenge",
120 ).toBe(legacyChallenge.headers.get("www-authenticate"));
121 }),
122 ),
123);
124
125scenario(
126 "MCP modern protocol · a default v2 auto client probes Executor and selects modern",
127 {},
128 Effect.scoped(
129 Effect.gen(function* () {
130 const target = yield* Target;
131 const mcp = yield* Mcp;
132 const identity = yield* target.newIdentity();
133 const bearer = yield* mcp.mintBearer(emailOf(identity));
134 const client = yield* connectModernMcpClient({
135 url: target.mcpUrl,
136 bearer,
137 mode: "auto",
138 });
139
140 expect(client.getProtocolEra(), "auto negotiation selected the modern path").toBe("modern");
141 expect(client.getNegotiatedProtocolVersion(), "auto selected the current revision").toBe(
142 MODERN_MCP_PROTOCOL_VERSION,
143 );
144 expect(
145 client.getDiscoverResult()?.supportedVersions,
146 "the probe result records the server's modern offer",
147 ).toContain(MODERN_MCP_PROTOCOL_VERSION);
148 expect(
149 (yield* Effect.promise(() => client.listTools())).tools.map((tool) => tool.name),
150 "the selected path is usable",
151 ).toContain("execute");
152 }),
153 ),
154);
155
156scenario(
157 "MCP modern protocol · native input_required resumes an approval-gated execution",
158 {},
159 Effect.scoped(
160 Effect.gen(function* () {
161 const target = yield* Target;
162 const { client: makeApiClient } = yield* Api;
163 const mcp = yield* Mcp;
164 const identity = yield* target.newIdentity();
165 const api = yield* makeApiClient(coreApi, identity);
166 const bearer = yield* mcp.mintBearer(emailOf(identity));
167 const pattern = `modern-native-${randomBytes(4).toString("hex")}.*`;
168 const code = `
169const result = await tools.executor.coreTools.policies.create({
170 owner: "user",
171 pattern: ${JSON.stringify(pattern)},
172 action: "block",
173});
174return JSON.stringify(result);
175`;
176
177 const cleanup = api.policies.list().pipe(
178 Effect.flatMap((policies) =>
179 Effect.forEach(
180 policies.filter((policy) => policy.pattern === pattern),
181 (policy) =>
182 api.policies
183 .remove({ params: { policyId: policy.id }, payload: { owner: "user" } })
184 .pipe(Effect.ignore),
185 ),
186 ),
187 Effect.ignore,
188 );
189
190 yield* Effect.gen(function* () {
191 const nativeUrl = new URL(target.mcpUrl);
192 nativeUrl.searchParams.set("elicitation_mode", "native");
193 const client = yield* connectModernMcpClient({
194 url: nativeUrl.toString(),
195 bearer,
196 mode: { pin: MODERN_MCP_PROTOCOL_VERSION },
197 manualInputRequired: true,
198 });
199
200 const first = yield* callModernToolWithInputRequired(client, {
201 name: "execute",
202 arguments: { code },
203 });
204 expect(isInputRequiredResult(first), "the gated action requests native input").toBe(true);
205 if (!isInputRequiredResult(first)) return;
206 expect(
207 first.inputRequests?.elicitation,
208 "the pause carries an elicitation request",
209 ).toMatchObject({ method: "elicitation/create" });
210 expect(first.requestState, "the continuation state is opaque and present").toEqual(
211 expect.any(String),
212 );
213
214 const completed = yield* callModernToolWithInputRequired(client, {
215 name: "execute",
216 arguments: { code },
217 inputResponses: { elicitation: { action: "accept", content: {} } },
218 requestState: first.requestState,
219 });
220 expect(isInputRequiredResult(completed), "the accepted round completes").toBe(false);
221 if (isInputRequiredResult(completed)) return;
222 expect(completed.isError, "the resumed execution succeeds").not.toBe(true);
223 expect(
224 modernToolText(completed),
225 "the tool result returns after the second round",
226 ).toContain('"ok":true');
227
228 expect(
229 (yield* api.policies.list()).map((policy) => policy.pattern),
230 "the approved side effect ran",
231 ).toContain(pattern);
232 }).pipe(Effect.ensuring(cleanup));
233 }),
234 ),
235);
e2e/local/cli-mcp-protocol.test.ts+0−104
@@ -1,104 +0,0 @@
1// Regression for #1449: a modern MCP client starts stdio negotiation with
2// `server/discover`, before `initialize`. This crosses the real CLI bridge:
3//
4// v2/v1 stdio client -> `executor mcp` -> local daemon HTTP MCP endpoint
5//
6// It reconnects with the v1 SDK too, proving discovery forwarding does not
7// regress established legacy stdio clients.
8import { expect } from "@effect/vitest";
9import { Client as ModernClient } from "@modelcontextprotocol/client";
10import { StdioClientTransport as ModernStdioClientTransport } from "@modelcontextprotocol/client/stdio";
11import { Client as LegacyClient } from "@modelcontextprotocol/sdk/client/index.js";
12import { StdioClientTransport as LegacyStdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
13import { Effect } from "effect";
14import { mkdtempSync, rmSync } from "node:fs";
15import { tmpdir } from "node:os";
16import { join } from "node:path";
17import { fileURLToPath } from "node:url";
18
19import { scenario } from "../src/scenario";
20import { stopAutoSpawnedDaemon } from "./daemon-process";
21
22const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
23const testScope = join(repoRoot, "apps/local");
24
25const bridgeCommand = (dataDir: string) => ({
26 command: "bun",
27 args: ["run", "dev:cli", "mcp", "--scope", testScope],
28 cwd: repoRoot,
29 env: {
30 ...process.env,
31 EXECUTOR_DATA_DIR: dataDir,
32 EXECUTOR_DISABLE_INTEGRATIONS_FETCH: "1",
33 } as Record<string, string>,
34 stderr: "pipe" as const,
35});
36
37const withTempData = Effect.acquireRelease(
38 Effect.sync(() => {
39 const root = mkdtempSync(join(tmpdir(), "executor-mcp-protocol-"));
40 return { root, dataDir: join(root, "data") };
41 }),
42 ({ root, dataDir }) =>
43 Effect.promise(async () => {
44 await stopAutoSpawnedDaemon(dataDir);
45 rmSync(root, { recursive: true, force: true });
46 }),
47);
48
49scenario(
50 "Local CLI MCP · modern discovery and legacy initialize cross the stdio bridge",
51 { timeout: 240_000 },
52 Effect.gen(function* () {
53 const { dataDir } = yield* withTempData;
54
55 const modernTransport = new ModernStdioClientTransport(bridgeCommand(dataDir));
56 const modernClient = new ModernClient(
57 { name: "executor-cli-modern-e2e", version: "1.0.0" },
58 { versionNegotiation: { mode: "auto" } },
59 );
60
61 yield* Effect.promise(async () => {
62 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: always reap the real CLI child when an assertion fails
63 try {
64 await modernClient.connect(modernTransport);
65 expect(modernClient.getProtocolEra()).toBe("modern");
66 expect((await modernClient.listTools()).tools.map(({ name }) => name)).toContain("execute");
67 const executed = await modernClient.callTool({
68 name: "execute",
69 arguments: { code: "return 42" },
70 });
71 expect(executed.structuredContent).toMatchObject({
72 status: "completed",
73 result: 42,
74 });
75 } finally {
76 await modernClient.close();
77 }
78 });
79
80 const legacyTransport = new LegacyStdioClientTransport(bridgeCommand(dataDir));
81 const legacyClient = new LegacyClient({
82 name: "executor-cli-legacy-e2e",
83 version: "1.0.0",
84 });
85
86 yield* Effect.promise(async () => {
87 // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: always reap the real CLI child when an assertion fails
88 try {
89 await legacyClient.connect(legacyTransport);
90 expect((await legacyClient.listTools()).tools.map(({ name }) => name)).toContain("execute");
91 const executed = await legacyClient.callTool({
92 name: "execute",
93 arguments: { code: "return 42" },
94 });
95 expect(executed.structuredContent).toMatchObject({
96 status: "completed",
97 result: 42,
98 });
99 } finally {
100 await legacyClient.close();
101 }
102 });
103 }).pipe(Effect.scoped),
104);
e2e/package.json+0−3
@@ -31,10 +31,7 @@ · 30 unmodified lines
31 "@executor-js/plugin-toolkits": "workspace:*",31 "@executor-js/plugin-toolkits": "workspace:*",
32 "@executor-js/sdk": "workspace:*",32 "@executor-js/sdk": "workspace:*",
33 "@kitlangton/terminal-control": "^0.3.0",33 "@kitlangton/terminal-control": "^0.3.0",
34 "@modelcontextprotocol/client": "2.0.0",
35 "@modelcontextprotocol/core": "2.0.0",
36 "@modelcontextprotocol/sdk": "^1.29.0",34 "@modelcontextprotocol/sdk": "^1.29.0",
37 "@modelcontextprotocol/server": "2.0.0",
38 "asciinema-player": "^3.15.1",35 "asciinema-player": "^3.15.1",
39 "effect": "catalog:",36 "effect": "catalog:",
40 "monaco-editor": "^0.55.1",37 "monaco-editor": "^0.55.1",
e2e/scenarios/mcp-modern-only-server.test.ts+0−131
@@ -1,131 +0,0 @@
1import { randomBytes } from "node:crypto";
2
3import { expect } from "@effect/vitest";
4import { Effect } from "effect";
5import { composePluginApi } from "@executor-js/api/server";
6import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api";
7import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared";
8
9import { serveModernOnlyMcp } from "../src/fixtures/modern-only-mcp";
10import { scenario } from "../src/scenario";
11import { Api, Target } from "../src/services";
12
13const api = composePluginApi([mcpHttpPlugin()] as const);
14const LEGACY_PROTOCOL_VERSION = "2025-03-26";
15
16const legacyInitialize = {
17 jsonrpc: "2.0" as const,
18 id: 1,
19 method: "initialize",
20 params: {
21 protocolVersion: LEGACY_PROTOCOL_VERSION,
22 capabilities: {},
23 clientInfo: { name: "executor-e2e-negative-control", version: "1.0.0" },
24 },
25};
26
27scenario(
28 "MCP outbound · Executor discovers and invokes a modern-only server that rejects legacy",
29 {},
30 Effect.scoped(
31 Effect.gen(function* () {
32 const target = yield* Target;
33 const { client: makeApiClient } = yield* Api;
34 const identity = yield* target.newIdentity();
35 const client = yield* makeApiClient(api, identity);
36 const fixture = yield* serveModernOnlyMcp();
37 const slug = IntegrationSlug.make(`modern_only_${randomBytes(4).toString("hex")}`);
38 const connectionName = ConnectionName.make("main");
39
40 const legacy = yield* Effect.promise(() =>
41 fetch(fixture.url, {
42 method: "POST",
43 headers: {
44 accept: "application/json, text/event-stream",
45 "content-type": "application/json",
46 },
47 body: JSON.stringify(legacyInitialize),
48 }),
49 );
50 expect(legacy.status, "the fixture rejects the legacy handshake on the wire").toBe(400);
51 expect(
52 yield* Effect.promise(() => legacy.json()),
53 "the negative control is the SDK's unsupported-version error",
54 ).toEqual({
55 jsonrpc: "2.0",
56 id: 1,
57 error: {
58 code: -32022,
59 message: `Unsupported protocol version: ${LEGACY_PROTOCOL_VERSION}`,
60 data: {
61 requested: LEGACY_PROTOCOL_VERSION,
62 supported: ["2026-07-28"],
63 },
64 },
65 });
66
67 yield* client.mcp.addServer({
68 payload: {
69 transport: "remote",
70 name: "Modern-only MCP",
71 endpoint: fixture.url,
72 slug: String(slug),
73 remoteTransport: "streamable-http",
74 },
75 });
76
77 yield* Effect.gen(function* () {
78 yield* client.connections.create({
79 payload: {
80 owner: "org",
81 name: connectionName,
82 integration: slug,
83 template: AuthTemplateSlug.make("none"),
84 value: "",
85 },
86 });
87
88 const catalog = yield* client.tools.list({ query: { integration: slug } });
89 expect(
90 catalog.map((tool) => String(tool.name)).sort(),
91 "catalog sync discovered both modern-only tools",
92 ).toEqual(["modern_identity", "modern_ping"]);
93
94 const executed = yield* client.executions.execute({
95 payload: {
96 code: `
97const result = await tools.${String(slug)}.org.main.modern_ping({});
98return { ok: result.ok, value: result.ok ? result.data : result.error };
99`,
100 autoApprove: true,
101 },
102 });
103 expect(executed.status, "the invocation completed through Executor").toBe("completed");
104 const outcome = JSON.parse(executed.text) as {
105 readonly ok?: boolean;
106 readonly value?: unknown;
107 };
108 expect(outcome.ok, `modern-only invocation result: ${executed.text}`).toBe(true);
109 expect(
110 JSON.stringify(outcome.value),
111 "the modern-only server's tool result returns through the sandbox",
112 ).toContain("pong-modern");
113 }).pipe(
114 Effect.ensuring(
115 Effect.gen(function* () {
116 yield* client.connections
117 .remove({
118 params: {
119 owner: "org",
120 integration: slug,
121 name: connectionName,
122 },
123 })
124 .pipe(Effect.ignore);
125 yield* client.mcp.removeServer({ params: { slug } }).pipe(Effect.ignore);
126 }),
127 ),
128 );
129 }),
130 ),
131);
e2e/selfhost/mcp-modern-protocol.test.ts+0−52
@@ -1,52 +0,0 @@
1import { expect } from "@effect/vitest";
2import { Effect } from "effect";
3
4import { scenario } from "../src/scenario";
5import { Mcp, Target } from "../src/services";
6import {
7 connectModernMcpClient,
8 MODERN_MCP_PROTOCOL_VERSION,
9 modernToolText,
10} from "../src/surfaces/modern-mcp";
11import type { Identity } from "../src/target";
12
13const emailOf = (identity: Identity): string => identity.credentials?.email ?? identity.label;
14
15scenario(
16 "MCP modern protocol · self-host accepts a pinned 2026 client end to end",
17 {},
18 Effect.scoped(
19 Effect.gen(function* () {
20 const target = yield* Target;
21 const mcp = yield* Mcp;
22 const identity = yield* target.newIdentity();
23 const bearer = yield* mcp.mintBearer(emailOf(identity));
24 const client = yield* connectModernMcpClient({
25 url: target.mcpUrl,
26 bearer,
27 mode: { pin: MODERN_MCP_PROTOCOL_VERSION },
28 });
29
30 expect(client.getProtocolEra(), "the self-host endpoint selects modern").toBe("modern");
31 expect(client.getNegotiatedProtocolVersion(), "the pinned revision is exact").toBe(
32 MODERN_MCP_PROTOCOL_VERSION,
33 );
34 expect(
35 client.getDiscoverResult()?.supportedVersions,
36 "self-host server/discover offers the pinned revision",
37 ).toContain(MODERN_MCP_PROTOCOL_VERSION);
38
39 const tools = yield* Effect.promise(() => client.listTools());
40 expect(
41 tools.tools.map((tool) => tool.name),
42 "self-host advertises Executor's tools over modern MCP",
43 ).toContain("execute");
44
45 const result = yield* Effect.promise(() =>
46 client.callTool({ name: "execute", arguments: { code: "return 20 + 22;" } }),
47 );
48 expect(result.isError, "the self-host modern call succeeds").not.toBe(true);
49 expect(modernToolText(result), "the sandbox result crosses the modern wire").toBe("42");
50 }),
51 ),
52);
e2e/setup/cloud.boot.ts+0−1
@@ -90,7 +90,6 @@ export const bootCloud = async (options: CloudBootOptions): Promise<CloudBooted> · 89 unmodified lines
90 // The AuthKit domain (MCP OAuth metadata + JWKS) is the emulator too.90 // The AuthKit domain (MCP OAuth metadata + JWKS) is the emulator too.
91 MCP_AUTHKIT_DOMAIN: workosUrl,91 MCP_AUTHKIT_DOMAIN: workosUrl,
92 MCP_RESOURCE_ORIGIN: options.publicUrl,92 MCP_RESOURCE_ORIGIN: options.publicUrl,
93 MCP_REQUEST_STATE_KEY: "e2e-mcp-request-state-key-0123456789abcdef",
94 MCP_SESSION_TIMEOUT_MS: process.env.MCP_SESSION_TIMEOUT_MS,93 MCP_SESSION_TIMEOUT_MS: process.env.MCP_SESSION_TIMEOUT_MS,
95 MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS: process.env.MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS,94 MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS: process.env.MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS,
96 ALLOW_LOCAL_NETWORK: "true",95 ALLOW_LOCAL_NETWORK: "true",
e2e/setup/cloudflare.boot.ts+0−2
@@ -48,8 +48,6 @@ export const bootCloudflare = async (options: CloudflareBootOptions): Promise<Bo · 47 unmodified lines
48 "--var",48 "--var",
49 "EXECUTOR_SECRET_KEY:e2e-secret-key-0123456789abcdef0123456789abcdef",49 "EXECUTOR_SECRET_KEY:e2e-secret-key-0123456789abcdef0123456789abcdef",
50 "--var",50 "--var",
51 "MCP_REQUEST_STATE_KEY:e2e-mcp-request-state-key-0123456789abcdef",
52 "--var",
53 "ALLOW_LOCAL_NETWORK:true",51 "ALLOW_LOCAL_NETWORK:true",
54 ],52 ],
55 cwd: cloudflareDir,53 cwd: cloudflareDir,
e2e/src/fixtures/modern-only-mcp.ts+0−83
@@ -1,83 +0,0 @@
1import { createServer, type IncomingHttpHeaders, type IncomingMessage } from "node:http";
2
3import { Effect, Scope } from "effect";
4import { createMcpHandler, McpServer } from "@modelcontextprotocol/server";
5
6/** Network address of a scoped modern-only MCP fixture. */
7export type ModernOnlyMcpFixture = {
8 readonly url: string;
9};
10
11const requestHeaders = (source: IncomingHttpHeaders): Headers => {
12 const headers = new Headers();
13 for (const [name, value] of Object.entries(source)) {
14 if (Array.isArray(value)) {
15 for (const item of value) headers.append(name, item);
16 } else if (value !== undefined) {
17 headers.set(name, value);
18 }
19 }
20 return headers;
21};
22
23const requestBody = (request: IncomingMessage): Promise<string> =>
24 new Promise((resolve) => {
25 const chunks: Uint8Array[] = [];
26 request.on("data", (chunk: Uint8Array) => chunks.push(chunk));
27 request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
28 });
29
30const makeServer = (): McpServer => {
31 const server = new McpServer({ name: "executor-modern-only-e2e", version: "1.0.0" });
32 server.registerTool(
33 "modern_ping",
34 { description: "Answers from a modern-only MCP server" },
35 async () => ({ content: [{ type: "text", text: "pong-modern" }] }),
36 );
37 server.registerTool(
38 "modern_identity",
39 { description: "Names the fixture's protocol posture" },
40 async () => ({ content: [{ type: "text", text: "modern-only" }] }),
41 );
42 return server;
43};
44
45/**
46 * Serve a real v2 MCP handler that rejects every legacy-classified request.
47 * The fixture follows the e2e suite's scoped, ephemeral localhost convention.
48 */
49export const serveModernOnlyMcp = (): Effect.Effect<ModernOnlyMcpFixture, never, Scope.Scope> =>
50 Effect.acquireRelease(
51 Effect.callback<ModernOnlyMcpFixture & { readonly close: () => Promise<void> }>((resume) => {
52 const handler = createMcpHandler(makeServer, { legacy: "reject" });
53 const server = createServer((incoming, outgoing) => {
54 void requestBody(incoming).then(async (body) => {
55 const host = incoming.headers.host ?? "127.0.0.1";
56 const request = new Request(new URL(incoming.url ?? "/", `http://${host}`), {
57 method: incoming.method,
58 headers: requestHeaders(incoming.headers),
59 ...(incoming.method === "GET" || incoming.method === "HEAD" ? {} : { body }),
60 });
61 const response = await handler.fetch(request);
62 outgoing.writeHead(response.status, Object.fromEntries(response.headers));
63 outgoing.end(Buffer.from(await response.arrayBuffer()));
64 });
65 });
66
67 server.listen(0, "127.0.0.1", () => {
68 const address = server.address();
69 const port = typeof address === "object" && address ? address.port : 0;
70 resume(
71 Effect.succeed({
72 url: `http://127.0.0.1:${port}/mcp`,
73 close: async () => {
74 server.close();
75 server.closeAllConnections();
76 await handler.close();
77 },
78 }),
79 );
80 });
81 }),
82 (fixture) => Effect.promise(fixture.close).pipe(Effect.ignore),
83 );
e2e/src/surfaces/modern-mcp.ts+0−112
@@ -1,112 +0,0 @@
1import { Effect } from "effect";
2import {
3 Client,
4 StreamableHTTPClientTransport,
5 withInputRequired,
6 type InputRequiredResult,
7 type Request as McpRequest,
8} from "@modelcontextprotocol/client";
9import { CallToolResultSchema } from "@modelcontextprotocol/core";
10
11/** The first MCP revision served through the modern per-request protocol. */
12export const MODERN_MCP_PROTOCOL_VERSION = "2026-07-28";
13
14/** Negotiation postures covered by the modern e2e client. */
15export type ModernMcpNegotiationMode =
16 | "auto"
17 | { readonly pin: typeof MODERN_MCP_PROTOCOL_VERSION };
18
19/** Connection inputs for a scoped modern MCP client. */
20export type ModernMcpClientOptions = {
21 readonly url: string;
22 readonly bearer: string;
23 readonly mode: ModernMcpNegotiationMode;
24 readonly manualInputRequired?: boolean;
25};
26
27const makeClient = (mode: ModernMcpNegotiationMode, manualInputRequired: boolean): Client =>
28 new Client(
29 { name: "executor-modern-e2e", version: "1.0.0" },
30 {
31 capabilities: { elicitation: { form: {} } },
32 versionNegotiation: { mode },
33 ...(manualInputRequired ? { inputRequired: { autoFulfill: false } } : {}),
34 },
35 );
36
37/**
38 * Connect a real v2 MCP client and close it when the surrounding Effect scope
39 * ends. The caller selects pinned or auto negotiation explicitly.
40 */
41export const connectModernMcpClient = (options: ModernMcpClientOptions) =>
42 Effect.acquireRelease(
43 Effect.promise(async () => {
44 const client = makeClient(options.mode, options.manualInputRequired ?? false);
45 const transport = new StreamableHTTPClientTransport(new URL(options.url), {
46 requestInit: { headers: { authorization: `Bearer ${options.bearer}` } },
47 });
48 await client.connect(transport);
49 return client;
50 }),
51 (client) => Effect.promise(() => client.close()).pipe(Effect.ignore),
52 );
53
54/**
55 * Issue a modern tool call in manual input-required mode, returning either the
56 * completed tool result or the server's next input request.
57 */
58export const callModernToolWithInputRequired = (
59 client: Client,
60 params: Record<string, unknown>,
61): Effect.Effect<InputRequiredResult | Awaited<ReturnType<Client["callTool"]>>> => {
62 const request: McpRequest = { method: "tools/call", params };
63 return Effect.promise(() =>
64 client.request(request, withInputRequired(CallToolResultSchema), {
65 allowInputRequired: true,
66 }),
67 );
68};
69
70/** Join the text content returned by an MCP tool call. */
71export const modernToolText = (result: Awaited<ReturnType<Client["callTool"]>>): string =>
72 result.content
73 .filter((part) => part.type === "text")
74 .map((part) => part.text)
75 .join("\n");
76
77/** The authentication response observed during an unauthenticated modern probe. */
78export type ModernMcpAuthChallenge = {
79 readonly status: number | undefined;
80 readonly wwwAuthenticate: string | null;
81};
82
83/**
84 * Drive the real pinned-modern probe without credentials and capture the HTTP
85 * authentication challenge that prevented the connection.
86 */
87export const readModernMcpAuthChallenge = (url: string): Effect.Effect<ModernMcpAuthChallenge> =>
88 Effect.promise(async () => {
89 let challenge: ModernMcpAuthChallenge = {
90 status: undefined,
91 wwwAuthenticate: null,
92 };
93 const client = makeClient({ pin: MODERN_MCP_PROTOCOL_VERSION }, false);
94 const transport = new StreamableHTTPClientTransport(new URL(url), {
95 fetch: async (input, init) => {
96 const response = await fetch(input, init);
97 if (response.status === 401) {
98 challenge = {
99 status: response.status,
100 wwwAuthenticate: response.headers.get("www-authenticate"),
101 };
102 }
103 return response;
104 },
105 });
106 await client.connect(transport).then(
107 () => undefined,
108 () => undefined,
109 );
110 await client.close();
111 return challenge;
112 });