History

Add modern-spec e2e coverage in both protocol directions

Rhys Sullivan committed 2 months ago

Commit c25ace21 parent 803b0877 files · +619 −0Browse files

Unified · Split

7 files
  1. bun.lock +3 −0
  2. e2e/cloud/mcp-modern-protocol.test.ts +235 −0
  3. e2e/package.json +3 −0
  4. e2e/scenarios/mcp-modern-only-server.test.ts +131 −0
  5. e2e/selfhost/mcp-modern-protocol.test.ts +52 −0
  6. e2e/src/fixtures/modern-only-mcp.ts +83 −0
  7. e2e/src/surfaces/modern-mcp.ts +112 −0
bun.lock+3−0
@@ -365,7 +365,10 @@ · 364 unmodified lines
365 "@executor-js/plugin-toolkits": "workspace:*",365 "@executor-js/plugin-toolkits": "workspace:*",
366 "@executor-js/sdk": "workspace:*",366 "@executor-js/sdk": "workspace:*",
367 "@kitlangton/terminal-control": "^0.3.0",367 "@kitlangton/terminal-control": "^0.3.0",
368 "@modelcontextprotocol/client": "2.0.0",
369 "@modelcontextprotocol/core": "2.0.0",
368 "@modelcontextprotocol/sdk": "^1.29.0",370 "@modelcontextprotocol/sdk": "^1.29.0",
371 "@modelcontextprotocol/server": "2.0.0",
369 "asciinema-player": "^3.15.1",372 "asciinema-player": "^3.15.1",
370 "effect": "catalog:",373 "effect": "catalog:",
371 "monaco-editor": "^0.55.1",374 "monaco-editor": "^0.55.1",
e2e/cloud/mcp-modern-protocol.test.ts+235−0
@@ -0,0 +1,235 @@
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/package.json+3−0
@@ -30,7 +30,10 @@ · 29 unmodified lines
30 "@executor-js/plugin-toolkits": "workspace:*",30 "@executor-js/plugin-toolkits": "workspace:*",
31 "@executor-js/sdk": "workspace:*",31 "@executor-js/sdk": "workspace:*",
32 "@kitlangton/terminal-control": "^0.3.0",32 "@kitlangton/terminal-control": "^0.3.0",
33 "@modelcontextprotocol/client": "2.0.0",
34 "@modelcontextprotocol/core": "2.0.0",
33 "@modelcontextprotocol/sdk": "^1.29.0",35 "@modelcontextprotocol/sdk": "^1.29.0",
36 "@modelcontextprotocol/server": "2.0.0",
34 "asciinema-player": "^3.15.1",37 "asciinema-player": "^3.15.1",
35 "effect": "catalog:",38 "effect": "catalog:",
36 "monaco-editor": "^0.55.1",39 "monaco-editor": "^0.55.1",
e2e/scenarios/mcp-modern-only-server.test.ts+131−0
@@ -0,0 +1,131 @@
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+52−0
@@ -0,0 +1,52 @@
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/src/fixtures/modern-only-mcp.ts+83−0
@@ -0,0 +1,83 @@
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+112−0
@@ -0,0 +1,112 @@
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 });