| 1 | import { assert } from "@effect/vitest" |
| 2 | import type * as McpProtocol from "effect/ai/McpProtocol" |
| 3 | import * as Effect from "effect/Effect" |
| 4 | import * as HttpRouter from "effect/http/HttpRouter" |
| 5 | import * as Layer from "effect/Layer" |
| 6 | import { readMcpHttpResponse } from "./McpHttpResponse.ts" |
| 7 | |
| 8 | export const MCP_ENDPOINT = "http://localhost/mcp" |
| 9 | |
| 10 | export const makeHttpHarness = Effect.fnUntraced(function*<A, E>( |
| 11 | serverLayer: Layer.Layer<A, E, HttpRouter.HttpRouter>, |
| 12 | options?: { |
| 13 | readonly routerLayer?: Layer.Layer<never, never, HttpRouter.HttpRouter> | undefined |
| 14 | } |
| 15 | ) { |
| 16 | const appLayer = options?.routerLayer ? Layer.merge(serverLayer, options.routerLayer) : serverLayer |
| 17 | const { dispose, handler } = HttpRouter.toWebHandler(appLayer, { disableLogger: true }) |
| 18 | yield* Effect.addFinalizer(() => Effect.promise(() => dispose())) |
| 19 | const responses: Array<Response> = [] |
| 20 | let sessionId: string | null = null |
| 21 | let protocolVersion: string | null = null |
| 22 | |
| 23 | const fetchImpl = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => { |
| 24 | const request = input instanceof Request ? input : new Request(input, init) |
| 25 | if (sessionId !== null) { |
| 26 | request.headers.set("Mcp-Session-Id", sessionId) |
| 27 | } |
| 28 | if (protocolVersion !== null && !request.headers.has("Mcp-Protocol-Version")) { |
| 29 | request.headers.set("Mcp-Protocol-Version", protocolVersion) |
| 30 | } |
| 31 | const response = await handler(request) |
| 32 | sessionId = response.headers.get("Mcp-Session-Id") ?? sessionId |
| 33 | protocolVersion = response.headers.get("Mcp-Protocol-Version") ?? protocolVersion |
| 34 | responses.push(response.clone()) |
| 35 | return response |
| 36 | } |
| 37 | const fetch: typeof globalThis.fetch = Object.assign(fetchImpl, { preconnect() {} }) |
| 38 | |
| 39 | const postText = (body: string, headers?: HeadersInit) => |
| 40 | Effect.promise(() => |
| 41 | handler( |
| 42 | new Request(MCP_ENDPOINT, { |
| 43 | method: "POST", |
| 44 | headers: { |
| 45 | accept: "application/json, text/event-stream", |
| 46 | "content-type": "application/json", |
| 47 | ...headers |
| 48 | }, |
| 49 | body |
| 50 | }) |
| 51 | ) |
| 52 | ) |
| 53 | |
| 54 | const post = (body: unknown, headers?: HeadersInit) => postText(JSON.stringify(body), headers) |
| 55 | |
| 56 | return { |
| 57 | handler, |
| 58 | fetch, |
| 59 | post, |
| 60 | postText, |
| 61 | responses |
| 62 | } as const |
| 63 | }) |
| 64 | |
| 65 | export const initializeHttpSession = Effect.fnUntraced(function*( |
| 66 | harness: Effect.Success<ReturnType<typeof makeHttpHarness>>, |
| 67 | selectedProtocol: McpProtocol.ProtocolAdapter |
| 68 | ) { |
| 69 | const response = yield* harness.post({ |
| 70 | jsonrpc: "2.0", |
| 71 | id: "initialize", |
| 72 | method: "initialize", |
| 73 | params: { |
| 74 | protocolVersion: selectedProtocol.protocolVersion, |
| 75 | capabilities: {}, |
| 76 | clientInfo: { name: "test", version: "1.0.0" } |
| 77 | } |
| 78 | }) |
| 79 | yield* readMcpHttpResponse(response) |
| 80 | const sessionId = response.headers.get("Mcp-Session-Id") |
| 81 | assert.isNotNull(sessionId) |
| 82 | const headers = { "Mcp-Session-Id": sessionId, "Mcp-Protocol-Version": selectedProtocol.protocolVersion } |
| 83 | yield* harness.post({ jsonrpc: "2.0", method: "notifications/initialized" }, headers) |
| 84 | return headers |
| 85 | }) |