1575 lines · 58.8 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Cause, Effect, Exit, Layer, Option, Predicate, Schema, Tracer } from "effect";import {  HttpClient,  HttpClientRequest,  HttpClientResponse,  HttpServerResponse,} from "effect/unstable/http"; import {  AuthTemplateSlug,  ConnectionName,  IntegrationSlug,  OAuthClientSlug,  ToolAddress,  createExecutor,} from "@executor-js/sdk";import {  makeTestConfig,  memoryCredentialsPlugin,  scopesFromAuthorizeUrl,  serveOAuthTestServer,  serveTestHttpApp,} from "@executor-js/sdk/testing"; import { createMcpConnector } from "./connection";import { mcpPlugin, userFacingProbeMessage, toIntegrationConfig } from "./plugin";import { McpInvocationError } from "./errors";import { extractManifestFromListToolsResult, deriveMcpNamespace, joinToolPath } from "./manifest";import { makeAnnotationsMcpServer, serveMcpServer } from "../testing"; // removed: the v1 addSource / scopes / secrets / credential-binding / usages /// sources.configure / multi-scope shadowing suites. v2 has no scope stack, no// secrets table, and no credential bindings — an MCP server is registered as an// integration (`addServer`) and a connection IS the credential (created via// `connections.create` / `oauth.start`). Owner isolation is covered by// owner-isolation.test.ts; the end-to-end auth/header path is covered by// elicitation.test.ts + owner-isolation.test.ts. const TEMPLATE = AuthTemplateSlug.make("none"); const JsonRpcId = Schema.Union([Schema.String, Schema.Number, Schema.Null]);const JsonRpcRequest = Schema.Struct({  id: Schema.optional(JsonRpcId),  method: Schema.String,});type JsonRpcRequest = typeof JsonRpcRequest.Type; const decodeJsonRpcRequest = Schema.decodeUnknownOption(Schema.fromJsonString(JsonRpcRequest)); const jsonRpcResult = (request: JsonRpcRequest, result: unknown) =>  HttpServerResponse.jsonUnsafe({    jsonrpc: "2.0",    id: request.id ?? null,    result,  }); // The call-tool fixtures share one JSON-RPC scaffold (handshake, tool listing,// unknown-method rejection); only the `tools/call` response varies. Each// scenario supplies that branch via a `CallToolResponder`.type CallToolResponder = (rpc: JsonRpcRequest) => ReturnType<typeof HttpServerResponse.text>; const callToolFixtureResponse = (rpc: JsonRpcRequest, callTool: CallToolResponder) => {  if (rpc.method === "initialize") {    return jsonRpcResult(rpc, {      protocolVersion: "2025-06-18",      capabilities: { tools: {} },      serverInfo: { name: "call-tool-fixture", version: "1.0.0" },    });  }  if (rpc.method === "notifications/initialized") {    return HttpServerResponse.text("", { status: 202 });  }  if (rpc.method === "tools/list") {    return jsonRpcResult(rpc, {      tools: [        {          name: "explode",          description: "Returns a failure from tools/call",          inputSchema: { type: "object", properties: {} },        },      ],    });  }  if (rpc.method === "tools/call") {    return callTool(rpc);  }  return HttpServerResponse.text("Unexpected JSON-RPC method", { status: 400 });}; const serveCallToolServer = (callTool: CallToolResponder) =>  serveTestHttpApp((request) =>    Effect.gen(function* () {      if (request.method === "GET") {        return HttpServerResponse.text("SSE disabled", { status: 405 });      }       const body = yield* request.text.pipe(Effect.orDie);      return Option.match(decodeJsonRpcRequest(body), {        onNone: () => HttpServerResponse.text("Invalid JSON-RPC fixture request", { status: 400 }),        onSome: (rpc) => callToolFixtureResponse(rpc, callTool),      });    }),  ); const rejectedOAuthDiscoveryLayer = (endpoint: string) => {  const issuer = new URL(endpoint).origin;  const requests: string[] = [];  const layer = Layer.succeed(HttpClient.HttpClient)(    HttpClient.make((request: HttpClientRequest.HttpClientRequest) => {      requests.push(request.url);      const url = new URL(request.url);      const response =        request.url === endpoint          ? new Response("", { status: 401 })          : url.pathname === "/.well-known/oauth-protected-resource/mcp"            ? Response.json({ resource: endpoint, authorization_servers: [issuer] })            : url.pathname === "/.well-known/oauth-authorization-server"              ? Response.json({                  issuer,                  authorization_endpoint: `${issuer}/authorize`,                  token_endpoint: `${issuer}/token`,                  registration_endpoint: `${issuer}/register`,                  response_types_supported: ["code"],                  code_challenge_methods_supported: ["S256"],                })              : url.pathname === "/register"                ? Response.json(                    {                      client_id: "replacement-client",                      redirect_uris: ["http://localhost/oauth/callback"],                      grant_types: ["authorization_code", "refresh_token"],                      response_types: ["code"],                      token_endpoint_auth_method: "none",                    },                    { status: 201 },                  )                : new Response("unexpected request", { status: 500 });      return Effect.succeed(HttpClientResponse.fromWeb(request, response));    }),  );  return { layer, requests };}; const clientRpcOf = (request: HttpClientRequest.HttpClientRequest): JsonRpcRequest | undefined =>  Predicate.isTagged(request.body, "Uint8Array")    ? Option.getOrUndefined(decodeJsonRpcRequest(new TextDecoder().decode(request.body.body)))    : undefined; /** Streamable-http fixture for the post-handshake auth wall: the handshake *  methods succeed and `tools/list` answers 401 for the first *  `revokedListResponses` calls (Infinity = the bearer stays revoked). The *  OAuth discovery + DCR endpoints are served so an SDK fallback that DID see *  the 401 could register — the ledger proves it never gets there. Entries are *  `pathname` or `pathname#jsonRpcMethod`. */const listRejectionFixtureLayer = (endpoint: string, revokedListResponses: number) => {  const issuer = new URL(endpoint).origin;  const requests: string[] = [];  let remaining = revokedListResponses;  const jsonRpc = (rpc: JsonRpcRequest, result: unknown) =>    Response.json({ jsonrpc: "2.0", id: rpc.id ?? null, result });  const layer = Layer.succeed(HttpClient.HttpClient)(    HttpClient.make((request: HttpClientRequest.HttpClientRequest) => {      const url = new URL(request.url);      const rpc = request.url === endpoint ? clientRpcOf(request) : undefined;      requests.push(rpc === undefined ? url.pathname : `${url.pathname}#${rpc.method}`);      const respond = (response: Response) =>        Effect.succeed(HttpClientResponse.fromWeb(request, response));      if (request.url === endpoint) {        if (rpc === undefined) return respond(new Response("SSE disabled", { status: 405 }));        if (rpc.method === "initialize") {          return respond(            jsonRpc(rpc, {              protocolVersion: "2025-06-18",              capabilities: { tools: {} },              serverInfo: { name: "list-reject-fixture", version: "1.0.0" },            }),          );        }        if (rpc.method === "notifications/initialized") {          return respond(new Response("", { status: 202 }));        }        if (rpc.method === "tools/list") {          if (remaining > 0) {            remaining -= 1;            return respond(new Response("", { status: 401 }));          }          return respond(            jsonRpc(rpc, {              tools: [                {                  name: "echo_back",                  description: "Echoes",                  inputSchema: { type: "object", properties: {} },                },              ],            }),          );        }        return respond(new Response("Unexpected JSON-RPC method", { status: 400 }));      }      const response =        url.pathname === "/.well-known/oauth-protected-resource/mcp"          ? Response.json({ resource: endpoint, authorization_servers: [issuer] })          : url.pathname === "/.well-known/oauth-authorization-server"            ? Response.json({                issuer,                authorization_endpoint: `${issuer}/authorize`,                token_endpoint: `${issuer}/token`,                registration_endpoint: `${issuer}/register`,                response_types_supported: ["code"],                code_challenge_methods_supported: ["S256"],              })            : url.pathname === "/register"              ? Response.json(                  {                    client_id: "replacement-client",                    redirect_uris: ["http://localhost/oauth/callback"],                    grant_types: ["authorization_code", "refresh_token"],                    response_types: ["code"],                    token_endpoint_auth_method: "none",                  },                  { status: 201 },                )              : new Response("unexpected request", { status: 500 });      return respond(response);    }),  );  return { layer, requests };}; // `tools/call` responders. Both embed a "do-not-leak" sentinel the assertions// confirm never reaches the caller-facing failure.const httpStatusCallTool =  (status: number): CallToolResponder =>  () =>    HttpServerResponse.text("do-not-leak: upstream auth challenge", { status }); const jsonRpcErrorCallTool =  (code: number): CallToolResponder =>  (rpc) =>    HttpServerResponse.jsonUnsafe({      jsonrpc: "2.0",      id: rpc.id ?? null,      error: { code, message: "application-level do-not-leak" },    }); const seedCallToolExecutor = (input: {  slug: string;  callTool: CallToolResponder;  /** Seed an oauth2-templated connection with a static access token, so the   *  transport gets a real authProvider — the production OAuth path where the   *  SDK's own challenge handling would otherwise swallow scope signals. */  oauth?: boolean;}) =>  Effect.acquireRelease(    Effect.gen(function* () {      const server = yield* serveCallToolServer(input.callTool);      const config = makeTestConfig({        plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const,      });      const executor = yield* createExecutor(config);       yield* executor.mcp.addServer({        name: "Call tool fixture",        endpoint: server.url("/mcp"),        slug: input.slug,        remoteTransport: "streamable-http",        ...(input.oauth ? { auth: { kind: "oauth2" as const } } : {}),      });      yield* executor.connections.create({        owner: "org",        name: ConnectionName.make("main"),        integration: IntegrationSlug.make(input.slug),        template: input.oauth ? AuthTemplateSlug.make("oauth2") : TEMPLATE,        value: input.oauth ? "static-access-token" : "",      });       return {        config,        executor,        toolAddress: ToolAddress.make(`tools.${input.slug}.org.main.explode`),      } as const;    }),    ({ config, executor }) =>      Effect.gen(function* () {        yield* executor.close().pipe(Effect.ignore);        yield* Effect.promise(() => config.testDb.close()).pipe(Effect.ignore);      }),  ); // ---------------------------------------------------------------------------// Manifest extraction// --------------------------------------------------------------------------- describe("extractManifestFromListToolsResult", () => {  it.effect("extracts tools from a valid listTools response", () =>    Effect.sync(() => {      const result = extractManifestFromListToolsResult({        tools: [          {            name: "get_weather",            description: "Get weather for a location",            inputSchema: {              type: "object",              properties: { location: { type: "string" } },            },          },          { name: "search", description: "Search the web" },        ],      });       expect(result.tools).toHaveLength(2);      expect(result.tools[0]!.toolName).toBe("get_weather");      expect(result.tools[0]!.toolId).toBe("get_weather");      expect(result.tools[0]!.description).toBe("Get weather for a location");      expect(result.tools[1]!.toolName).toBe("search");    }),  );   it.effect("sanitizes tool IDs", () =>    Effect.sync(() => {      const result = extractManifestFromListToolsResult({        tools: [          { name: "My Tool!!", description: null },          { name: "My Tool!!", description: null },        ],      });       expect(result.tools[0]!.toolId).toBe("my_tool");      expect(result.tools[1]!.toolId).toBe("my_tool_2");    }),  );   it.effect("handles empty tools list", () =>    Effect.sync(() => {      const result = extractManifestFromListToolsResult({ tools: [] });      expect(result.tools).toHaveLength(0);    }),  );   it.effect("extracts server metadata", () =>    Effect.sync(() => {      const result = extractManifestFromListToolsResult(        { tools: [] },        { serverInfo: { name: "test-server", version: "1.0.0" } },      );      expect(result.server?.name).toBe("test-server");      expect(result.server?.version).toBe("1.0.0");    }),  );   it.effect("decodes upstream tool annotations", () =>    Effect.sync(() => {      const result = extractManifestFromListToolsResult({        tools: [          { name: "delete", annotations: { destructiveHint: true } },          { name: "list", annotations: { readOnlyHint: true } },          { name: "ping" },        ],      });       expect(result.tools[0]!.annotations?.destructiveHint).toBe(true);      expect(result.tools[1]!.annotations?.readOnlyHint).toBe(true);      expect(result.tools[2]!.annotations).toBeUndefined();    }),  );   it.effect("carries the reserved `_meta` map through verbatim", () =>    Effect.sync(() => {      const meta = {        serverName: "time",        shortDescription: "Current time",        defer_loading: false,        nested: { any: ["shape"] },      };       const result = extractManifestFromListToolsResult({        tools: [          {            name: "time_get_current_time",            description: "Get the current time",            inputSchema: { type: "object" },            _meta: meta,          },          { name: "no_meta", description: "Has no _meta" },        ],      });       expect(result.tools[0]!._meta).toEqual(meta);      expect(result.tools[1]!._meta).toBeUndefined();    }),  );   // `_meta` is opaque and server-controlled, so a value that does not match the  // spec's map shape must not take the rest of the list down with it.  it.effect("ignores a malformed `_meta` without dropping the tool", () =>    Effect.sync(() => {      const result = extractManifestFromListToolsResult({        tools: [{ name: "odd_meta", _meta: "not-a-map" }, { name: "plain" }],      });       expect(result.tools.map((tool) => tool.toolName)).toEqual(["odd_meta", "plain"]);      expect(result.tools[0]!._meta).toBeUndefined();    }),  );}); // ---------------------------------------------------------------------------// Namespace derivation// --------------------------------------------------------------------------- describe("deriveMcpNamespace", () => {  it.effect("derives from name", () =>    Effect.sync(() => {      expect(deriveMcpNamespace({ name: "GitHub MCP" })).toBe("github_mcp");    }),  );   it.effect("derives from endpoint", () =>    Effect.sync(() => {      expect(deriveMcpNamespace({ endpoint: "https://api.example.com/mcp" })).toBe(        "api_example_com",      );    }),  );   it.effect("derives from command", () =>    Effect.sync(() => {      expect(deriveMcpNamespace({ command: "/usr/local/bin/my-mcp-server" })).toBe("my_mcp_server");    }),  );   it.effect("falls back to 'mcp'", () =>    Effect.sync(() => {      expect(deriveMcpNamespace({})).toBe("mcp");    }),  );}); // ---------------------------------------------------------------------------// joinToolPath// --------------------------------------------------------------------------- describe("joinToolPath", () => {  it.effect("joins namespace and toolId", () =>    Effect.sync(() => {      expect(joinToolPath("github", "search")).toBe("github.search");    }),  );   it.effect("returns toolId when namespace is undefined", () =>    Effect.sync(() => {      expect(joinToolPath(undefined, "search")).toBe("search");    }),  );}); // ---------------------------------------------------------------------------// Plugin lifecycle// --------------------------------------------------------------------------- describe("mcpPlugin", () => {  it.effect("surfaces OAuth reauthorization from resolveTools as expired health", () =>    Effect.gen(function* () {      const endpoint = "https://mcp.example.test/mcp";      const plugin = mcpPlugin();      const ledger = rejectedOAuthDiscoveryLayer(endpoint);      const result = yield* plugin.resolveTools!({        config: {          transport: "remote",          endpoint,          remoteTransport: "streamable-http",          authenticationTemplate: [{ slug: "oauth2", kind: "oauth2" }],        },        connection: {          owner: "org",          integration: IntegrationSlug.make("oauth_mcp"),          name: ConnectionName.make("main"),        },        template: AuthTemplateSlug.make("oauth2"),        getValues: () => Effect.succeed({ token: "rejected-token" }),        getValue: () => Effect.succeed("rejected-token"),        httpClientLayer: ledger.layer,        ctx: null as never,        integration: null as never,        storage: {},      });       expect(result).toMatchObject({        tools: [],        incomplete: true,        health: {          status: "expired",          detail: expect.stringContaining("reauthorization"),        },      });      expect(ledger.requests.filter((url) => new URL(url).pathname === "/register")).toEqual([]);    }),  );

Lines 1–500 of 1575.