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
365365 "@executor-js/plugin-toolkits": "workspace:*",
366366 "@executor-js/sdk": "workspace:*",
367367 "@kitlangton/terminal-control": "^0.3.0",
368+ "@modelcontextprotocol/client": "2.0.0",
369+ "@modelcontextprotocol/core": "2.0.0",
368370 "@modelcontextprotocol/sdk": "^1.29.0",
371+ "@modelcontextprotocol/server": "2.0.0",
369372 "asciinema-player": "^3.15.1",
370373 "effect": "catalog:",
371374 "monaco-editor": "^0.55.1",
e2e/cloud/mcp-modern-protocol.test.ts+235−0
@@ -0,0 +1,235 @@
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/package.json+3−0
@@ -30,7 +30,10 @@ · 29 unmodified lines
3030 "@executor-js/plugin-toolkits": "workspace:*",
3131 "@executor-js/sdk": "workspace:*",
3232 "@kitlangton/terminal-control": "^0.3.0",
33+ "@modelcontextprotocol/client": "2.0.0",
34+ "@modelcontextprotocol/core": "2.0.0",
3335 "@modelcontextprotocol/sdk": "^1.29.0",
36+ "@modelcontextprotocol/server": "2.0.0",
3437 "asciinema-player": "^3.15.1",
3538 "effect": "catalog:",
3639 "monaco-editor": "^0.55.1",
e2e/scenarios/mcp-modern-only-server.test.ts+131−0
@@ -0,0 +1,131 @@
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+52−0
@@ -0,0 +1,52 @@
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/src/fixtures/modern-only-mcp.ts+83−0
@@ -0,0 +1,83 @@
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+112−0
@@ -0,0 +1,112 @@
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+ });