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 * as Schema from "effect/Schema"
6import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts"
7
8const decodeCompletion = Schema.decodeUnknownEffect(McpSchema.CompleteResult)
9
10const complete = (
11 ref: { readonly type: "ref/prompt"; readonly name: string } | {
12 readonly type: "ref/resource"
13 readonly uri: string
14 },
15 argument: { readonly name: string; readonly value: string },
16 context?: { readonly arguments?: Readonly<Record<string, string>> | undefined }
17) =>
18 Effect.gen(function*() {
19 const response = yield* completeRaw(ref, argument, context)
20 const test = yield* McpConformance
21 return yield* test.decodeResult(response).pipe(
22 Effect.flatMap((message) => decodeCompletion(message.result))
23 )
24 })
25
26const completeRaw = (
27 ref: { readonly type: "ref/prompt"; readonly name: string } | {
28 readonly type: "ref/resource"
29 readonly uri: string
30 },
31 argument: { readonly name: string; readonly value: string },
32 context?: { readonly arguments?: Readonly<Record<string, string>> | undefined }
33) =>
34 Effect.gen(function*() {
35 const test = yield* McpConformance
36 const initialized = yield* test.initialize({ server: "features" })
37 yield* test.notifyInitialized(initialized)
38 const response = yield* test.send(initialized, {
39 jsonrpc: "2.0",
40 id: 2,
41 method: "completion/complete",
42 params: { ref, argument, ...(context === undefined ? {} : { context }) }
43 })
44 return response
45 })
46
47export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) =>
48 it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => {
49 describe("Completion", () => {
50 // https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/completion
51 describe("Capabilities", () => {
52 it.effect("MUST advertise completions when argument completion is supported", () =>
53 Effect.gen(function*() {
54 const test = yield* McpConformance
55 const initialized = yield* test.initialize({ server: "features" })
56
57 assert.property(initialized.message.result.capabilities, "completions")
58 }))
59 })
60
61 describe("Requesting Completions", () => {
62 it.effect("MUST complete a prompt argument", () =>
63 Effect.gen(function*() {
64 const result = yield* complete(
65 { type: "ref/prompt", name: "TestPrompt" },
66 { name: "required", value: "f" }
67 )
68
69 assert.deepStrictEqual(result.completion.values, ["first", "second"])
70 }))
71
72 it.effect("MUST complete a resource template argument", () =>
73 Effect.gen(function*() {
74 const result = yield* complete(
75 { type: "ref/resource", uri: "file:///template/{path}" },
76 { name: "path", value: "a" }
77 )
78
79 assert.deepStrictEqual(result.completion.values, ["alpha", "beta"])
80 }))
81 it.effect("SHOULD reject an unknown prompt reference with Invalid Params", () =>
82 Effect.gen(function*() {
83 const test = yield* McpConformance
84 const initialized = yield* test.initialize({ server: "features" })
85 yield* test.notifyInitialized(initialized)
86 const response = yield* test.send(initialized, {
87 jsonrpc: "2.0",
88 id: 2,
89 method: "completion/complete",
90 params: {
91 ref: { type: "ref/prompt", name: "UnknownPrompt" },
92 argument: { name: "value", value: "" }
93 }
94 })
95 const error = yield* test.decodeError(response)
96
97 assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE)
98 }))
99
100 it.effect("MUST reject an unknown argument name", () =>
101 Effect.gen(function*() {
102 const test = yield* McpConformance
103 const initialized = yield* test.initialize({ server: "features" })
104 yield* test.notifyInitialized(initialized)
105 const response = yield* test.send(initialized, {
106 jsonrpc: "2.0",
107 id: 2,
108 method: "completion/complete",
109 params: {
110 ref: { type: "ref/prompt", name: "TestPrompt" },
111 argument: { name: "unknown", value: "" }
112 }
113 })
114 const error = yield* test.decodeError(response)
115
116 assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE)
117 }))
118
119 it.effect("MUST return completion values in order", () =>
120 Effect.gen(function*() {
121 const result = yield* complete(
122 { type: "ref/resource", uri: "file:///template/{path}" },
123 { name: "path", value: "" }
124 )
125
126 assert.deepStrictEqual(result.completion.values, ["beta", "alpha"])
127 }))
128
129 it.effect("SCHEMA returns the total and additional-results indicator", () =>
130 Effect.gen(function*() {
131 const result = yield* complete(
132 { type: "ref/resource", uri: "file:///template/{path}" },
133 { name: "path", value: "" }
134 )
135
136 assert.strictEqual(result.completion.total, 2)
137 assert.strictEqual(result.completion.hasMore, false)
138 }))
139
140 it.effect("MUST return at most one hundred completion values", () =>
141 Effect.gen(function*() {
142 const result = yield* complete(
143 { type: "ref/prompt", name: "TestPrompt" },
144 { name: "required", value: "limit" }
145 )
146
147 assert.strictEqual(result.completion.values.length, 100)
148 assert.strictEqual(result.completion.total, 101)
149 assert.strictEqual(result.completion.hasMore, true)
150 }))
151
152 // https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/completion#completioncontext
153 it.effect.skipIf(["2024-11-05", "2025-03-26"].includes(protocol.protocolVersion))(
154 "should pass previously resolved arguments when completion context is supplied",
155 () =>
156 Effect.gen(function*() {
157 const result = yield* complete(
158 { type: "ref/prompt", name: "ContextCompletionPrompt" },
159 { name: "value", value: "c" },
160 { arguments: { locale: "en" } }
161 )
162
163 assert.deepStrictEqual(result.completion.values, ["context received"])
164 })
165 )
166 })
167 })
168 })