1036 lines · 37.1 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Layer, Option, Predicate, Schema } 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 } 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),      });    }),  ); // `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();    }),  );}); // ---------------------------------------------------------------------------// 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("creates executor with mcp plugin", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({          plugins: [mcpPlugin()] as const,        }),      );       expect(executor.mcp).toBeDefined();      expect(executor.mcp.addServer).toBeTypeOf("function");      expect(executor.mcp.removeServer).toBeTypeOf("function");      expect(executor.mcp.getServer).toBeTypeOf("function");      expect(executor.mcp.probeEndpoint).toBeTypeOf("function");      expect(executor.oauth.start).toBeTypeOf("function");      expect(executor.oauth.complete).toBeTypeOf("function");    }),  );   it.effect("routes remote connector traffic through the provided HttpClient layer", () =>    Effect.gen(function* () {      const seen: string[] = [];      const httpClientLayer = Layer.succeed(HttpClient.HttpClient)(        HttpClient.make((request: HttpClientRequest.HttpClientRequest) => {          seen.push(request.url);          return Effect.succeed(            HttpClientResponse.fromWeb(request, new Response("blocked", { status: 403 })),          );        }),      );       const error = yield* createMcpConnector({        transport: "remote",        endpoint: "https://internal.example/mcp",        remoteTransport: "streamable-http",        httpClientLayer,      }).pipe(Effect.flip);       expect(Predicate.isTagged(error, "McpConnectionError")).toBe(true);      expect(seen).toEqual(["https://internal.example/mcp"]);    }),  );   it.effect("integration catalog has no configured MCP integrations initially", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [mcpPlugin()] as const }));      const integrations = yield* executor.integrations.list();      expect(integrations.filter((i) => i.kind === "mcp")).toHaveLength(0);    }),  );   it.effect("connection tools list is empty until a connection is created", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [mcpPlugin()] as const }));      const tools = yield* executor.tools.list();      expect(tools.filter((tool) => String(tool.address).startsWith("tools."))).toHaveLength(0);    }),  );   it.effect("removing an MCP server removes the OAuth client used by its connection", () =>    Effect.scoped(      Effect.gen(function* () {        const server = yield* serveOAuthTestServer({ scopes: [] });        const executor = yield* createExecutor(          makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const }),        );        const attachedClient = OAuthClientSlug.make("axiom-mcp");        const unrelatedClient = OAuthClientSlug.make("manual-app");         yield* executor.mcp.addServer({          name: "Axiom MCP",          endpoint: "http://127.0.0.1:1/mcp",          slug: "axiom_mcp",          auth: { kind: "oauth2" },        });        yield* executor.oauth.createClient({          owner: "org",          slug: attachedClient,          authorizationUrl: server.authorizationEndpoint,          tokenUrl: server.tokenEndpoint,          grant: "client_credentials",          clientId: "test-client",          clientSecret: "test-secret",          resource: server.mcpResourceUrl,          origin: {            kind: "dynamic_client_registration",            integration: IntegrationSlug.make("axiom_mcp"),          },        });        yield* executor.oauth.createClient({          owner: "org",          slug: unrelatedClient,          authorizationUrl: server.authorizationEndpoint,          tokenUrl: server.tokenEndpoint,          grant: "client_credentials",          clientId: "test-client",          clientSecret: "test-secret",          resource: server.mcpResourceUrl,        });         const connected = yield* executor.oauth.start({          owner: "org",          client: attachedClient,          clientOwner: "org",          name: ConnectionName.make("main"),          integration: IntegrationSlug.make("axiom_mcp"),          template: AuthTemplateSlug.make("oauth2"),        });        expect(connected.status).toBe("connected");         yield* executor.mcp.removeServer("axiom_mcp");         const clients = yield* executor.oauth.listClients();        expect(clients.map((client) => String(client.slug))).not.toContain("axiom-mcp");        expect(clients.map((client) => String(client.slug))).toContain("manual-app");      }),    ),  );   it.effect("removing an MCP server removes a legacy orphaned DCR-looking OAuth client", () =>    Effect.scoped(      Effect.gen(function* () {        const executor = yield* createExecutor(          makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const }),        );         yield* executor.mcp.addServer({          name: "Axiom MCP",          endpoint: "https://mcp.axiom.co/mcp",          slug: "axiom_mcp",          auth: { kind: "oauth2" },        });        yield* executor.oauth.createClient({          owner: "org",          slug: OAuthClientSlug.make("axiom-mcp"),          authorizationUrl: "https://mcp.axiom.co/authorize",          tokenUrl: "https://mcp.axiom.co/token",          grant: "authorization_code",          clientId: "stale-dcr-client",          clientSecret: "",          resource: "https://mcp.axiom.co/mcp",        });        yield* executor.oauth.createClient({          owner: "org",          slug: OAuthClientSlug.make("manual-app"),          authorizationUrl: "https://mcp.axiom.co/authorize",          tokenUrl: "https://mcp.axiom.co/token",          grant: "authorization_code",          clientId: "manual-client",          clientSecret: "",          resource: "https://mcp.axiom.co/mcp",        });         yield* executor.mcp.removeServer("axiom_mcp");         const clients = yield* executor.oauth.listClients();        expect(clients.map((client) => String(client.slug))).not.toContain("axiom-mcp");        expect(clients.map((client) => String(client.slug))).toContain("manual-app");      }),    ),  );   // Custom-method create (configureAuth) merge-appends onto the declared set —  // adding an API key to an OAuth server must NOT displace the OAuth method.  it.effect("configureAuth merge-appends a custom method without clobbering oauth", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [mcpPlugin()] as const }));       yield* executor.mcp.addServer({        name: "OAuth MCP",        endpoint: "https://mcp.example.com/mcp",        slug: "oauth_mcp",        auth: { kind: "oauth2" },      });       const merged = yield* executor.mcp.configureAuth("oauth_mcp", {        authenticationTemplate: [          { type: "apiKey", headers: { "X-Api-Key": [{ type: "variable", name: "token" }] } },        ],      });       expect(merged.map((method) => method.kind)).toEqual(["oauth2", "apikey"]);      expect(merged[0]?.slug).toBe("oauth2");      expect(merged[1]?.slug).toMatch(/^custom_/);       // The catalog projects both methods.      const integration = yield* executor.integrations.get(IntegrationSlug.make("oauth_mcp"));      expect(integration?.authMethods.map((method) => method.kind)).toEqual(["oauth", "apikey"]);    }),  );   it.effect("configureAuth replace mode swaps the declared set with kind-based slugs", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(makeTestConfig({ plugins: [mcpPlugin()] as const }));       yield* executor.mcp.addServer({        name: "Open MCP",        endpoint: "https://mcp.example.com/mcp",        slug: "open_mcp",      });       const merged = yield* executor.mcp.configureAuth("open_mcp", {        authenticationTemplate: [

Lines 1–500 of 1036.