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 Base64 from "effect/encoding/Base64"
6import * as Schema from "effect/Schema"
7import * as BaseProtocolTest from "./McpConformance/BaseProtocolTest.ts"
8import * as CompletionTest from "./McpConformance/CompletionTest.ts"
9import * as LoggingTest from "./McpConformance/LoggingTest.ts"
10import { layer as makeMcpConformanceLayer, McpConformance } from "./McpConformance/McpConformance.ts"
11import {
12 MrtrInvalidStateToolName,
13 MrtrSamplingToolChoiceToolName,
14 MrtrSamplingToolsToolName,
15 MrtrToolName
16} from "./McpConformance/McpConformanceFixtures.ts"
17import * as MultiRoundTripTest from "./McpConformance/MultiRoundTripTest.ts"
18import * as PromptsTest from "./McpConformance/PromptsTest.ts"
19import * as ResourcesTest from "./McpConformance/ResourcesTest.ts"
20import * as SubscriptionsTest from "./McpConformance/SubscriptionsTest.ts"
21import * as ToolsTest from "./McpConformance/ToolsTest.ts"
22import * as TransportsTest from "./McpConformance/TransportsTest.ts"
23import * as UtilitiesTest from "./McpConformance/UtilitiesTest.ts"
24import { makeHttpHarness } from "./TestUtils/McpHttpHarness.ts"
25import { readMcpHttpResponse } from "./TestUtils/McpHttpResponse.ts"
26import { makeServerLayer } from "./TestUtils/McpServerLayer.ts"
27import { makeMcpStdioHarness } from "./TestUtils/McpStdioHarness.ts"
28
29const protocol = McpProtocol.v2026_07_28
30const testLayer = makeMcpConformanceLayer(protocol)
31
32const metadata = {
33 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
34 "io.modelcontextprotocol/clientCapabilities": {},
35 "io.modelcontextprotocol/clientInfo": { name: "McpConformanceClient", version: "1.0.0" }
36} as const
37
38const request = (
39 id: string | number,
40 method: string,
41 params: Record<string, unknown> = {}
42) => ({
43 jsonrpc: "2.0",
44 id,
45 method,
46 params: { ...params, _meta: metadata }
47})
48
49const headers = (method: string, name?: string): HeadersInit => ({
50 "MCP-Protocol-Version": protocol.protocolVersion,
51 "Mcp-Method": method,
52 ...(name === undefined ? {} : { "Mcp-Name": name })
53})
54
55const decodeError = (response: Response) =>
56 Effect.promise<unknown>(() => response.json()).pipe(
57 Effect.flatMap(Schema.decodeUnknownEffect(Schema.Struct({
58 id: Schema.NullOr(Schema.Union([Schema.String, Schema.Number])),
59 error: McpSchema.McpError
60 })))
61 )
62
63BaseProtocolTest.suite(protocol, testLayer)
64BaseProtocolTest.statelessModernSuite(protocol, testLayer)
65TransportsTest.statelessModernSuite(protocol, testLayer)
66UtilitiesTest.statelessModernSuite(protocol, testLayer)
67ToolsTest.suite(protocol, testLayer)
68ToolsTest.statelessModernSuite(protocol, testLayer)
69ResourcesTest.suite(protocol, testLayer)
70PromptsTest.suite(protocol, testLayer, { additionalPromptNames: ["MrtrPrompt"] })
71CompletionTest.suite(protocol, testLayer)
72LoggingTest.statelessModernSuite(protocol, testLayer)
73MultiRoundTripTest.suite(protocol, testLayer)
74SubscriptionsTest.suite(protocol, testLayer)
75
76it.layer(testLayer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => {
77 describe("Lifecycle", () => {
78 // SEP-2575: https://modelcontextprotocol.io/seps/2575-stateless-mcp
79 // https://modelcontextprotocol.io/specification/2026-07-28/server/discovery
80 it.effect("should discover the server when no initialization or session exists", () =>
81 Effect.gen(function*() {
82 const test = yield* McpConformance
83 const discovered = yield* test.initialize({ server: "features" })
84
85 assert.strictEqual(discovered.response.status, 200)
86 assert.isNull(discovered.sessionId)
87 assert.strictEqual(discovered.message.result.protocolVersion, protocol.protocolVersion)
88 assert.strictEqual(discovered.message.result.instructions, "Follow the test server instructions.")
89 }))
90
91 // Modern extension settings are JSON objects; legacy-only scalar settings must not break discovery.
92 // https://modelcontextprotocol.io/specification/2026-07-28/schema#servercapabilities
93 it.effect("should advertise supported extension settings when discovering a server with legacy configuration", () =>
94 Effect.gen(function*() {
95 const extensions = {
96 "example/settings": { enabled: true, formats: ["text/html"], nested: { value: null } },
97 "example/empty": {}
98 }
99 const harness = yield* makeHttpHarness(makeServerLayer({
100 name: "ExtensionServer",
101 protocols: [protocol, McpProtocol.v2025_06_18],
102 extensions: {
103 ...extensions,
104 "example/boolean": true,
105 "example/array": [],
106 "example/null": null
107 }
108 }))
109 const response = yield* harness.post(request("discovery", "server/discover"), headers("server/discover"))
110 assert.strictEqual(response.status, 200)
111 const message = Schema.decodeUnknownSync(Schema.Struct({ result: Schema.JsonObject }))(
112 yield* readMcpHttpResponse(response)
113 )
114 assert.deepStrictEqual(message.result.capabilities, { completions: {}, logging: {}, extensions })
115 }))
116
117 it.effect("should serve independent requests when no discovery or session exists", () =>
118 Effect.gen(function*() {
119 const test = yield* McpConformance
120 const first = yield* test.post(request(1, "tools/list"), headers("tools/list"))
121 const second = yield* test.post(request(2, "tools/list"), {
122 ...headers("tools/list"),
123 "Mcp-Session-Id": "ignored-modern-session"
124 })
125
126 assert.strictEqual(first.status, 200)
127 assert.strictEqual(second.status, 200)
128 assert.isNull(first.headers.get("Mcp-Session-Id"))
129 }))
130
131 it.effect("should return method not found when the removed initialize method is requested", () =>
132 Effect.gen(function*() {
133 const test = yield* McpConformance
134 const response = yield* test.post(request(10, "initialize"), headers("initialize"))
135 const message = yield* decodeError(response)
136
137 assert.strictEqual(response.status, 404)
138 assert.strictEqual(message.error.code, McpSchema.METHOD_NOT_FOUND_ERROR_CODE)
139 }))
140
141 it.effect("should accept cancellation notifications without request metadata", () =>
142 Effect.gen(function*() {
143 const test = yield* McpConformance
144 const response = yield* test.post({
145 jsonrpc: "2.0",
146 method: "notifications/cancelled",
147 params: { requestId: "already-complete" }
148 }, headers("notifications/cancelled"))
149
150 assert.strictEqual(response.status, 202)
151 assert.strictEqual(yield* Effect.promise(() => response.text()), "")
152 }))
153 })
154
155 describe("Transports", () => {
156 // Required headers must produce HeaderMismatch even when body metadata is also absent.
157 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-validation
158 it.effect("should return HeaderMismatch when a July-only request omits headers and metadata", () =>
159 Effect.gen(function*() {
160 const test = yield* McpConformance
161 const cases = [
162 { jsonrpc: "2.0", id: "missing", method: "tools/list", params: {} },
163 {
164 jsonrpc: "2.0",
165 id: "legacy-initialize",
166 method: "initialize",
167 params: {
168 protocolVersion: "2025-11-25",
169 capabilities: {},
170 clientInfo: { name: "legacy-client", version: "1.0.0" }
171 }
172 }
173 ]
174 for (const body of cases) {
175 const response = yield* test.post(body)
176 assert.strictEqual(response.status, 400, body.id)
177 const error = yield* decodeError(response)
178 assert.strictEqual(error.id, body.id)
179 assert.strictEqual(error.error.code, McpSchema.HEADER_MISMATCH_ERROR_CODE)
180 }
181 }))
182
183 // Nested properties may mirror parameters; missing or mismatched headers must prevent execution.
184 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#server-behavior-for-custom-headers
185 it.effect("should validate nested mirrored parameters before executing a tool", () =>
186 Effect.gen(function*() {
187 const test = yield* McpConformance
188 const initialized = yield* test.initialize({ server: "features" })
189 for (const header of [undefined, "us", "eu", "=?base64?ZXU=?="]) {
190 yield* test.resetObservations
191 const response = yield* test.send(initialized, {
192 jsonrpc: "2.0",
193 id: "nested",
194 method: "tools/call",
195 params: { name: "HeaderTool", arguments: { region: "eu", routing: { region: "eu" } } }
196 }, {
197 headers: {
198 "Mcp-Param-Region": "eu",
199 ...(header === undefined ? {} : { "Mcp-Param-Nested-Region": header })
200 }
201 })
202 if (header === undefined || header === "us") {
203 assert.strictEqual((yield* test.observations).toolInvocations, 0)
204 assert.strictEqual(response.status, 400)
205 const error = yield* decodeError(response)
206 assert.strictEqual(error.error.code, McpSchema.HEADER_MISMATCH_ERROR_CODE)
207 } else {
208 assert.strictEqual((yield* test.observations).toolInvocations, 1)
209 assert.strictEqual(response.status, 200)
210 }
211 }
212 }))
213
214 // SEP-2567: https://modelcontextprotocol.io/seps/2567-remove-sessions
215 it.effect("should exchange self-contained newline-delimited requests over stdio", () =>
216 Effect.gen(function*() {
217 const fixture = yield* makeMcpStdioHarness(protocol)
218 const response = yield* fixture.sendRequest("server/discover", {})
219 const result = Schema.decodeUnknownSync(Schema.Struct({
220 supportedVersions: Schema.Array(Schema.String),
221 capabilities: Schema.Record(Schema.String, Schema.Unknown),
222 resultType: Schema.Literal("complete"),
223 ttlMs: Schema.Number,
224 cacheScope: Schema.Literal("private"),
225 _meta: Schema.Struct({
226 "io.modelcontextprotocol/serverInfo": Schema.Struct({
227 name: Schema.String,
228 version: Schema.String
229 })
230 })
231 }))(response.result)
232
233 assert.deepStrictEqual(result.supportedVersions, ["2026-07-28"])
234 assert.deepStrictEqual(result.capabilities, { completions: {}, logging: {} })
235 assert.strictEqual(result.ttlMs, 0)
236 assert.deepStrictEqual(result._meta["io.modelcontextprotocol/serverInfo"], {
237 name: "McpConformance",
238 version: "1.0.0"
239 })
240 }))
241
242 it.effect("should accept every required routing name when the header matches the request", () =>
243 Effect.gen(function*() {
244 const test = yield* McpConformance
245 const cases = [
246 ["tools/call", { name: "ConformanceTool", arguments: {} }, "ConformanceTool"],
247 ["resources/read", { uri: "file:///conformance.txt" }, "file:///conformance.txt"],
248 ["prompts/get", { name: "ConformancePrompt", arguments: {} }, "ConformancePrompt"]
249 ] as const
250
251 for (const [index, [method, params, name]] of cases.entries()) {
252 const response = yield* test.post(request(index + 1, method, params), headers(method, name))
253 assert.strictEqual(response.status, 200)
254 }
255
256 const encodedName = `=?base64?${Base64.encode("file:///conformance.txt")}?=`
257 const encoded = yield* test.post(
258 request(4, "resources/read", { uri: "file:///conformance.txt" }),
259 headers("resources/read", encodedName)
260 )
261 assert.strictEqual(encoded.status, 200)
262 }))
263
264 // Conformance: http-header-validation
265 it.effect("should treat an unmatched Base64 wrapper as a literal when routing a request", () =>
266 Effect.gen(function*() {
267 const test = yield* McpConformance
268 const value = "=?base64?SGVsbG8="
269 const response = yield* test.post(
270 request(5, "tools/call", { name: "HeaderTool", arguments: { region: value } }),
271 { ...headers("tools/call", "HeaderTool"), "Mcp-Param-region": value }
272 )
273
274 assert.strictEqual(response.status, 200)
275 }))
276
277 it.effect("should reject routing headers when they are missing, malformed, or mismatched", () =>
278 Effect.gen(function*() {
279 const test = yield* McpConformance
280 const cases = [
281 [request("missing", "tools/list"), { "MCP-Protocol-Version": protocol.protocolVersion }],
282 [request("mismatch", "tools/list"), headers("tools/call")],
283 [
284 request("malformed", "resources/read", { uri: "file:///conformance.txt" }),
285 headers("resources/read", "=?base64?not-valid!?=")
286 ],
287 [
288 request("invalid-utf8", "resources/read", { uri: "\uFFFD" }),
289 headers("resources/read", "=?base64?/w==?=")
290 ]
291 ] as const
292
293 // SEP-2243: https://modelcontextprotocol.io/seps/2243-http-standardization
294 // The final specification assigns the HeaderMismatch error code.
295 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#request-metadata
296 for (const [body, requestHeaders] of cases) {
297 const response = yield* test.post(body, requestHeaders)
298 const error = yield* decodeError(response)
299 assert.strictEqual(response.status, 400)
300 assert.strictEqual(error.id, body.id)
301 assert.strictEqual(error.error.code, McpSchema.HEADER_MISMATCH_ERROR_CODE)
302 }
303 }))
304
305 it.effect("should reject an unsupported request version with the supported versions", () =>
306 Effect.gen(function*() {
307 const test = yield* McpConformance
308 const body = {
309 ...request(5, "tools/list"),
310 params: {
311 _meta: {
312 ...metadata,
313 "io.modelcontextprotocol/protocolVersion": "2099-01-01"
314 }
315 }
316 }
317 const response = yield* test.post(body, {
318 "MCP-Protocol-Version": "2099-01-01",
319 "Mcp-Method": "tools/list"
320 })
321 const error = yield* decodeError(response)
322
323 assert.strictEqual(response.status, 400)
324 assert.strictEqual(error.id, 5)
325 assert.strictEqual(error.error.code, -32022)
326 assert.deepStrictEqual(error.error.data, {
327 supported: ["2026-07-28"],
328 requested: "2099-01-01"
329 })
330 }))
331
332 // https://modelcontextprotocol.io/specification/2026-07-28/changelog#removals
333 it.effect("should return method not found when a request uses an unknown or removed method", () =>
334 Effect.gen(function*() {
335 const test = yield* McpConformance
336 const methods = [
337 "unknown/method",
338 "ping",
339 "logging/setLevel",
340 "resources/subscribe",
341 "resources/unsubscribe",
342 "tasks/get"
343 ] as const
344
345 for (const [index, method] of methods.entries()) {
346 const id = index + 6
347 const response = yield* test.post(request(id, method), headers(method))
348 const error = yield* decodeError(response)
349 assert.strictEqual(response.status, 404, method)
350 assert.strictEqual(error.id, id, method)
351 assert.strictEqual(error.error.code, McpSchema.METHOD_NOT_FOUND_ERROR_CODE, method)
352 }
353 }))
354 })
355
356 describe("Request metadata", () => {
357 it.effect("should return InvalidParams when required request metadata is missing", () =>
358 Effect.gen(function*() {
359 const test = yield* McpConformance
360 const body = request(20, "tools/list")
361 const cases = [
362 { ...body, params: {} },
363 {
364 ...body,
365 params: {
366 _meta: {
367 "io.modelcontextprotocol/clientCapabilities": {},
368 "io.modelcontextprotocol/clientInfo": metadata["io.modelcontextprotocol/clientInfo"]
369 }
370 }
371 },
372 {
373 ...body,
374 params: {
375 _meta: { "io.modelcontextprotocol/protocolVersion": protocol.protocolVersion }
376 }
377 }
378 ]
379
380 for (const requestBody of cases) {
381 const response = yield* test.post(requestBody, headers("tools/list"))
382 const error = yield* decodeError(response)
383 assert.strictEqual(response.status, 400)
384 assert.strictEqual(error.id, body.id)
385 assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE)
386 }
387 }))
388
389 it.effect("should accept a request when optional client identity is omitted", () =>
390 Effect.gen(function*() {
391 const test = yield* McpConformance
392 const body = request(22, "tools/list")
393 const response = yield* test.post({
394 ...body,
395 params: {
396 _meta: {
397 "io.modelcontextprotocol/protocolVersion": protocol.protocolVersion,
398 "io.modelcontextprotocol/clientCapabilities": {}
399 }
400 }
401 }, headers("tools/list"))
402
403 assert.strictEqual(response.status, 200)
404 }))
405
406 it.effect("should preserve caller metadata alongside authoritative protocol facts", () =>
407 Effect.gen(function*() {
408 const test = yield* McpConformance
409 const discovered = yield* test.initialize({ server: "features" })
410 const response = yield* test.send(discovered, {
411 jsonrpc: "2.0",
412 id: 8,
413 method: "tools/call",
414 params: {
415 name: "RequestMetadataTool",
416 arguments: {},
417 _meta: { sentinel: "preserved" }
418 }
419 })
420 const message = yield* test.decodeResult(response)
421 const result = Schema.decodeUnknownSync(Schema.Struct({ structuredContent: Schema.String }))(message.result)
422 const observed = Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Unknown))(
423 JSON.parse(result.structuredContent)
424 )
425
426 assert.strictEqual(observed.sentinel, "preserved")
427 assert.strictEqual(observed["io.modelcontextprotocol/protocolVersion"], protocol.protocolVersion)
428 assert.deepStrictEqual(observed["io.modelcontextprotocol/clientCapabilities"], {
429 elicitation: { form: {} },
430 roots: {},
431 sampling: {}
432 })
433 assert.deepStrictEqual(observed["io.modelcontextprotocol/clientInfo"], {
434 name: "McpConformanceClient",
435 version: "1.0.0"
436 })
437 }))
438 })
439
440 describe("Result envelopes", () => {
441 // Conformance: sep-2164-resource-not-found
442 it.effect("should include the requested URI when a resource is not found", () =>
443 Effect.gen(function*() {
444 const test = yield* McpConformance
445 const uri = "test://nonexistent-resource-for-conformance-testing"
446 const response = yield* test.post(
447 request(29, "resources/read", { uri }),
448 headers("resources/read", uri)
449 )
450 const error = yield* decodeError(response)
451
452 assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE)
453 assert.deepStrictEqual(error.error.data, { uri })
454 }))
455
456 // SEP-2322 requires every successful result to declare its result type.
457 // SEP-2549 requires cache metadata on discovery, list, and resource-read results.
458 it.effect("should attach modern result and cache metadata to every cacheable operation", () =>
459 Effect.gen(function*() {
460 const test = yield* McpConformance
461 const discovered = yield* test.initialize({ server: "features" })
462 const cases = [
463 ["server/discover", {}],
464 ["tools/list", {}],
465 ["prompts/list", {}],
466 ["resources/list", {}],
467 ["resources/templates/list", {}],
468 ["resources/read", { uri: "file:///test" }]
469 ] as const
470
471 for (const [index, [method, params]] of cases.entries()) {
472 const response = yield* test.send(discovered, {
473 jsonrpc: "2.0",
474 id: index + 30,
475 method,
476 params
477 })
478 assert.strictEqual(response.status, 200, method)
479 const body = yield* Effect.promise<unknown>(() => response.json())
480 assert.isObject(body, method)
481 assert.property(body, "result", method)
482 const result = Schema.decodeUnknownSync(Schema.Struct({
483 resultType: Schema.Literal("complete"),
484 ttlMs: Schema.Int,
485 cacheScope: Schema.Literals(["public", "private"]),
486 _meta: Schema.Struct({
487 "io.modelcontextprotocol/serverInfo": Schema.Struct({
488 name: Schema.String,
489 version: Schema.String
490 })
491 })
492 }))((body as { result: unknown }).result)
493
494 assert.isAtLeast(result.ttlMs, 0)
495 assert.deepStrictEqual(result._meta["io.modelcontextprotocol/serverInfo"], {
496 name: "McpConformance",
497 version: "1.0.0"
498 })
499 }
500 }))
501
502 // https://modelcontextprotocol.io/specification/2026-07-28/basic#results
503 it.effect("should attach a complete result type and server identity to every non-cacheable operation", () =>
504 Effect.gen(function*() {
505 const test = yield* McpConformance
506 const discovered = yield* test.initialize({ server: "features" })
507 const cases = [
508 ["tools/call", { name: "TestTool", arguments: { value: "called" } }],
509 ["prompts/get", { name: "NoArgumentPrompt", arguments: {} }],
510 [
511 "completion/complete",
512 {
513 ref: { type: "ref/prompt", name: "TestPrompt" },
514 argument: { name: "required", value: "f" }
515 }
516 ]
517 ] as const
518
519 for (const [index, [method, params]] of cases.entries()) {
520 const response = yield* test.send(discovered, {
521 jsonrpc: "2.0",
522 id: index + 50,
523 method,
524 params
525 })
526 const message = yield* test.decodeResult(response)
527 const result = Schema.decodeUnknownSync(Schema.Struct({
528 resultType: Schema.Literal("complete"),
529 _meta: Schema.Struct({
530 "io.modelcontextprotocol/serverInfo": Schema.Struct({
531 name: Schema.String,
532 version: Schema.String
533 })
534 })
535 }))(message.result)
536
537 assert.deepStrictEqual(result._meta["io.modelcontextprotocol/serverInfo"], {
538 name: "McpConformance",
539 version: "1.0.0"
540 })
541 }
542 }))
543 })
544
545 describe("Multi round-trip request capabilities", () => {
546 // Conformance: input-required-result-tampered-state
547 it.effect("should return InvalidParams when a resumed tool rejects its requestState", () =>
548 Effect.gen(function*() {
549 const test = yield* McpConformance
550 const discovered = yield* test.initialize({ server: "features" })
551 const response = yield* test.send(discovered, {
552 jsonrpc: "2.0",
553 id: 37,
554 method: "tools/call",
555 params: {
556 name: MrtrInvalidStateToolName,
557 arguments: {},
558 inputResponses: { approval: { action: "accept", content: { approved: true } } },
559 requestState: "tampered"
560 }
561 })
562 const error = yield* decodeError(response)
563
564 assert.strictEqual(response.status, 200)
565 assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE)
566 }))
567
568 // https://modelcontextprotocol.io/specification/2026-07-28/client/sampling#tools-in-sampling
569 it.effect("should reject tool-enabled sampling when the client omits sampling.tools", () =>
570 Effect.gen(function*() {
571 const test = yield* McpConformance
572 const discovered = yield* test.initialize({ server: "features" })
573 const cases = [MrtrSamplingToolsToolName, MrtrSamplingToolChoiceToolName] as const
574 for (const [index, name] of cases.entries()) {
575 const response = yield* test.send(discovered, {
576 jsonrpc: "2.0",
577 id: index + 38,
578 method: "tools/call",
579 params: { name, arguments: {} }
580 }, { clientCapabilities: { sampling: {} } })
581 const error = yield* decodeError(response)
582
583 assert.strictEqual(response.status, 400)
584 assert.strictEqual(error.error.code, -32021)
585 assert.deepStrictEqual(error.error.data, {
586 requiredCapabilities: { sampling: { tools: {} } }
587 })
588 }
589 }))
590
591 // https://modelcontextprotocol.io/specification/2026-07-28/client/elicitation#capabilities
592 it.effect("should treat an empty elicitation capability as form support", () =>
593 Effect.gen(function*() {
594 const test = yield* McpConformance
595 const discovered = yield* test.initialize({ server: "features" })
596 const response = yield* test.send(discovered, {
597 jsonrpc: "2.0",
598 id: 39,
599 method: "tools/call",
600 params: { name: MrtrToolName, arguments: {} }
601 }, {
602 clientCapabilities: {
603 elicitation: {},
604 sampling: {},
605 roots: {}
606 }
607 })
608 const message = yield* test.decodeResult(response)
609 const result = Schema.decodeUnknownSync(Schema.Struct({
610 resultType: Schema.Literal("input_required"),
611 inputRequests: Schema.Record(Schema.String, Schema.Unknown)
612 }))(message.result)
613
614 assert.deepStrictEqual(result.inputRequests.approval, {
615 method: "elicitation/create",
616 params: {
617 message: "Approve the operation",
618 requestedSchema: {
619 type: "object",
620 properties: {
621 approved: { type: "boolean" },
622 color: { type: "string", enum: ["red", "blue"] },
623 titled: { type: "string", oneOf: [{ const: "red", title: "Red" }, { const: "blue", title: "Blue" }] },
624 legacy: { type: "string", enum: ["red", "blue"], enumNames: ["Red", "Blue"] }
625 },
626 required: ["approved"]
627 }
628 }
629 })
630 }))
631
632 // https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr#server-requirements-capability-validation
633 it.effect("should reject input requests when the client omits their required capabilities", () =>
634 Effect.gen(function*() {
635 const test = yield* McpConformance
636 const discovered = yield* test.initialize({ server: "features" })
637 const response = yield* test.send(discovered, {
638 jsonrpc: "2.0",
639 id: 40,
640 method: "tools/call",
641 params: { name: MrtrToolName, arguments: {} }
642 }, { clientCapabilities: {} })
643 const error = yield* decodeError(response)
644
645 assert.strictEqual(response.status, 400)
646 assert.strictEqual(error.error.code, -32021)
647 const data = Schema.decodeUnknownSync(Schema.Struct({
648 requiredCapabilities: Schema.JsonObject
649 }))(error.error.data)
650 assert.deepStrictEqual(data, {
651 requiredCapabilities: {
652 elicitation: { form: {} },
653 sampling: {},
654 roots: {}
655 }
656 })
657 }))
658 })
659})