1import { assert, describe, it } from "@effect/vitest"
2import * as McpProtocol from "effect/ai/McpProtocol"
3import * as McpSchema from "effect/ai/McpSchema"
4import * as Effect from "effect/Effect"
5import * as BaseProtocolTest from "./McpConformance/BaseProtocolTest.ts"
6import * as CompletionTest from "./McpConformance/CompletionTest.ts"
7import * as LifecycleTest from "./McpConformance/LifecycleTest.ts"
8import * as LoggingTest from "./McpConformance/LoggingTest.ts"
9import { layer as makeMcpConformanceLayer, McpConformance } from "./McpConformance/McpConformance.ts"
10import * as PromptsTest from "./McpConformance/PromptsTest.ts"
11import * as ResourcesTest from "./McpConformance/ResourcesTest.ts"
12import * as RootsTest from "./McpConformance/RootsTest.ts"
13import * as SamplingTest from "./McpConformance/SamplingTest.ts"
14import * as ToolsTest from "./McpConformance/ToolsTest.ts"
15import * as TransportsTest from "./McpConformance/TransportsTest.ts"
16import * as UtilitiesTest from "./McpConformance/UtilitiesTest.ts"
17import { makeMcpStdioHarness } from "./TestUtils/McpStdioHarness.ts"
18
19const protocol = McpProtocol.v2025_03_26
20const testLayer = makeMcpConformanceLayer(protocol)
21
22LifecycleTest.suite(protocol, testLayer)
23BaseProtocolTest.suite(protocol, testLayer)
24BaseProtocolTest.statefulLegacySuite(protocol, testLayer)
25TransportsTest.suite(protocol, testLayer)
26UtilitiesTest.suite(protocol, testLayer)
27UtilitiesTest.statefulLegacySuite(protocol, testLayer)
28LoggingTest.suite(protocol, testLayer)
29CompletionTest.suite(protocol, testLayer)
30ToolsTest.suite(protocol, testLayer)
31ToolsTest.statefulLegacySuite(protocol, testLayer)
32ResourcesTest.suite(protocol, testLayer)
33ResourcesTest.statefulLegacySuite(protocol, testLayer)
34PromptsTest.suite(protocol, testLayer)
35PromptsTest.statefulLegacySuite(protocol, testLayer)
36RootsTest.suite(protocol, testLayer)
37SamplingTest.suite(protocol, testLayer)
38
39it.layer(testLayer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => {
40 describe("Transport-specific behavior", () => {
41 it.effect("MUST accept operational JSON-RPC batches over stdio", () =>
42 Effect.gen(function*() {
43 const fixture = yield* makeMcpStdioHarness(protocol)
44 yield* fixture.sendRaw({
45 jsonrpc: "2.0",
46 id: 1,
47 method: "initialize",
48 params: {
49 protocolVersion: protocol.protocolVersion,
50 capabilities: {},
51 clientInfo: { name: "stdio-client", version: "1.0.0" }
52 }
53 })
54 yield* fixture.takeFrame
55 yield* fixture.sendRaw([
56 { jsonrpc: "2.0", id: 2, method: "ping", params: {} },
57 { jsonrpc: "2.0", id: 3, method: "ping", params: {} }
58 ])
59
60 assert.deepStrictEqual(yield* fixture.takeFrame, [
61 { jsonrpc: "2.0", id: 2, result: {} },
62 { jsonrpc: "2.0", id: 3, result: {} }
63 ])
64 }))
65
66 it.effect("SERVER rejects initialize as part of a JSON-RPC batch", () =>
67 Effect.gen(function*() {
68 const test = yield* McpConformance
69 const response = yield* test.post([test.initializeRequest()])
70
71 assert.isAtLeast(response.status, 400)
72 assert.isNull(response.headers.get("Mcp-Session-Id"))
73 }))
74
75 it.effect("MUST reject an empty JSON-RPC batch", () =>
76 Effect.gen(function*() {
77 const test = yield* McpConformance
78 const response = yield* test.post([])
79 const error = yield* test.decodeError(response)
80
81 assert.strictEqual(error.id, null)
82 assert.strictEqual(error.error.code, -32600)
83 }))
84
85 it.effect("MUST accept an operational mixed batch and correlate responses by id", () =>
86 Effect.gen(function*() {
87 const test = yield* McpConformance
88 const initialized = yield* test.initialize()
89 assert.isNotNull(initialized.sessionId)
90
91 const response = yield* test.send(initialized, [
92 test.pingRequest(41),
93 test.initializedNotification,
94 { jsonrpc: "2.0", id: 42, method: "unknown/method" }
95 ], { includeProtocolVersion: false })
96 const messages = yield* Effect.promise<
97 ReadonlyArray<{
98 readonly jsonrpc?: unknown
99 readonly id?: unknown
100 readonly result?: unknown
101 readonly error?: { readonly code?: unknown }
102 }>
103 >(() => response.json())
104 const success = messages.find((message) => message.id === 41)
105 const failure = messages.find((message) => message.id === 42)
106
107 assert.deepStrictEqual(success, { jsonrpc: "2.0", id: 41, result: {} })
108 assert.strictEqual(failure?.error?.code, McpSchema.METHOD_NOT_FOUND_ERROR_CODE)
109 }))
110
111 it.effect("MUST return 202 with no body for an accepted notification-only batch", () =>
112 Effect.gen(function*() {
113 const test = yield* McpConformance
114 const initialized = yield* test.initialize()
115 assert.isNotNull(initialized.sessionId)
116
117 const response = yield* test.send(initialized, [
118 test.initializedNotification,
119 { jsonrpc: "2.0", method: "notifications/roots/list_changed" }
120 ], { includeProtocolVersion: false })
121
122 assert.strictEqual(response.status, 202)
123 assert.strictEqual(yield* Effect.promise(() => response.text()), "")
124 }))
125
126 it.effect("MUST operate without the later protocol-version header", () =>
127 Effect.gen(function*() {
128 const test = yield* McpConformance
129 const initialized = yield* test.initialize()
130 assert.isNotNull(initialized.sessionId)
131
132 const response = yield* test.ping(initialized, {
133 includeProtocolVersion: false
134 })
135
136 assert.strictEqual(response.status, 200)
137 }))
138 })
139})