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/src (2 of 91 files). Show all

Unified · Split

2 files
  1. e2e/src/fixtures/modern-only-mcp.ts +0 −83
  2. e2e/src/surfaces/modern-mcp.ts +0 −112
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− });