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 @@
1−import { randomBytes, randomUUID } from "node:crypto";
2−
3−import { expect } from "@effect/vitest";
4−import { Effect } from "effect";
5−import { composePluginApi } from "@executor-js/api/server";
6−import { isInputRequiredResult } from "@modelcontextprotocol/client";
7−
8−import { scenario } from "../src/scenario";
9−import { Api, Mcp, Target } from "../src/services";
10−import {
11− callModernToolWithInputRequired,
12− connectModernMcpClient,
13− MODERN_MCP_PROTOCOL_VERSION,
14− modernToolText,
15− readModernMcpAuthChallenge,
16−} from "../src/surfaces/modern-mcp";
17−import type { Identity } from "../src/target";
18−
19−const coreApi = composePluginApi([] as const);
20−const LEGACY_PROTOCOL_VERSION = "2025-03-26";
21−const JSON_AND_SSE = "application/json, text/event-stream";
22−
23−const emailOf = (identity: Identity): string => identity.credentials?.email ?? identity.label;
24−
25−const 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−
36−const 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−
53−scenario(
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−
125−scenario(
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−
156−scenario(
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 = `
169−const result = await tools.executor.coreTools.policies.create({
170− owner: "user",
171− pattern: ${JSON.stringify(pattern)},
172− action: "block",
173−});
174−return 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.
8−import { expect } from "@effect/vitest";
9−import { Client as ModernClient } from "@modelcontextprotocol/client";
10−import { StdioClientTransport as ModernStdioClientTransport } from "@modelcontextprotocol/client/stdio";
11−import { Client as LegacyClient } from "@modelcontextprotocol/sdk/client/index.js";
12−import { StdioClientTransport as LegacyStdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
13−import { Effect } from "effect";
14−import { mkdtempSync, rmSync } from "node:fs";
15−import { tmpdir } from "node:os";
16−import { join } from "node:path";
17−import { fileURLToPath } from "node:url";
18−
19−import { scenario } from "../src/scenario";
20−import { stopAutoSpawnedDaemon } from "./daemon-process";
21−
22−const repoRoot = fileURLToPath(new URL("../../", import.meta.url));
23−const testScope = join(repoRoot, "apps/local");
24−
25−const 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−
37−const 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−
49−scenario(
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
3131 "@executor-js/plugin-toolkits": "workspace:*",
3232 "@executor-js/sdk": "workspace:*",
3333 "@kitlangton/terminal-control": "^0.3.0",
34− "@modelcontextprotocol/client": "2.0.0",
35− "@modelcontextprotocol/core": "2.0.0",
3634 "@modelcontextprotocol/sdk": "^1.29.0",
37− "@modelcontextprotocol/server": "2.0.0",
3835 "asciinema-player": "^3.15.1",
3936 "effect": "catalog:",
4037 "monaco-editor": "^0.55.1",
e2e/scenarios/mcp-modern-only-server.test.ts+0−131
@@ -1,131 +0,0 @@
1−import { randomBytes } from "node:crypto";
2−
3−import { expect } from "@effect/vitest";
4−import { Effect } from "effect";
5−import { composePluginApi } from "@executor-js/api/server";
6−import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api";
7−import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/sdk/shared";
8−
9−import { serveModernOnlyMcp } from "../src/fixtures/modern-only-mcp";
10−import { scenario } from "../src/scenario";
11−import { Api, Target } from "../src/services";
12−
13−const api = composePluginApi([mcpHttpPlugin()] as const);
14−const LEGACY_PROTOCOL_VERSION = "2025-03-26";
15−
16−const 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−
27−scenario(
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: `
97−const result = await tools.${String(slug)}.org.main.modern_ping({});
98−return { 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 @@
1−import { expect } from "@effect/vitest";
2−import { Effect } from "effect";
3−
4−import { scenario } from "../src/scenario";
5−import { Mcp, Target } from "../src/services";
6−import {
7− connectModernMcpClient,
8− MODERN_MCP_PROTOCOL_VERSION,
9− modernToolText,
10−} from "../src/surfaces/modern-mcp";
11−import type { Identity } from "../src/target";
12−
13−const emailOf = (identity: Identity): string => identity.credentials?.email ?? identity.label;
14−
15−scenario(
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
9090 // The AuthKit domain (MCP OAuth metadata + JWKS) is the emulator too.
9191 MCP_AUTHKIT_DOMAIN: workosUrl,
9292 MCP_RESOURCE_ORIGIN: options.publicUrl,
93− MCP_REQUEST_STATE_KEY: "e2e-mcp-request-state-key-0123456789abcdef",
9493 MCP_SESSION_TIMEOUT_MS: process.env.MCP_SESSION_TIMEOUT_MS,
9594 MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS: process.env.MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS,
9695 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
4848 "--var",
4949 "EXECUTOR_SECRET_KEY:e2e-secret-key-0123456789abcdef0123456789abcdef",
5050 "--var",
51− "MCP_REQUEST_STATE_KEY:e2e-mcp-request-state-key-0123456789abcdef",
52− "--var",
5351 "ALLOW_LOCAL_NETWORK:true",
5452 ],
5553 cwd: cloudflareDir,
e2e/src/fixtures/modern-only-mcp.ts+0−83
@@ -1,83 +0,0 @@
1−import { createServer, type IncomingHttpHeaders, type IncomingMessage } from "node:http";
2−
3−import { Effect, Scope } from "effect";
4−import { createMcpHandler, McpServer } from "@modelcontextprotocol/server";
5−
6−/** Network address of a scoped modern-only MCP fixture. */
7−export type ModernOnlyMcpFixture = {
8− readonly url: string;
9−};
10−
11−const 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−
23−const 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−
30−const 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− */
49−export 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 @@
1−import { Effect } from "effect";
2−import {
3− Client,
4− StreamableHTTPClientTransport,
5− withInputRequired,
6− type InputRequiredResult,
7− type Request as McpRequest,
8−} from "@modelcontextprotocol/client";
9−import { CallToolResultSchema } from "@modelcontextprotocol/core";
10−
11−/** The first MCP revision served through the modern per-request protocol. */
12−export const MODERN_MCP_PROTOCOL_VERSION = "2026-07-28";
13−
14−/** Negotiation postures covered by the modern e2e client. */
15−export type ModernMcpNegotiationMode =
16− | "auto"
17− | { readonly pin: typeof MODERN_MCP_PROTOCOL_VERSION };
18−
19−/** Connection inputs for a scoped modern MCP client. */
20−export type ModernMcpClientOptions = {
21− readonly url: string;
22− readonly bearer: string;
23− readonly mode: ModernMcpNegotiationMode;
24− readonly manualInputRequired?: boolean;
25−};
26−
27−const 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− */
41−export 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− */
58−export 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. */
71−export 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. */
78−export 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− */
87−export 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− });