1import type * as McpProtocol from "effect/ai/McpProtocol"
2import * as McpSchema from "effect/ai/McpSchema"
3import * as McpServer from "effect/ai/McpServer"
4import * as Tool from "effect/ai/Tool"
5import * as Toolkit from "effect/ai/Toolkit"
6import * as Context from "effect/Context"
7import * as Effect from "effect/Effect"
8import * as Layer from "effect/Layer"
9import * as Ref from "effect/Ref"
10import { CurrentLogLevel } from "effect/References"
11import * as Schema from "effect/Schema"
12import { makeServerLayer } from "../TestUtils/McpServerLayer.ts"
13
14export interface Observations {
15 readonly toolInvocations: number
16 readonly promptInvocations: number
17 readonly resourceTemplateInvocations: number
18}
19
20export const MrtrToolName = "MrtrTool"
21export const MrtrStateOnlyToolName = "MrtrStateOnlyTool"
22export const MrtrInvalidStateToolName = "MrtrInvalidStateTool"
23export const MrtrPromptName = "MrtrPrompt"
24export const MrtrSamplingToolsToolName = "MrtrSamplingToolsTool"
25export const MrtrSamplingToolChoiceToolName = "MrtrSamplingToolChoiceTool"
26export const mrtrRequestState = "opaque:+/=\u0000é"
27
28const TestTool = Tool.make("TestTool", {
29 description: "A test tool",
30 parameters: Schema.Struct({
31 value: Schema.String
32 }),
33 success: Schema.String
34})
35
36const makeStructuredTool = (protocolVersion: string) =>
37 Tool.make("StructuredTool", {
38 parameters: Tool.EmptyParams,
39 success: Schema.Struct({
40 value: Schema.String
41 })
42 }).annotate(
43 McpSchema.EnabledWhen,
44 (client) => client.protocolVersion === protocolVersion
45 )
46
47const LogLevelTool = Tool.make("LogLevelTool", {
48 parameters: Tool.EmptyParams,
49 success: Schema.String,
50 dependencies: [CurrentLogLevel]
51})
52
53const RequestMetadataTool = Tool.make("RequestMetadataTool", {
54 parameters: Tool.EmptyParams,
55 success: Schema.String,
56 dependencies: [McpSchema.McpRequestContext]
57})
58
59const makeTestToolkitLayer = (observations: Ref.Ref<Observations>, protocolVersion: string) => {
60 const TestToolkit = Toolkit.make(TestTool, makeStructuredTool(protocolVersion), LogLevelTool, RequestMetadataTool)
61 return McpServer.toolkit(TestToolkit).pipe(
62 Layer.provide(TestToolkit.toLayer({
63 TestTool: ({ value }) =>
64 Ref.update(observations, (current) => ({
65 ...current,
66 toolInvocations: current.toolInvocations + 1
67 })).pipe(Effect.as(value)),
68 StructuredTool: () => Effect.succeed({ value: "structured" }),
69 LogLevelTool: () => CurrentLogLevel,
70 RequestMetadataTool: () =>
71 McpSchema.McpRequestContext.useSync((context) => JSON.stringify(context.requestMetadata))
72 }))
73 )
74}
75
76const makeContentToolsLayer = Layer.effectDiscard(
77 Effect.gen(function*() {
78 const server = yield* McpServer.McpServer
79 const add = (
80 name: string,
81 result: McpSchema.CallToolResult
82 ) =>
83 server.addTool({
84 tool: new McpSchema.Tool({
85 name,
86 inputSchema: { type: "object", properties: {} }
87 }),
88 annotations: Context.empty(),
89 handle: () => Effect.succeed(result)
90 })
91
92 yield* add(
93 "ImageTool",
94 new McpSchema.CallToolResult({
95 content: [{
96 type: "image",
97 data: new Uint8Array([1, 2, 3]),
98 mimeType: "image/png"
99 }]
100 })
101 )
102 yield* add(
103 "EmbeddedResourceTool",
104 new McpSchema.CallToolResult({
105 content: [{
106 type: "resource",
107 resource: {
108 uri: "file:///embedded",
109 mimeType: "text/plain",
110 text: "embedded"
111 }
112 }]
113 })
114 )
115 yield* add(
116 "MultipleContentTool",
117 new McpSchema.CallToolResult({
118 content: [
119 { type: "text", text: "first" },
120 { type: "text", text: "second" }
121 ]
122 })
123 )
124 yield* add(
125 "ErrorTool",
126 new McpSchema.CallToolResult({
127 content: [{ type: "text", text: "expected failure" }],
128 isError: true
129 })
130 )
131 yield* server.addTool({
132 tool: new McpSchema.Tool({
133 name: "DefectTool",
134 inputSchema: { type: "object", properties: {} }
135 }),
136 annotations: Context.empty(),
137 handle: () => Effect.die("private defect details")
138 })
139
140 yield* server.addTool({
141 tool: new McpSchema.Tool({
142 name: "JsonSchema2020Tool",
143 inputSchema: {
144 $schema: "https://json-schema.org/draft/2020-12/schema",
145 type: "object",
146 $defs: { identifier: { type: "string" } },
147 properties: { value: { $ref: "#/$defs/identifier" } },
148 allOf: [{ required: ["value"] }],
149 unevaluatedProperties: false
150 }
151 }),
152 annotations: Context.make(
153 McpSchema.EnabledWhen,
154 (client) => client.protocolVersion === "2026-07-28"
155 ),
156 handle: () => Effect.succeed(new McpSchema.CallToolResult({ content: [] }))
157 })
158
159 yield* add(
160 "AudioTool",
161 new McpSchema.CallToolResult({
162 content: [{
163 type: "audio",
164 data: new Uint8Array([4, 5, 6]),
165 mimeType: "audio/wav"
166 }]
167 })
168 )
169 yield* add(
170 "ResourceLinkTool",
171 new McpSchema.CallToolResult({
172 content: [{
173 type: "resource_link",
174 uri: "file:///test",
175 name: "TestResource",
176 mimeType: "text/plain"
177 }]
178 })
179 )
180 })
181)
182
183const makeHeaderToolLayer = (observations: Ref.Ref<Observations>) =>
184 Layer.effectDiscard(
185 Effect.gen(function*() {
186 const server = yield* McpServer.McpServer
187 yield* server.addTool({
188 tool: new McpSchema.Tool({
189 name: "HeaderTool",
190 inputSchema: {
191 type: "object",
192 properties: {
193 region: {
194 type: "string",
195 "x-mcp-header": "Region"
196 },
197 shard: {
198 type: "integer",
199 "x-mcp-header": "Shard"
200 },
201 routing: {
202 type: "object",
203 properties: { region: { type: "string", "x-mcp-header": "Nested-Region" } }
204 },
205 dryRun: {
206 type: "boolean",
207 "x-mcp-header": "Dry-Run"
208 }
209 },
210 required: ["region"]
211 }
212 }),
213 annotations: Context.make(
214 McpSchema.EnabledWhen,
215 (client) => client.protocolVersion === "2026-07-28"
216 ),
217 handle: () =>
218 Ref.update(observations, (current) => ({
219 ...current,
220 toolInvocations: current.toolInvocations + 1
221 })).pipe(Effect.as(new McpSchema.CallToolResult({ content: [] })))
222 })
223 })
224 )
225
226const mrtrToolLayer = Layer.effectDiscard(
227 Effect.gen(function*() {
228 const server = yield* McpServer.McpServer
229 yield* server.addTool({
230 tool: new McpSchema.Tool({
231 name: MrtrToolName,
232 description: "Requests confirmation before completing",
233 inputSchema: { type: "object" }
234 }),
235 annotations: Context.make(
236 McpSchema.EnabledWhen,
237 (client) => client.protocolVersion === "2026-07-28"
238 ),
239 handle: () =>
240 McpSchema.McpRequestContext.useSync((context) => {
241 const approval = context.inputResponses?.approval
242 const sample = context.inputResponses?.sample
243 const roots = context.inputResponses?.roots
244 if (
245 context.requestState === mrtrRequestState && approval?.action === "accept" &&
246 sample !== undefined && roots !== undefined
247 ) {
248 return new McpSchema.CallToolResult({
249 content: [{
250 type: "text",
251 text: JSON.stringify({ approval: approval.content, sample, roots })
252 }]
253 })
254 }
255 return new McpSchema.InputRequired({
256 inputRequests: {
257 approval: {
258 method: "elicitation/create",
259 params: {
260 message: "Approve the operation",
261 requestedSchema: {
262 type: "object",
263 properties: {
264 approved: { type: "boolean" },
265 color: { type: "string", enum: ["red", "blue"] },
266 titled: {
267 type: "string",
268 oneOf: [{ const: "red", title: "Red" }, { const: "blue", title: "Blue" }]
269 },
270 legacy: { type: "string", enum: ["red", "blue"], enumNames: ["Red", "Blue"] }
271 },
272 required: ["approved"]
273 }
274 }
275 },
276 sample: {
277 method: "sampling/createMessage",
278 params: {
279 messages: [{ role: "user", content: { type: "text", text: "Suggest a title" } }],
280 maxTokens: 20
281 }
282 },
283 roots: {
284 method: "roots/list"
285 }
286 },
287 requestState: mrtrRequestState
288 })
289 })
290 })
291 yield* server.addTool({
292 tool: new McpSchema.Tool({
293 name: MrtrStateOnlyToolName,
294 description: "Requests a retry without client input",
295 inputSchema: { type: "object" }
296 }),
297 annotations: Context.make(
298 McpSchema.EnabledWhen,
299 (client) => client.protocolVersion === "2026-07-28"
300 ),
301 handle: () => Effect.succeed(new McpSchema.InputRequired({ requestState: mrtrRequestState }))
302 })
303 yield* server.addTool({
304 tool: new McpSchema.Tool({
305 name: MrtrInvalidStateToolName,
306 description: "Rejects an invalid continuation state",
307 inputSchema: { type: "object" }
308 }),
309 annotations: Context.make(
310 McpSchema.EnabledWhen,
311 (client) => client.protocolVersion === "2026-07-28"
312 ),
313 handle: () => Effect.fail(new McpSchema.InvalidParams({ message: "requestState integrity check failed" }))
314 })
315 const samplingToolRequests = [
316 [
317 MrtrSamplingToolsToolName,
318 {
319 messages: [{ role: "user", content: { type: "text", text: "Check the weather" } }],
320 maxTokens: 20,
321 tools: [{ name: "weather", inputSchema: { type: "object" } }]
322 }
323 ],
324 [
325 MrtrSamplingToolChoiceToolName,
326 {
327 messages: [{ role: "user", content: { type: "text", text: "Check the weather" } }],
328 maxTokens: 20,
329 toolChoice: { mode: "required" }
330 }
331 ]
332 ] as const
333 for (const [name, params] of samplingToolRequests) {
334 yield* server.addTool({
335 tool: new McpSchema.Tool({
336 name,
337 description: "Requests tool-enabled sampling before completing",
338 inputSchema: { type: "object" }
339 }),
340 annotations: Context.make(
341 McpSchema.EnabledWhen,
342 (client) => client.protocolVersion === "2026-07-28"
343 ),
344 handle: () =>
345 Effect.succeed(
346 new McpSchema.InputRequired({
347 inputRequests: {
348 sample: { method: "sampling/createMessage", params }
349 }
350 })
351 )
352 })
353 }
354 yield* server.addPrompt({
355 prompt: new McpSchema.Prompt({
356 name: MrtrPromptName,
357 description: "Requests client input before returning a prompt"
358 }),
359 annotations: Context.make(
360 McpSchema.EnabledWhen,
361 (client) => client.protocolVersion === "2026-07-28"
362 ),
363 completions: {},
364 handle: () =>
365 McpSchema.McpRequestContext.useSync((context) =>
366 context.inputResponses?.userContext === undefined
367 ? new McpSchema.InputRequired({
368 inputRequests: {
369 userContext: {
370 method: "elicitation/create",
371 params: {
372 message: "What context should the prompt use?",
373 requestedSchema: {
374 type: "object",
375 properties: { context: { type: "string" } },
376 required: ["context"]
377 }
378 }
379 }
380 }
381 })
382 : new McpSchema.GetPromptResult({
383 messages: [{ role: "user", content: { type: "text", text: "Prompt completed" } }]
384 })
385 )
386 })
387 })
388)
389
390const templatePath = McpSchema.param("path", Schema.String)
391const TestResourceTemplate = McpServer.resource`file:///template/${templatePath}`({
392 name: "TestResourceTemplate",
393 description: "A test resource template",
394 mimeType: "text/plain",
395 completion: {
396 path: (value) => Effect.succeed(value === "" ? ["beta", "alpha"] : ["alpha", "beta"])
397 },
398 content: (uri, path) => Effect.succeed(`${uri}:${path}`)
399})
400
401const numericId = McpSchema.param("id", Schema.FiniteFromString)
402const makeNumericResourceTemplate = (observations: Ref.Ref<Observations>) =>
403 McpServer.resource`file:///numeric/${numericId}`({
404 name: "NumericResourceTemplate",
405 content: (uri) =>
406 Ref.update(observations, (current) => ({
407 ...current,
408 resourceTemplateInvocations: current.resourceTemplateInvocations + 1
409 })).pipe(Effect.as(uri))
410 })
411
412const ImagePrompt = McpServer.prompt({
413 name: "ImagePrompt",
414 content: () =>
415 Effect.succeed([{
416 role: "user",
417 content: McpSchema.ImageContent.make({
418 data: new Uint8Array([1, 2, 3]),
419 mimeType: "image/png"
420 })
421 }])
422})
423
424const AudioPrompt = McpServer.prompt({
425 name: "AudioPrompt",
426 content: () =>
427 Effect.succeed([{
428 role: "user",
429 content: McpSchema.AudioContent.make({
430 data: new Uint8Array([4, 5, 6]),
431 mimeType: "audio/wav"
432 })
433 }])
434})
435
436const EmbeddedResourcePrompt = McpServer.prompt({
437 name: "EmbeddedResourcePrompt",
438 content: () =>
439 Effect.succeed([{
440 role: "user",
441 content: McpSchema.EmbeddedResource.make({
442 resource: {
443 uri: "file:///embedded",
444 mimeType: "text/plain",
445 text: "embedded"
446 }
447 })
448 }])
449})
450
451const ContextCompletionPrompt = McpServer.prompt({
452 name: "ContextCompletionPrompt",
453 parameters: {
454 value: Schema.String
455 },
456 completion: {
457 value: (_input, context) =>
458 Effect.succeed(context?.arguments?.locale === "en" ? ["context received"] : ["context missing"])
459 },
460 content: ({ value }) => Effect.succeed(value)
461})
462
463export const makeFeaturesServerLayer = (
464 protocol: McpProtocol.ProtocolAdapter,
465 observations: Ref.Ref<Observations>
466) =>
467 Layer.mergeAll(
468 makeTestToolkitLayer(observations, protocol.protocolVersion),
469 makeContentToolsLayer,
470 makeHeaderToolLayer(observations),
471 mrtrToolLayer,
472 McpServer.resource({
473 uri: "file:///test",
474 name: "TestResource",
475 description: "A test resource",
476 mimeType: "text/plain",
477 content: Effect.succeed(McpSchema.ReadResourceResult.make({
478 contents: [{
479 uri: "file:///test",
480 mimeType: "text/plain",
481 text: "test"
482 }]
483 }))
484 }),
485 McpServer.resource({
486 uri: "file:///binary",
487 name: "BinaryResource",
488 mimeType: "application/octet-stream",
489 content: Effect.succeed(McpSchema.ReadResourceResult.make({
490 contents: [{
491 uri: "file:///binary",
492 mimeType: "application/octet-stream",
493 blob: new Uint8Array([1, 2, 3])
494 }]
495 }))
496 }),
497 McpServer.resource({
498 uri: "file:///multiple",
499 name: "MultipleResource",
500 content: Effect.succeed(McpSchema.ReadResourceResult.make({
501 contents: [
502 {
503 uri: "file:///multiple#first",
504 mimeType: "text/plain",
505 text: "first"
506 },
507 {
508 uri: "file:///multiple#second",
509 mimeType: "text/plain",
510 text: "second"
511 }
512 ]
513 }))
514 }),
515 TestResourceTemplate,
516 makeNumericResourceTemplate(observations),
517 McpServer.prompt({
518 name: "TestPrompt",
519 title: "Test prompt",
520 description: "A test prompt",
521 parameters: {
522 required: Schema.String,
523 optional: Schema.optional(Schema.String)
524 },
525 completion: {
526 required: (value) =>
527 Effect.succeed(
528 value === "limit"
529 ? Array.from({ length: 101 }, (_, index) => `value-${index}`)
530 : ["first", "second"]
531 )
532 },
533 content: ({ optional, required }) =>
534 Ref.update(observations, (current) => ({
535 ...current,
536 promptInvocations: current.promptInvocations + 1
537 })).pipe(Effect.as(`${required}:${optional ?? "omitted"}`))
538 }),
539 McpServer.prompt({
540 name: "NoArgumentPrompt",
541 content: () => Effect.succeed("no arguments")
542 }),
543 ImagePrompt,
544 EmbeddedResourcePrompt,
545 AudioPrompt,
546 ContextCompletionPrompt
547 ).pipe(
548 Layer.provide(makeServerLayer({
549 name: "McpConformance",
550 instructions: "Follow the test server instructions.",
551 protocols: [protocol],
552 extensions: { "example/lifecycle": { enabled: true } }
553 }))
554 )