1import { assert, describe, it } from "@effect/vitest"
2import type * as McpProtocol from "effect/ai/McpProtocol"
3import * as McpSchema from "effect/ai/McpSchema"
4import * as Effect from "effect/Effect"
5import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts"
6
7export const suite = (
8 protocol: McpProtocol.ProtocolAdapter,
9 layer: McpConformanceLayer
10) =>
11 it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => {
12 describe("Lifecycle", () => {
13 // Shared lifecycle behavior. Dated transport requirements stay in the
14 // version entrypoints that compose this suite.
15 describe("Lifecycle Phases", () => {
16 describe("Initialization", () => {
17 it.effect("returns configured server instructions", () =>
18 Effect.gen(function*() {
19 const test = yield* McpConformance
20 const { message } = yield* test.initialize({ server: "features" })
21
22 assert.strictEqual(message.result.instructions, "Follow the test server instructions.")
23 }))
24
25 it.effect("MUST reject non-ping requests before initialize", () =>
26 Effect.gen(function*() {
27 const test = yield* McpConformance
28 const response = yield* test.post({
29 jsonrpc: "2.0",
30 id: 1,
31 method: "tools/list",
32 params: {}
33 })
34
35 assert.strictEqual(response.status, 400)
36 }))
37
38 it.effect("MUST reject initialized notifications before initialize", () =>
39 Effect.gen(function*() {
40 const test = yield* McpConformance
41 const response = yield* test.post(test.initializedNotification)
42
43 assert.strictEqual(response.status, 400)
44 }))
45
46 it.effect("SCHEMA requires protocolVersion, capabilities, and clientInfo", () =>
47 Effect.gen(function*() {
48 const test = yield* McpConformance
49 const invalidParams = [
50 {
51 capabilities: {},
52 clientInfo: { name: "McpConformanceClient", version: "1.0.0" }
53 },
54 {
55 protocolVersion: protocol.protocolVersion,
56 clientInfo: { name: "McpConformanceClient", version: "1.0.0" }
57 },
58 {
59 protocolVersion: protocol.protocolVersion,
60 capabilities: {}
61 }
62 ]
63
64 for (const [index, params] of invalidParams.entries()) {
65 const response = yield* test.post({
66 jsonrpc: "2.0",
67 id: index + 1,
68 method: "initialize",
69 params
70 })
71 const error = yield* test.decodeError(response)
72
73 assert.strictEqual(error.id, index + 1)
74 assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE)
75 assert.isNull(response.headers.get("Mcp-Session-Id"))
76 }
77 }))
78
79 it.effect("SCHEMA returns server capabilities and implementation information", () =>
80 Effect.gen(function*() {
81 const test = yield* McpConformance
82 const { message, response, sessionId } = yield* test.initialize()
83
84 assert.strictEqual(response.status, 200)
85 assert.strictEqual(message.id, 1)
86 assert.isObject(message.result.capabilities)
87 assert.deepStrictEqual(message.result.serverInfo, test.serverInfo)
88 assert.isNotNull(sessionId)
89 assert.match(sessionId, /^[\x21-\x7e]+$/)
90 }))
91
92 it.effect("MUST accept initialized after a successful initialize response", () =>
93 Effect.gen(function*() {
94 const test = yield* McpConformance
95 const initialized = yield* test.initialize()
96 assert.isNotNull(initialized.sessionId)
97
98 const response = yield* test.notifyInitialized(initialized)
99
100 assert.strictEqual(response.status, 202)
101 assert.strictEqual(yield* Effect.promise(() => response.text()), "")
102 }))
103 })
104
105 describe("Version Negotiation", () => {
106 it.effect("MUST echo a requested version supported by the server", () =>
107 Effect.gen(function*() {
108 const test = yield* McpConformance
109 const { message } = yield* test.initialize()
110
111 assert.strictEqual(message.result.protocolVersion, protocol.protocolVersion)
112 }))
113
114 it.effect("SHOULD negotiate an unsupported requested version to a supported version", () =>
115 Effect.gen(function*() {
116 const test = yield* McpConformance
117 const { message } = yield* test.initialize({
118 protocolVersion: "unsupported-version"
119 })
120
121 assert.strictEqual(message.result.protocolVersion, protocol.protocolVersion)
122 }))
123 })
124
125 describe("Capability Negotiation", () => {
126 it.effect("SCHEMA advertises the registered prompt, resource, and tool capabilities", () =>
127 Effect.gen(function*() {
128 const test = yield* McpConformance
129 const { message } = yield* test.initialize({ server: "features" })
130
131 assert.deepStrictEqual(message.result.capabilities.prompts, { listChanged: true })
132 assert.deepStrictEqual(message.result.capabilities.resources, {
133 listChanged: true,
134 subscribe: false
135 })
136 assert.deepStrictEqual(message.result.capabilities.tools, { listChanged: true })
137 }))
138 })
139
140 describe("Operation", () => {
141 it.effect("MUST continue to use the version negotiated during initialization", () =>
142 Effect.gen(function*() {
143 const test = yield* McpConformance
144 const initialized = yield* test.initialize()
145 assert.isNotNull(initialized.sessionId)
146
147 const response = yield* test.ping(initialized)
148
149 assert.strictEqual(response.status, 200)
150 assert.strictEqual(response.headers.get("Mcp-Protocol-Version"), protocol.protocolVersion)
151 }))
152 })
153 })
154 })
155 })