| 1 | import type * as McpProtocol from "effect/ai/McpProtocol" |
| 2 | import * as McpSchema from "effect/ai/McpSchema" |
| 3 | import * as McpServer from "effect/ai/McpServer" |
| 4 | import * as Tool from "effect/ai/Tool" |
| 5 | import * as Toolkit from "effect/ai/Toolkit" |
| 6 | import * as Context from "effect/Context" |
| 7 | import * as Effect from "effect/Effect" |
| 8 | import * as Layer from "effect/Layer" |
| 9 | import * as Ref from "effect/Ref" |
| 10 | import { CurrentLogLevel } from "effect/References" |
| 11 | import * as Schema from "effect/Schema" |
| 12 | import { makeServerLayer } from "../TestUtils/McpServerLayer.ts" |
| 13 | |
| 14 | export interface Observations { |
| 15 | readonly toolInvocations: number |
| 16 | readonly promptInvocations: number |
| 17 | readonly resourceTemplateInvocations: number |
| 18 | } |
| 19 | |
| 20 | export const MrtrToolName = "MrtrTool" |
| 21 | export const MrtrStateOnlyToolName = "MrtrStateOnlyTool" |
| 22 | export const MrtrInvalidStateToolName = "MrtrInvalidStateTool" |
| 23 | export const MrtrPromptName = "MrtrPrompt" |
| 24 | export const MrtrSamplingToolsToolName = "MrtrSamplingToolsTool" |
| 25 | export const MrtrSamplingToolChoiceToolName = "MrtrSamplingToolChoiceTool" |
| 26 | export const mrtrRequestState = "opaque:+/=\u0000é" |
| 27 | |
| 28 | const TestTool = Tool.make("TestTool", { |
| 29 | description: "A test tool", |
| 30 | parameters: Schema.Struct({ |
| 31 | value: Schema.String |
| 32 | }), |
| 33 | success: Schema.String |
| 34 | }) |
| 35 | |
| 36 | const 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 | |
| 47 | const LogLevelTool = Tool.make("LogLevelTool", { |
| 48 | parameters: Tool.EmptyParams, |
| 49 | success: Schema.String, |
| 50 | dependencies: [CurrentLogLevel] |
| 51 | }) |
| 52 | |
| 53 | const RequestMetadataTool = Tool.make("RequestMetadataTool", { |
| 54 | parameters: Tool.EmptyParams, |
| 55 | success: Schema.String, |
| 56 | dependencies: [McpSchema.McpRequestContext] |
| 57 | }) |
| 58 | |
| 59 | const 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 | |
| 76 | const 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 | |
| 183 | const 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 | |
| 226 | const 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 | |
| 390 | const templatePath = McpSchema.param("path", Schema.String) |
| 391 | const 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 | |
| 401 | const numericId = McpSchema.param("id", Schema.FiniteFromString) |
| 402 | const 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 | |
| 412 | const 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 | |
| 424 | const 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 | |
| 436 | const 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 | |
| 451 | const 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 | |
| 463 | export 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 | ) |