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

Unified · Split

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