import { describe, expect, it } from "@effect/vitest";import { Effect, Option, Predicate, Schema } from "effect";import { HttpServerResponse } from "effect/unstable/http"; import { AuthTemplateSlug, ConnectionName, IntegrationSlug, OAuthClientSlug, ToolAddress, createExecutor,} from "@executor-js/sdk";import { makeTestConfig, memoryCredentialsPlugin, serveOAuthTestServer, serveTestHttpApp,} from "@executor-js/sdk/testing"; 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 }) => 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", }); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: IntegrationSlug.make(input.slug), template: TEMPLATE, value: "", }); 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("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: [ { kind: "oauth2" }, { type: "apiKey", headers: { Authorization: ["Bearer ", { type: "variable", name: "token" }] }, }, ], mode: "replace", }); expect(merged.map((method) => method.slug)).toEqual(["oauth2", "header"]); }), ); // When discovery fails (auth, network, etc.) the connection still lands with // an empty tool set so the user can retry via `connections.refresh` once they // fix the underlying problem. it.effect("registers integration + connection with 0 tools when discovery fails", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const }), ); const slugStr = "broken_source"; yield* executor.mcp.addServer({ name: "broken", // Port 1 is reserved — connection-refused immediately, giving a // deterministic discovery failure without any server mocks. endpoint: "http://127.0.0.1:1/mcp", slug: slugStr, }); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: IntegrationSlug.make(slugStr), template: TEMPLATE, value: "", }); expect(String(connection.address)).toBe("tools.broken_source.org.main"); Lines 1–500 of 857. Load more
import { describe, expect, it } from "@effect/vitest"; import { Effect, Option, Predicate, Schema } from "effect"; import { HttpServerResponse } from "effect/unstable/http"; import { AuthTemplateSlug, ConnectionName, IntegrationSlug, OAuthClientSlug, ToolAddress, createExecutor, } from "@executor-js/sdk"; import { makeTestConfig, memoryCredentialsPlugin, serveOAuthTestServer, serveTestHttpApp, } from "@executor-js/sdk/testing"; 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 }) => 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", }); yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: IntegrationSlug.make(input.slug), template: TEMPLATE, value: "", }); 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("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: [ { kind: "oauth2" }, { type: "apiKey", headers: { Authorization: ["Bearer ", { type: "variable", name: "token" }] }, }, ], mode: "replace", }); expect(merged.map((method) => method.slug)).toEqual(["oauth2", "header"]); }), ); // When discovery fails (auth, network, etc.) the connection still lands with // an empty tool set so the user can retry via `connections.refresh` once they // fix the underlying problem. it.effect("registers integration + connection with 0 tools when discovery fails", () => Effect.gen(function* () { const executor = yield* createExecutor( makeTestConfig({ plugins: [memoryCredentialsPlugin(), mcpPlugin()] as const }), ); const slugStr = "broken_source"; yield* executor.mcp.addServer({ name: "broken", // Port 1 is reserved — connection-refused immediately, giving a // deterministic discovery failure without any server mocks. endpoint: "http://127.0.0.1:1/mcp", slug: slugStr, }); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: IntegrationSlug.make(slugStr), template: TEMPLATE, value: "", }); expect(String(connection.address)).toBe("tools.broken_source.org.main");