// --------------------------------------------------------------------------- // Handler-level integration test for the MCP group. // // Verifies the layer wiring stays coherent end-to-end: the handlers // pull the wrapped extension from the service, and any un-caught cause // lands in the observability middleware — producing a 500 whose body is // the opaque `InternalError` schema (no internal leakage). // --------------------------------------------------------------------------- import { HttpApiBuilder } from "effect/unstable/httpapi"; import { HttpRouter, HttpServer } from "effect/unstable/http"; import { describe, expect, it } from "@effect/vitest"; import { Effect, Layer, Schema } from "effect"; import { addGroup, observabilityMiddleware } from "@executor-js/api"; import { CoreHandlers, ExecutionEngineService, ExecutorService } from "@executor-js/api/server"; import type { McpPluginExtension } from "../sdk/plugin"; import { McpConnectionError } from "../sdk/errors"; import { McpExtensionService, McpHandlers } from "./handlers"; import { McpGroup } from "./group"; const unused = Effect.die("unused"); const failingExtension: McpPluginExtension = { // oxlint-disable-next-line executor/no-error-constructor -- boundary: test injects a defect to verify opaque handler error responses probeEndpoint: () => Effect.die(new Error("Not implemented")), addServer: () => unused, removeServer: () => unused, reconcileStdioConnections: () => unused, getServer: () => Effect.succeed(null), configureServer: () => unused, configureAuth: () => unused, }; const Api = addGroup(McpGroup); const UnusedExecutor = Layer.succeed(ExecutorService)({} as ExecutorService["Service"]); const UnusedExecutionEngine = Layer.succeed(ExecutionEngineService)( {} as ExecutionEngineService["Service"], ); const webHandlerFor = (extension: McpPluginExtension) => Effect.acquireRelease( Effect.sync(() => HttpRouter.toWebHandler( HttpApiBuilder.layer(Api).pipe( Layer.provide(CoreHandlers), Layer.provide(McpHandlers), Layer.provide(observabilityMiddleware(Api)), Layer.provide(UnusedExecutor), Layer.provide(UnusedExecutionEngine), Layer.provide(Layer.succeed(McpExtensionService, extension)), Layer.provideMerge(HttpServer.layerServices), Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })), ), ), ), (web) => Effect.promise(() => web.dispose()), ); // `acquireRelease` keeps disposal inside the Effect scope — no // try/finally, no per-test cleanup plumbing. `it.scoped` closes the // scope for us. Each `Layer.provide` satisfies a piece of the api // builder's dependency graph; `provideMerge` at the bottom keeps // framework services available to the router itself. const WebHandler = webHandlerFor(failingExtension); const McpConnectionErrorResponse = Schema.Struct({ _tag: Schema.Literal("McpConnectionError"), message: Schema.String, }); describe("McpHandlers", () => { it.effect("defect-returning methods produce an opaque InternalError, no leakage", () => Effect.gen(function* () { const web = yield* WebHandler; const response = yield* Effect.promise(() => (web.handler as (request: Request) => Promise)( new Request("http://localhost/mcp/probe", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ endpoint: "https://example.com/mcp" }), }), ), ); expect(response.status).toBe(500); const body = yield* Effect.promise(() => response.text()); expect(body).not.toContain("Not implemented"); }), ); it.effect("domain MCP connection errors are encoded as 400 responses", () => Effect.gen(function* () { const web = yield* webHandlerFor({ ...failingExtension, probeEndpoint: () => Effect.fail( new McpConnectionError({ transport: "remote", message: "Failed to connect to MCP endpoint and no OAuth was detected. Do you need to provide an API key, header, or query parameter?", }), ), }); const response = yield* Effect.promise(() => (web.handler as (request: Request) => Promise)( new Request("http://localhost/mcp/probe", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ endpoint: "https://ui.sh/mcp" }), }), ), ); expect(response.status).toBe(400); const body = yield* Schema.decodeUnknownEffect(McpConnectionErrorResponse)( yield* Effect.promise(() => response.json()), ); expect(body.message).toContain("Do you need to provide an API key"); }), ); });