1import { assert } from "@effect/vitest"
2import type * as McpProtocol from "effect/ai/McpProtocol"
3import * as Effect from "effect/Effect"
4import * as HttpRouter from "effect/http/HttpRouter"
5import * as Layer from "effect/Layer"
6import { readMcpHttpResponse } from "./McpHttpResponse.ts"
7
8export const MCP_ENDPOINT = "http://localhost/mcp"
9
10export 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
65export 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})