446 lines · 17.3 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Envelope regression tests — lock in the streamable-HTTP contract the shared// `McpServingRoutes` must preserve, independent of any provider:////   1. A method the transport doesn't serve (PUT/PATCH/…) -> 405 -32001.//   2. An OPTIONS preflight on a provider-declared discovery path -> 204 + CORS.//   3. A request-orchestration defect -> 500 -32603 + the McpErrorReporter fires.//// Built with minimal stub seams so the assertions target the envelope alone.// --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest";import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";import { Cause, Effect, Layer, Ref } from "effect";import { HttpRouter, HttpServer } from "effect/unstable/http"; import {  authenticated,  forbidden,  McpAuthProvider,  McpErrorReporter,  McpErrorReporterNoop,  McpModernServerBuilder,  McpServingRoutes,  McpDiscoveryRoutes,  McpSessionStore,  unauthorized,  type McpResource,  type McpDispatchResult,  type Principal,} from "./index";import type { ExecutionEngine } from "@executor-js/execution";import { EXTENSION_ID, RESOURCE_MIME_TYPE } from "./mcp-apps";import { buildMcpServer } from "./tool-server"; const DISCOVERY_PATH = "/.well-known/oauth-protected-resource" as const; const TEST_PRINCIPAL: Principal = {  accountId: "acct_test",  organizationId: "org_test",  organizationName: "Test Org",  email: "test@example.com",  name: "Test",  avatarUrl: null,  roles: ["user"],}; const testEngine: ExecutionEngine = {  execute: (code) => Effect.succeed({ result: `ran: ${code}` }),  executeWithPause: (code) =>    Effect.succeed({ status: "completed", result: { result: `ran: ${code}` } }),  resume: () => Effect.succeed(null),  isExecutionSettled: () => Effect.succeed(false),  getPausedExecution: () => Effect.succeed(null),  pausedExecutionCount: () => Effect.succeed(0),  hasPausedExecutions: () => Effect.succeed(false),  getDescription: Effect.succeed("envelope test executor"),}; const ModernBuilderLive = Layer.succeed(McpModernServerBuilder)({  build: (_principal, options) => {    const { resource: _resource, ...requestOptions } = options;    return buildMcpServer({ engine: testEngine, ...requestOptions });  },}); /** An auth provider that authenticates everything (so dispatch is reached). */const AuthProviderLive = Layer.succeed(McpAuthProvider)({  discoveryRoutes: [    {      path: DISCOVERY_PATH,      handler: () => Effect.succeed(new Response(JSON.stringify({ ok: true }), { status: 200 })),    },  ],  resourceMetadataUrl: (request) => `${new URL(request.url).origin}${DISCOVERY_PATH}`,  authenticate: () => Effect.succeed(authenticated(TEST_PRINCIPAL)),}); /** A store whose dispatch dies — induces the orchestration defect for case 3. */const DefectStoreLive = Layer.succeed(McpSessionStore)({  dispatch: (): Effect.Effect<McpDispatchResult> => Effect.die("induced defect"),  dispose: () => Effect.void,}); /** A store whose dispatch never runs — used for the 405 case (rejected first). */const OkStoreLive = Layer.succeed(McpSessionStore)({  dispatch: (): Effect.Effect<McpDispatchResult> =>    Effect.succeed(new Response(JSON.stringify({ jsonrpc: "2.0", id: 1 }), { status: 200 })),  dispose: () => Effect.void,}); const buildHandler = (  store: Layer.Layer<McpSessionStore>,  reporter: Layer.Layer<McpErrorReporter>,  authProvider: Layer.Layer<McpAuthProvider> = AuthProviderLive,  modernBuilder: Layer.Layer<McpModernServerBuilder> = ModernBuilderLive,): ((request: Request) => Promise<Response>) => {  const Seams = Layer.mergeAll(authProvider, store, modernBuilder, reporter);  const RouteLive = McpServingRoutes.pipe(    HttpRouter.provideRequest(Seams),    Layer.provide(authProvider),  );  return HttpRouter.toWebHandler(RouteLive.pipe(Layer.provideMerge(HttpServer.layerServices)))    .handler;}; describe("McpServingRoutes envelope", () => {  it("rejects HEAD with a JSON-RPC 405 before dispatch", async () => {    const handler = buildHandler(OkStoreLive, McpErrorReporterNoop);    const response = await handler(new Request("https://host.test/mcp", { method: "HEAD" }));    expect(response.status).toBe(405);    expect(response.headers.get("content-type")).toContain("application/json");  });   it("rejects a non-GET/POST/DELETE/OPTIONS method with 405 -32001 before dispatch", async () => {    const handler = buildHandler(OkStoreLive, McpErrorReporterNoop);    for (const method of ["PUT", "PATCH"] as const) {      const response = await handler(        new Request("https://host.test/mcp", {          method,          headers: { authorization: "Bearer x", "content-type": "application/json" },          body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),        }),      );      expect(response.status, `${method} should be 405`).toBe(405);      const body = (await response.json()) as { error: { code: number; message: string } };      expect(body.error.code).toBe(-32001);      expect(body.error.message).toMatch(/method not allowed/i);    }  });   it("answers an OPTIONS preflight on a discovery path with 204 + CORS", async () => {    const handler = buildHandler(OkStoreLive, McpErrorReporterNoop);    const response = await handler(      new Request(`https://host.test${DISCOVERY_PATH}`, {        method: "OPTIONS",        headers: { origin: "https://claude.ai", "access-control-request-method": "GET" },      }),    );    expect(response.status).toBe(204);    expect(response.headers.get("access-control-allow-origin")).toBe("*");    expect(response.headers.get("access-control-allow-methods")).toBe("GET, POST, DELETE, OPTIONS");    const allowedHeaders = response.headers.get("access-control-allow-headers") ?? "";    expect(allowedHeaders).toContain("authorization");    expect(allowedHeaders).toContain("mcp-method");    expect(allowedHeaders).toContain("mcp-name");  });   it("echoes requested preflight headers so dynamic Mcp-Param names pass", async () => {    const handler = buildHandler(OkStoreLive, McpErrorReporterNoop);    const requested = "content-type, authorization, mcp-protocol-version, mcp-param-search";    const response = await handler(      new Request("https://host.test/mcp", {        method: "OPTIONS",        headers: {          origin: "https://claude.ai",          "access-control-request-method": "POST",          "access-control-request-headers": requested,        },      }),    );    expect(response.status).toBe(204);    expect(response.headers.get("access-control-allow-headers")).toBe(requested);  });   it("serves modern list/call traffic without dispatching a legacy session", async () => {    const legacyDispatches = await Effect.runPromise(Ref.make(0));    const appsEnabled = await Effect.runPromise(Ref.make(false));    const RecordingStoreLive = Layer.succeed(McpSessionStore)({      dispatch: () =>        Ref.update(legacyDispatches, (count) => count + 1).pipe(Effect.as("not-found")),      dispose: () => Effect.void,    });    const RecordingModernBuilder = Layer.succeed(McpModernServerBuilder)({      build: (_principal, options) => {        const { resource: _resource, ...requestOptions } = options;        return Ref.set(appsEnabled, options.appsEnabled).pipe(          Effect.flatMap(() => buildMcpServer({ engine: testEngine, ...requestOptions })),        );      },    });    const handler = buildHandler(      RecordingStoreLive,      McpErrorReporterNoop,      AuthProviderLive,      RecordingModernBuilder,    );    const transport = new StreamableHTTPClientTransport(new URL("https://host.test/mcp"), {      fetch: (input, init) =>        handler(          input instanceof Request ? new Request(input, init) : new Request(input.toString(), init),        ),    });    const client = new Client(      { name: "envelope-modern-test", version: "1.0.0" },      {        capabilities: {          extensions: { [EXTENSION_ID]: { mimeTypes: [RESOURCE_MIME_TYPE] } },        },        versionNegotiation: { mode: { pin: "2026-07-28" } },      },    );     await client.connect(transport);    // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: always close the in-process modern client    try {      expect((await client.listTools()).tools.map(({ name }) => name)).toContain("execute");      const result = await client.callTool({        name: "execute",        arguments: { code: "1 + 1" },      });      expect(result.content).toEqual([{ type: "text", text: "ran: 1 + 1" }]);      expect(await Effect.runPromise(Ref.get(legacyDispatches))).toBe(0);      expect(await Effect.runPromise(Ref.get(appsEnabled))).toBe(true);    } finally {      await client.close();    }  });   it("returns the existing 401 challenge before routing a modern request", async () => {    const challenge = 'Bearer resource_metadata="https://host.test/custom-metadata"';    const UnauthorizedAuthProviderLive = Layer.succeed(McpAuthProvider)({      discoveryRoutes: [],      resourceMetadataUrl: () => "https://host.test/custom-metadata",      authenticate: () => Effect.succeed(unauthorized(challenge)),    });    const handler = buildHandler(OkStoreLive, McpErrorReporterNoop, UnauthorizedAuthProviderLive);    const response = await handler(modernRequest("https://host.test/mcp"));     expect(response.status).toBe(401);    expect(response.headers.get("www-authenticate")).toBe(challenge);  });   it("gracefully rejects modern discovery when inbound 2026-07-28 is disabled", async () => {    const DisabledModernBuilder = Layer.succeed(McpModernServerBuilder)({      enabled: false,      build: () => Effect.die("disabled modern builder should not run"),    });    const handler = buildHandler(      OkStoreLive,      McpErrorReporterNoop,      AuthProviderLive,      DisabledModernBuilder,    );     const response = await handler(modernRequest("https://host.test/mcp"));    expect(response.status).toBe(400);    expect(await response.json()).toEqual({      jsonrpc: "2.0",      error: { code: -32022, message: "MCP 2026-07-28 support is disabled" },      id: null,    });  });   it("answers a dead-session standalone GET with 405 so old clients stop retrying", async () => {    const NotFoundStoreLive = Layer.succeed(McpSessionStore)({      dispatch: (): Effect.Effect<McpDispatchResult> => Effect.succeed("not-found"),      dispose: () => Effect.void,    });    const handler = buildHandler(NotFoundStoreLive, McpErrorReporterNoop);     const get = await handler(      new Request("https://host.test/mcp", {        method: "GET",        headers: {          authorization: "Bearer x",          accept: "text/event-stream",          "mcp-session-id": "dead-session",          "mcp-protocol-version": "2025-06-18",        },      }),    );    expect(get.status).toBe(405);    expect(get.headers.get("allow")).toBe("POST, DELETE");    expect(await get.json()).toMatchObject({ error: { code: -32001 } });     const post = await handler(      new Request("https://host.test/mcp", {        method: "POST",        headers: {          authorization: "Bearer x",          "content-type": "application/json",          "mcp-session-id": "dead-session",          "mcp-protocol-version": "2025-06-18",        },        body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),      }),    );    expect(post.status).toBe(404);  });   it("404s a modern request whose toolkit route is not served", async () => {    const handler = buildHandler(OkStoreLive, McpErrorReporterNoop);    const response = await handler(modernRequest("https://host.test/mcp/toolkits/unknown/extra"));    expect(response.status).toBe(404);  });   it("renders 500 -32603 + CORS and fires the reporter on an orchestration defect", async () => {    const reported = await Effect.runPromise(Ref.make<ReadonlyArray<string>>([]));    const RecordingReporter = Layer.succeed(McpErrorReporter)({      report: (cause: Cause.Cause<unknown>) =>        Ref.update(reported, (acc) => [...acc, Cause.pretty(cause)]),    });     const handler = buildHandler(DefectStoreLive, RecordingReporter);    const response = await handler(      new Request("https://host.test/mcp", {        method: "POST",        headers: { authorization: "Bearer x", "content-type": "application/json" },        body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),      }),    );     expect(response.status).toBe(500);    expect(response.headers.get("access-control-allow-origin")).toBe("*");    const body = (await response.json()) as { error: { code: number; message: string } };    expect(body.error.code).toBe(-32603);    expect(body.error.message).toMatch(/internal server error/i);     const captures = await Effect.runPromise(Ref.get(reported));    expect(captures).toHaveLength(1);    expect(captures[0]).toContain("induced defect");  });   it("does not dispose a session id on an auth-level Forbidden outcome", async () => {    const disposed = await Effect.runPromise(Ref.make<ReadonlyArray<string>>([]));    const ForbiddenAuthProviderLive = Layer.succeed(McpAuthProvider)({      discoveryRoutes: [],      resourceMetadataUrl: (request) => `${new URL(request.url).origin}${DISCOVERY_PATH}`,      authenticate: () => Effect.succeed(forbidden("No organization in session", -32001)),    });    const RecordingStoreLive = Layer.succeed(McpSessionStore)({      dispatch: (): Effect.Effect<McpDispatchResult> => Effect.die("dispatch should not run"),      dispose: (sessionId) => Ref.update(disposed, (ids) => [...ids, sessionId]),    });     const handler = buildHandler(      RecordingStoreLive,      McpErrorReporterNoop,      ForbiddenAuthProviderLive,    );    const response = await handler(      new Request("https://host.test/mcp/toolkits/deploy", {        method: "POST",        headers: {          authorization: "Bearer x",          "mcp-session-id": "leaked-session",          "content-type": "application/json",        },        body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),      }),    );     expect(response.status).toBe(403);    expect(await Effect.runPromise(Ref.get(disposed))).toEqual([]);  });}); const modernRequest = (url: string): Request =>  new Request(url, {    method: "POST",    headers: {      "content-type": "application/json",      "mcp-protocol-version": "2026-07-28",      "mcp-method": "server/discover",    },    body: JSON.stringify({      jsonrpc: "2.0",      id: 1,      method: "server/discover",      params: {        _meta: {          "io.modelcontextprotocol/protocolVersion": "2026-07-28",          "io.modelcontextprotocol/clientCapabilities": {},        },      },    }),  }); it("dispatches toolkit MCP routes with the parsed toolkit resource", async () => {  const seen = await Effect.runPromise(Ref.make<McpResource | null>(null));  const RecordingStoreLive = Layer.succeed(McpSessionStore)({    dispatch: ({ resource }): Effect.Effect<McpDispatchResult> =>      Ref.set(seen, resource).pipe(        Effect.as(new Response(JSON.stringify({ jsonrpc: "2.0", id: 1 }), { status: 200 })),      ),    dispose: () => Effect.void,  });   const handler = buildHandler(RecordingStoreLive, McpErrorReporterNoop);  const response = await handler(    new Request("https://host.test/mcp/toolkits/deploy", {      method: "POST",      headers: { authorization: "Bearer x", "content-type": "application/json" },      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),    }),  );   expect(response.status).toBe(200);  expect(await Effect.runPromise(Ref.get(seen))).toEqual({    kind: "toolkit",    slug: "deploy",  });}); describe("McpDiscoveryRoutes (discovery-only, no session store)", () => {  // Builds with the auth seam ALONE — no McpSessionStore. This is the cloud  // shape: the Agent bridge serves /mcp transport, the envelope only publishes  // the provider's OAuth discovery docs. If this required McpSessionStore it  // would not compile, so the build itself is part of the assertion.  const discoveryHandler = (): ((request: Request) => Promise<Response>) =>    HttpRouter.toWebHandler(      McpDiscoveryRoutes.pipe(        Layer.provide(AuthProviderLive),        Layer.provideMerge(HttpServer.layerServices),      ),    ).handler;   it("serves the provider discovery document on GET", async () => {    const handler = discoveryHandler();    const response = await handler(new Request(`https://host.test${DISCOVERY_PATH}`));    expect(response.status).toBe(200);    expect((await response.json()) as { ok: boolean }).toEqual({ ok: true });  });   it("answers an OPTIONS preflight on a discovery path with 204 + CORS", async () => {    const handler = discoveryHandler();    const response = await handler(      new Request(`https://host.test${DISCOVERY_PATH}`, { method: "OPTIONS" }),    );    expect(response.status).toBe(204);    expect(response.headers.get("access-control-allow-origin")).toBe("*");  });   it("does NOT mount the /mcp transport route", async () => {    const handler = discoveryHandler();    const response = await handler(      new Request("https://host.test/mcp", {        method: "POST",        headers: { authorization: "Bearer x", "content-type": "application/json" },        body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),      }),    );    expect(response.status).toBe(404);  });});

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t