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 @@
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 });