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
7const rootsHandler = (roots: ReadonlyArray<{ readonly uri: string; readonly name?: string }>) => () =>
8 Effect.succeed({ roots })
9
10export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) =>
11 it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => {
12 describe("Roots", () => {
13 // Identical requirements in the 2024-11-05, 2025-03-26, and 2025-06-18 specifications.
14 describe("Capabilities", () => {
15 it.effect("MUST send roots requests when the client advertises roots", () =>
16 Effect.gen(function*() {
17 const test = yield* McpConformance
18 const peer = yield* test.makePeer({
19 capabilities: { roots: {} },
20 handlers: {
21 "roots/list": rootsHandler([])
22 }
23 })
24
25 yield* peer.wireClient["roots/list"](undefined)
26
27 assert.strictEqual((yield* peer.takeRequest).method, "roots/list")
28 }))
29
30 it.effect("MUST accept roots requests when the client advertises list changes", () =>
31 Effect.gen(function*() {
32 const test = yield* McpConformance
33 const peer = yield* test.makePeer({
34 capabilities: { roots: { listChanged: true } },
35 handlers: {
36 "roots/list": rootsHandler([])
37 }
38 })
39
40 yield* peer.wireClient["roots/list"](undefined)
41
42 assert.strictEqual((yield* peer.takeRequest).method, "roots/list")
43 }))
44 })
45
46 describe("Listing Roots", () => {
47 it.effect("MUST accept roots with file URIs and preserve optional names", () =>
48 Effect.gen(function*() {
49 const test = yield* McpConformance
50 const peer = yield* test.makePeer({
51 capabilities: { roots: {} },
52 handlers: {
53 "roots/list": rootsHandler([
54 { uri: "file:///workspace", name: "Workspace" },
55 { uri: "file:///unnamed" }
56 ])
57 }
58 })
59
60 const result = yield* peer.wireClient["roots/list"](undefined)
61
62 assert.deepStrictEqual(
63 result.roots.map((root) => ({
64 uri: root.uri,
65 name: root.name
66 })),
67 [
68 { uri: "file:///workspace", name: "Workspace" },
69 { uri: "file:///unnamed", name: undefined }
70 ]
71 )
72 }))
73
74 it.effect("MAY accept an empty roots list", () =>
75 Effect.gen(function*() {
76 const test = yield* McpConformance
77 const peer = yield* test.makePeer({
78 capabilities: { roots: {} },
79 handlers: {
80 "roots/list": rootsHandler([])
81 }
82 })
83
84 const result = yield* peer.wireClient["roots/list"](undefined)
85
86 assert.deepStrictEqual(result.roots, [])
87 }))
88
89 it.effect("MUST surface client errors returned by roots/list", () =>
90 Effect.gen(function*() {
91 const test = yield* McpConformance
92 const peer = yield* test.makePeer({
93 capabilities: { roots: {} },
94 handlers: {
95 "roots/list": () =>
96 Effect.fail(
97 new McpSchema.InternalError({
98 message: "Roots unavailable"
99 })
100 )
101 }
102 })
103
104 const error = yield* peer.wireClient["roots/list"](undefined).pipe(Effect.flip)
105
106 assert.isTrue("code" in error)
107 if ("code" in error) assert.strictEqual(error.code, McpSchema.INTERNAL_ERROR_CODE)
108 assert.strictEqual(error.message, "Roots unavailable")
109 }))
110 })
111 })
112 })