1/**
2 * Builds Model Context Protocol (MCP) servers with Effect.
3 *
4 * The `McpServer` service stores the tools, resources, resource templates,
5 * prompts, completions, and outgoing notifications exposed by a server. This
6 * module also includes the server runner, custom protocol,
7 * stdio, and HTTP layers, registration helpers, and APIs that let handlers ask
8 * the connected client for structured input or read its advertised
9 * capabilities.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import * as Arr from "../Array.ts"
15import * as Cause from "../Cause.ts"
16import * as Context from "../Context.ts"
17import * as Data from "../Data.ts"
18import * as Deferred from "../Deferred.ts"
19import * as Effect from "../Effect.ts"
20import * as ErrorReporter from "../ErrorReporter.ts"
21import * as Exit from "../Exit.ts"
22import * as Fiber from "../Fiber.ts"
23import * as FindMyWay from "../http/FindMyWay.ts"
24import * as Headers from "../http/Headers.ts"
25import { appendPreResponseHandlerUnsafe } from "../http/HttpEffect.ts"
26import * as HttpRouter from "../http/HttpRouter.ts"
27import * as HttpServerRequest from "../http/HttpServerRequest.ts"
28import * as HttpServerResponse from "../http/HttpServerResponse.ts"
29import type * as JsonSchema from "../JsonSchema.ts"
30import * as Layer from "../Layer.ts"
31import * as Option from "../Option.ts"
32import * as Predicate from "../Predicate.ts"
33import * as PubSub from "../PubSub.ts"
34import * as Queue from "../Queue.ts"
35import * as RcMap from "../RcMap.ts"
36import { CurrentLogLevel, Scheduler } from "../References.ts"
37import * as Result from "../Result.ts"
38import * as Rpc from "../rpc/Rpc.ts"
39import * as RpcClient from "../rpc/RpcClient.ts"
40import type * as RpcGroup from "../rpc/RpcGroup.ts"
41import * as RpcMessage from "../rpc/RpcMessage.ts"
42import * as RpcSerialization from "../rpc/RpcSerialization.ts"
43import * as RpcServer from "../rpc/RpcServer.ts"
44import * as Schema from "../Schema.ts"
45import * as SchemaAST from "../SchemaAST.ts"
46import * as Scope from "../Scope.ts"
47import type { Stdio } from "../Stdio.ts"
48import * as Stream from "../Stream.ts"
49import * as AiError from "./AiError.ts"
50import * as McpCore from "./internal/mcpCore.ts"
51import * as McpProtocolInternal from "./internal/mcpProtocol.ts"
52import * as McpRuntime from "./internal/mcpRuntime.ts"
53import * as InternalStructuredOutput from "./internal/structured-output.ts"
54import type * as McpProtocol from "./McpProtocol.ts"
55import * as McpSchema from "./McpSchema.ts"
56import {
57 CallToolResult,
58 ElicitationDeclined,
59 EnabledWhen,
60 GetPromptResult,
61 InternalError,
62 InvalidParams,
63 InvalidRequest,
64 isParam,
65 McpRequestContext,
66 McpServerClient,
67 McpServerClientMiddleware,
68 MethodNotFound,
69 Prompt,
70 Resource,
71 ResourceTemplate,
72 ServerNotificationRpcs,
73 TextContent,
74 Tool as McpTool,
75 ToolJson
76} from "./McpSchema.ts"
77import type {
78 CallTool,
79 ClientCapabilities,
80 Complete,
81 CompleteResult,
82 GetPrompt,
83 McpErrorBase,
84 Param,
85 PromptArgument,
86 PromptMessage,
87 ReadResourceResult,
88 ServerCapabilities
89} from "./McpSchema.ts"
90import * as Tool from "./Tool.ts"
91import * as Toolkit from "./Toolkit.ts"
92
93type CompletionContext = typeof Complete.payloadSchema.Type["context"]
94
95interface QueuedServerNotification {
96 readonly notification: McpCore.ServerNotification
97 readonly targetClientId?: number | undefined
98 readonly delivered: Deferred.Deferred<void>
99 readonly requestContext?: McpRequestContext["Service"] | undefined
100 readonly requestHeaders?: Headers.Headers | undefined
101}
102
103const internalState = new WeakMap<object, {
104 readonly core: McpCore.McpCore
105 readonly notifications: Queue.Dequeue<QueuedServerNotification>
106 readonly notificationDelivery: { consumers: number }
107}>()
108type ServerExtensions = NonNullable<ServerCapabilities["extensions"]>
109type ServerNotificationRequest<
110 R extends Rpc.Any = RpcGroup.Rpcs<typeof BroadcastServerNotificationRpcs>
111> = R extends Rpc.Any ? RpcMessage.Request<R> : never
112
113const BroadcastServerNotificationRpcs = ServerNotificationRpcs.omit("notifications/elicitation/complete")
114const isLoggingLevel = Schema.is(McpSchema.LoggingLevel)
115
116const toInternalServerNotification = (
117 message: ServerNotificationRequest
118): McpCore.ServerNotification | undefined => {
119 switch (message.tag) {
120 case "notifications/cancelled":
121 return McpCore.ServerNotification.Cancelled({
122 requestId: message.payload.requestId,
123 reason: message.payload.reason,
124 metadata: message.payload._meta
125 })
126 case "notifications/progress":
127 return McpCore.ServerNotification.Progress({
128 progressToken: message.payload.progressToken,
129 progress: message.payload.progress,
130 total: message.payload.total,
131 message: message.payload.message,
132 metadata: message.payload._meta
133 })
134 case "notifications/message":
135 return McpCore.ServerNotification.LoggingMessage({
136 level: message.payload.level,
137 logger: message.payload.logger,
138 data: message.payload.data,
139 metadata: message.payload._meta
140 })
141 case "notifications/resources/updated":
142 return McpCore.ServerNotification.ResourceUpdated({
143 uri: message.payload.uri,
144 metadata: message.payload._meta
145 })
146 case "notifications/resources/list_changed":
147 return McpCore.ServerNotification.ResourcesChanged({ metadata: message.payload?._meta })
148 case "notifications/tools/list_changed":
149 return McpCore.ServerNotification.ToolsChanged({ metadata: message.payload?._meta })
150 case "notifications/prompts/list_changed":
151 return McpCore.ServerNotification.PromptsChanged({ metadata: message.payload?._meta })
152 default:
153 return undefined
154 }
155}
156
157// Request services must come from the invocation, including when a handler is registered during a request.
158const omitRequestServices = Context.omit(
159 McpRequestContext,
160 McpServerClient,
161 HttpServerRequest.HttpServerRequest,
162 CurrentLogLevel
163)
164
165const provideInvocationContext = <A, E, R>(
166 effect: Effect.Effect<A, E, R>,
167 invocation: McpCore.McpInvocation
168): Effect.Effect<A, E, Exclude<R, McpRequestContext>> => {
169 let provided = Effect.provideService(effect, McpRequestContext, invocation.requestContext)
170 const requestMetadata = invocation.requestContext.requestMetadata
171 const logLevel = Predicate.hasProperty(requestMetadata, "io.modelcontextprotocol/logLevel")
172 ? requestMetadata["io.modelcontextprotocol/logLevel"]
173 : undefined
174 if (isLoggingLevel(logLevel)) {
175 provided = Effect.provideService(
176 provided,
177 CurrentLogLevel,
178 McpProtocolInternal.mcpLogLevels[logLevel].effect
179 )
180 }
181 return (invocation.serverClient === undefined
182 ? provided
183 : Effect.provideService(provided, McpServerClient, invocation.serverClient)) as Effect.Effect<
184 A,
185 E,
186 Exclude<R, McpRequestContext>
187 >
188}
189
190/**
191 * Service that stores and serves an MCP server's registered tools, resources,
192 * prompts, completions, and outgoing notifications.
193 *
194 * **Details**
195 *
196 * Handlers use this service to register capabilities and resolve incoming MCP
197 * requests.
198 *
199 * @stability unstable
200 * @category services
201 * @since 4.0.0
202 */
203export class McpServer extends Context.Service<McpServer, {
204 readonly notifications: RpcClient.RpcClient<RpcGroup.Rpcs<typeof BroadcastServerNotificationRpcs>>
205 readonly notifyElicitationComplete: (options: {
206 readonly clientId: number
207 readonly elicitationId: string
208 }) => Effect.Effect<void>
209 readonly tools: ReadonlyArray<{
210 readonly tool: McpTool
211 readonly annotations: Context.Context<never>
212 }>
213 readonly addTool: (options: {
214 readonly tool: McpTool
215 readonly annotations: Context.Context<never>
216 readonly handle: (
217 payload: any
218 ) => Effect.Effect<
219 CallToolResult | McpSchema.InputRequired,
220 InternalError | InvalidParams,
221 McpRequestContext
222 >
223 }) => Effect.Effect<void>
224 readonly callTool: (
225 requests: typeof CallTool.payloadSchema.Type
226 ) => Effect.Effect<CallToolResult, InternalError | InvalidParams, McpServerClient>
227
228 readonly resources: ReadonlyArray<{
229 readonly resource: Resource
230 readonly annotations: Context.Context<never>
231 }>
232 readonly addResource: (options: {
233 readonly resource: Resource
234 readonly annotations: Context.Context<never>
235 readonly handle: Effect.Effect<
236 ReadResourceResult,
237 InternalError,
238 McpRequestContext
239 >
240 }) => Effect.Effect<void>
241
242 readonly resourceTemplates: ReadonlyArray<{
243 readonly template: ResourceTemplate
244 readonly annotations: Context.Context<never>
245 }>
246 readonly addResourceTemplate: (
247 options: {
248 readonly template: ResourceTemplate
249 readonly annotations: Context.Context<never>
250 readonly routerPath: string
251 readonly completions: Record<
252 string,
253 (
254 input: string,
255 context: CompletionContext
256 ) => Effect.Effect<CompleteResult, InternalError>
257 >
258 readonly handle: (
259 uri: string,
260 params: Array<string>
261 ) => Effect.Effect<
262 ReadResourceResult,
263 InvalidParams | InternalError,
264 McpRequestContext
265 >
266 }
267 ) => Effect.Effect<void>
268
269 readonly findResource: (
270 uri: string
271 ) => Effect.Effect<ReadResourceResult, McpErrorBase | InvalidParams | InternalError, McpServerClient>
272
273 readonly prompts: ReadonlyArray<{
274 readonly prompt: Prompt
275 readonly annotations: Context.Context<never>
276 }>
277 readonly addPrompt: (options: {
278 readonly prompt: Prompt
279 readonly annotations: Context.Context<never>
280 readonly completions: Record<
281 string,
282 (
283 input: string,
284 context: CompletionContext
285 ) => Effect.Effect<CompleteResult, InternalError, McpRequestContext>
286 >
287 readonly handle: (
288 params: Record<string, string>
289 ) => Effect.Effect<GetPromptResult | McpSchema.InputRequired, InternalError | InvalidParams, McpRequestContext>
290 }) => Effect.Effect<void>
291 readonly getPromptResult: (
292 request: typeof GetPrompt.payloadSchema.Type
293 ) => Effect.Effect<GetPromptResult, InternalError | InvalidParams, McpServerClient>
294
295 readonly completion: (
296 complete: typeof Complete.payloadSchema.Type
297 ) => Effect.Effect<CompleteResult, InvalidParams | InternalError, McpServerClient>
298}>()("effect/ai/McpServer") {
299 /**
300 * Builds an MCP server service from registered tools, prompts, resources, and completions.
301 *
302 * @stability unstable
303 * @since 4.0.0
304 */
305 static readonly make = Effect.gen(function*() {
306 const internalCore = yield* McpCore.make
307 const tools = Arr.empty<{
308 readonly tool: McpTool
309 readonly annotations: Context.Context<never>
310 }>()
311 const resources: Array<{
312 readonly resource: Resource
313 readonly annotations: Context.Context<never>
314 }> = []
315 const resourceTemplates: Array<{
316 readonly template: ResourceTemplate
317 readonly annotations: Context.Context<never>
318 }> = []
319 const prompts: Array<{
320 readonly prompt: Prompt
321 readonly annotations: Context.Context<never>
322 }> = []
323 const notificationsQueue = yield* Queue.make<QueuedServerNotification>()
324 const notificationDelivery = { consumers: 0 }
325 const pendingListChanges = new Set<string>()
326 const dispatcher = (yield* Scheduler).makeDispatcher()
327 const notifications = yield* RpcClient.makeNoSerialization(BroadcastServerNotificationRpcs, {
328 spanPrefix: "McpServer/Notifications",
329 onFromClient: (options) =>
330 Deferred.make<void>().pipe(Effect.flatMap((delivered) =>
331 Effect.withFiber((fiber): Effect.Effect<void> => {
332 const message = options.message
333 if (message._tag !== "Request") {
334 return Effect.void
335 }
336 const notification = toInternalServerNotification(message)
337 if (notification === undefined) {
338 return Effect.void
339 }
340 const queued = {
341 notification,
342 delivered,
343 requestContext: Context.getOrUndefined(fiber.context, McpRequestContext),
344 requestHeaders: Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest)?.headers
345 }
346 let enqueued = false
347 if (message.tag.includes("list_changed")) {
348 if (!pendingListChanges.has(message.tag)) {
349 enqueued = true
350 const tag = message.tag
351 dispatcher.scheduleTask(() => {
352 Queue.offerUnsafe(notificationsQueue, queued)
353 pendingListChanges.delete(message.tag)
354 }, 0)
355 pendingListChanges.add(tag)
356 }
357 } else {
358 enqueued = true
359 Queue.offerUnsafe(notificationsQueue, queued)
360 }
361 // write resumes the caller synchronously, so defer it until delivery completes.
362 const acknowledge = Effect.suspend(() =>
363 notifications.write({
364 clientId: 0,
365 requestId: message.id,
366 _tag: "Exit",
367 exit: Exit.void
368 })
369 )
370 return enqueued && queued.requestContext !== undefined && notificationDelivery.consumers > 0
371 ? Effect.andThen(Deferred.await(delivered), acknowledge)
372 : acknowledge
373 })
374 ))
375 })
376
377 const service = McpServer.of({
378 notifications: notifications.client,
379 notifyElicitationComplete: ({ clientId, elicitationId }) =>
380 Deferred.make<void>().pipe(
381 Effect.flatMap((delivered) =>
382 Queue.offer(notificationsQueue, {
383 notification: McpCore.ServerNotification.ElicitationComplete({ elicitationId }),
384 targetClientId: clientId,
385 delivered
386 }).pipe(
387 Effect.andThen(
388 Effect.suspend(() => notificationDelivery.consumers > 0 ? Deferred.await(delivered) : Effect.void)
389 )
390 )
391 )
392 ),
393 get tools() {
394 return tools
395 },
396 addTool: (options) =>
397 Effect.gen(function*() {
398 const existingIndex = tools.findIndex(({ tool }) => tool.name === options.tool.name)
399 if (existingIndex === -1) {
400 tools.push(options)
401 } else {
402 tools[existingIndex] = options
403 }
404 const enabledWhen = Context.getOrUndefined(options.annotations, EnabledWhen)
405 yield* internalCore.tools.register({
406 descriptor: new McpTool({
407 ...options.tool,
408 title: options.tool.title ?? options.tool.annotations?.title
409 }),
410 isVisible: (profile) =>
411 enabledWhen === undefined || enabledWhen(
412 {
413 protocolVersion: profile.protocolVersion,
414 capabilities: profile.clientCapabilities,
415 clientInfo: profile.clientInfo
416 }
417 ),
418 handle: (call, invocation) =>
419 provideInvocationContext(options.handle(call.arguments), invocation).pipe(
420 Effect.catchTags({
421 InternalError: (error) =>
422 Effect.fail(
423 new McpCore.ToolExecutionError({
424 name: options.tool.name,
425 message: error.message
426 })
427 ),
428 InvalidParams: (error) =>
429 Effect.fail(
430 new (invocation.requestContext.requestState === undefined &&
431 invocation.requestContext.inputResponses === undefined
432 ? McpCore.InvalidToolInput
433 : McpCore.InvalidToolContinuation)({
434 name: options.tool.name,
435 message: error.message
436 })
437 )
438 }),
439 Effect.map((result) =>
440 Predicate.isTagged(result, "InputRequired")
441 ? McpCore.OperationOutcome.InputRequired(result)
442 : McpCore.OperationOutcome.Complete(result)
443 )
444 )
445 })
446 yield* notifications.client["notifications/tools/list_changed"]({})
447 }),
448 callTool: (request) =>
449 Effect.gen(function*() {
450 const client = yield* McpServerClient
451 const result = yield* internalCore.tools.call(
452 request,
453 McpProtocolInternal.invocationFromClient(client)
454 ).pipe(
455 Effect.mapError((error) =>
456 new InvalidParams({
457 message: error._tag === "ToolNotFound"
458 ? `Tool '${error.name}' not found`
459 : error.message
460 })
461 )
462 )
463 if (result._tag === "InputRequired") {
464 return yield* new InvalidParams({ message: "Client input is not supported by this MCP protocol" })
465 }
466 return result.value
467 }),
468 get resources() {
469 return resources
470 },
471 get resourceTemplates() {
472 return resourceTemplates
473 },
474 addResource: (options) =>
475 Effect.gen(function*() {
476 const existingIndex = resources.findIndex(({ resource }) => resource.uri === options.resource.uri)
477 if (existingIndex === -1) {
478 resources.push(options)
479 } else {
480 resources[existingIndex] = options
481 }
482 yield* internalCore.resources.register({
483 descriptor: options.resource,
484 isVisible: (profile) => {
485 const enabledWhen = Context.getOrUndefined(options.annotations, EnabledWhen)
486 return enabledWhen === undefined || enabledWhen({
487 protocolVersion: profile.protocolVersion,
488 capabilities: profile.clientCapabilities,
489 clientInfo: profile.clientInfo
490 })
491 },
492 read: (invocation) => provideInvocationContext(options.handle, invocation)
493 })
494 yield* notifications.client["notifications/resources/list_changed"]({})
495 }),
496 addResourceTemplate: ({ annotations, completions, handle, routerPath, template }) =>
497 Effect.gen(function*() {
498 const existingIndex = resourceTemplates.findIndex(({ template: current }) =>
499 current.uriTemplate === template.uriTemplate
500 )
501 if (existingIndex === -1) {
502 resourceTemplates.push({ template, annotations })
503 } else {
504 resourceTemplates[existingIndex] = { template, annotations }
505 }
506 const templateMatcher = makeUriMatcher<true>()
507 templateMatcher.add(routerPath, true)
508 yield* internalCore.resources.registerTemplate({
509 descriptor: template,
510 isVisible: (profile) => {
511 const enabledWhen = Context.getOrUndefined(annotations, EnabledWhen)
512 return enabledWhen === undefined || enabledWhen({
513 protocolVersion: profile.protocolVersion,
514 capabilities: profile.clientCapabilities,
515 clientInfo: profile.clientInfo
516 })
517 },
518 match: (uri) => {
519 const match = templateMatcher.find(uri)
520 if (match === undefined) {
521 return undefined
522 }
523 const params: Array<string> = []
524 for (const key of Object.keys(match.params)) {
525 params[Number(key)] = match.params[key]!
526 }
527 return params
528 },
529 read: (uri, params, invocation) => provideInvocationContext(handle(uri, Array.from(params)), invocation)
530 })
531 for (const [param, handle] of Object.entries(completions)) {
532 yield* internalCore.completions.register(
533 `resource/${template.uriTemplate}/${param}`,
534 (request, invocation) =>
535 provideInvocationContext(handle(request.argument.value, request.context), invocation).pipe(
536 Effect.map((result) => ({
537 values: result.completion.values,
538 total: result.completion.total,
539 hasMore: result.completion.hasMore,
540 metadata: result._meta
541 }))
542 )
543 )
544 }
545 yield* notifications.client["notifications/resources/list_changed"]({})
546 }),
547 findResource: (uri) =>
548 Effect.gen(function*() {
549 const client = yield* McpServerClient
550 return yield* internalCore.resources.read(
551 uri,
552 McpProtocolInternal.invocationFromClient(client)
553 ).pipe(
554 Effect.catchTag("ResourceNotFound", (error) =>
555 Effect.fail(new InvalidParams({ message: `Resource '${error.uri}' not found` })))
556 )
557 }),
558 get prompts() {
559 return prompts
560 },
561 addPrompt: (options) =>
562 Effect.gen(function*() {
563 const existingIndex = prompts.findIndex(({ prompt }) => prompt.name === options.prompt.name)
564 if (existingIndex === -1) {
565 prompts.push(options)
566 } else {
567 prompts[existingIndex] = options
568 }
569 yield* internalCore.prompts.register({
570 descriptor: options.prompt,
571 isVisible: (profile) => {
572 const enabledWhen = Context.getOrUndefined(options.annotations, EnabledWhen)
573 return enabledWhen === undefined || enabledWhen({
574 protocolVersion: profile.protocolVersion,
575 capabilities: profile.clientCapabilities,
576 clientInfo: profile.clientInfo
577 })
578 },
579 get: (params, invocation) =>
580 provideInvocationContext(options.handle(params), invocation).pipe(
581 Effect.map((result) =>
582 Predicate.isTagged(result, "InputRequired")
583 ? McpCore.OperationOutcome.InputRequired(result)
584 : McpCore.OperationOutcome.Complete(result)
585 )
586 )
587 })
588 for (const [param, handle] of Object.entries(options.completions)) {
589 yield* internalCore.completions.register(
590 `prompt/${options.prompt.name}/${param}`,
591 (request, invocation) =>
592 provideInvocationContext(handle(request.argument.value, request.context), invocation).pipe(
593 Effect.map((result) => ({
594 values: result.completion.values,
595 total: result.completion.total,
596 hasMore: result.completion.hasMore,
597 metadata: result._meta
598 }))
599 )
600 )
601 }
602 yield* notifications.client["notifications/prompts/list_changed"]({})
603 }),
604 getPromptResult: Effect.fnUntraced(function*({ arguments: params, name }) {
605 const client = yield* McpServerClient
606 const outcome = yield* internalCore.prompts.get(
607 name,
608 params ?? {},
609 McpProtocolInternal.invocationFromClient(client)
610 ).pipe(
611 Effect.catchTag("PromptNotFound", () => new InvalidParams({ message: `Prompt '${name}' not found` }))
612 )
613 return outcome._tag === "Complete"
614 ? outcome.value
615 : yield* new InvalidParams({ message: "Client input is not supported by this MCP protocol" })
616 }),
617 completion: Effect.fnUntraced(function*(complete) {
618 const client = yield* McpServerClient
619 const ref = complete.ref
620 const result = yield* internalCore.completions.complete({
621 reference: ref.type === "ref/resource"
622 ? { type: "resourceTemplate", uriTemplate: ref.uri }
623 : { type: "prompt", name: ref.name },
624 argument: complete.argument,
625 context: complete.context
626 }, McpProtocolInternal.invocationFromClient(client))
627 return {
628 _meta: result.metadata,
629 completion: {
630 values: result.values,
631 total: result.total,
632 hasMore: result.hasMore
633 }
634 }
635 })
636 })
637 internalState.set(service, { core: internalCore, notifications: notificationsQueue, notificationDelivery })
638 return service
639 })
640
641 /**
642 * Layer that provides the MCP server and client services.
643 *
644 * @stability unstable
645 * @since 4.0.0
646 */
647 static readonly layer: Layer.Layer<McpServer | McpServerClient> = Layer.effect(McpServer)(McpServer.make) as any
648}
649
650const MCP_SESSION_ID_HEADER = "mcp-session-id"
651const MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version"
652const MCP_INVALID_BATCH_METHOD = "invalid/json-rpc-batch"
653const cancelledResponses = new WeakMap<object, string | number>()
654const requestKey = (requestId: string | number): string => `${typeof requestId}:${requestId}`
655
656interface ActiveRequest {
657 readonly prepared: McpRuntime.PreparedRequest
658 readonly cancelled: boolean
659}
660
661class McpClientKey extends Data.Class<{
662 readonly clientId: number
663 readonly profile: McpCore.NegotiatedProtocolProfile<string>
664}> {}
665
666/**
667 * Runs an MCP server over the current `RpcServer.Protocol`.
668 *
669 * **Details**
670 *
671 * The server serves registered tools, resources, and prompts. The selected MCP
672 * runtime handles protocol lifecycle state and determines which clients receive
673 * queued server notifications.
674 *
675 * @stability unstable
676 * @category running
677 * @since 4.0.0
678 */
679export const run: (options: {
680 readonly name: string
681 readonly version: string
682 readonly instructions?: string | undefined
683 readonly description?: string | undefined
684 readonly websiteUrl?: string | undefined
685 readonly icons?: ReadonlyArray<McpSchema.Icon> | undefined
686 readonly protocols: Arr.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>
687 readonly extensions?: ServerExtensions | undefined
688}) => Effect.Effect<
689 never,
690 Cause.IllegalArgumentError,
691 McpServer | RpcServer.Protocol
692> = Effect.fnUntraced(function*(options: {
693 readonly name: string
694 readonly version: string
695 readonly instructions?: string | undefined
696 readonly description?: string | undefined
697 readonly websiteUrl?: string | undefined
698 readonly icons?: ReadonlyArray<McpSchema.Icon> | undefined
699 readonly protocols: Arr.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>
700 readonly extensions?: ServerExtensions | undefined
701}) {
702 const runtimeOption = yield* Effect.serviceOption(McpRuntime.ServerRuntime)
703 const runtime = Option.isSome(runtimeOption)
704 ? runtimeOption.value
705 : yield* McpRuntime.make(options.protocols)
706 return yield* runWithRuntime(options, runtime, "custom")
707})
708
709const runWithRuntime = Effect.fnUntraced(function*(
710 options: {
711 readonly name: string
712 readonly version: string
713 readonly instructions?: string | undefined
714 readonly description?: string | undefined
715 readonly websiteUrl?: string | undefined
716 readonly icons?: ReadonlyArray<McpSchema.Icon> | undefined
717 readonly extensions?: ServerExtensions | undefined
718 },
719 runtime: McpRuntime.ServerRuntimeShape,
720 transport: "custom" | "http" | "stdio"
721) {
722 const protocol = yield* RpcServer.Protocol
723 const server = yield* McpServer
724 const defaultLogLevel = yield* CurrentLogLevel
725 const isHttp = transport === "http"
726 const clientStates = new Map<number, {
727 protocol: McpProtocol.AnyProtocolAdapter
728 profile: McpCore.NegotiatedProtocolProfile<string> | undefined
729 sessionHeaders: Headers.Headers | undefined
730 }>()
731 const activeRequests = new Map<number, Map<string, ActiveRequest>>()
732 const reverseRequestClients = new Map<string, Array<McpClientKey>>()
733 const disconnectClient = (clientId: number) => {
734 activeRequests.delete(clientId)
735 clientStates.delete(clientId)
736 runtime.disconnect(clientId)
737 }
738 const sendRequestError = (clientId: number, requestId: string | number, error: unknown) =>
739 protocol.send(clientId, {
740 _tag: "Exit",
741 requestId,
742 exit: { _tag: "Failure", cause: [{ _tag: "Fail", error }] }
743 })
744 const removeReverseRequestClient = (requestId: string, key: McpClientKey) => {
745 const waiting = reverseRequestClients.get(requestId)
746 if (waiting === undefined) return
747 const remaining = waiting.filter((entry) => entry !== key)
748 if (remaining.length === 0) reverseRequestClients.delete(requestId)
749 else reverseRequestClients.set(requestId, remaining)
750 }
751 // A bounded PubSub would let one slow listener block the shared worker and
752 // legacy delivery. Each request scope releases its subscription on exit.
753 const serverNotifications = yield* PubSub.unbounded<McpProtocolInternal.CanonicalServerNotification>()
754 const sendNotification = (
755 protocolVersion: string,
756 clientId: number,
757 notification: McpProtocol.ProjectedNotification
758 ) =>
759 Effect.suspend(() => {
760 const selectedProtocol = runtime.selectProtocol(protocolVersion)
761 const rpc = selectedProtocol.serverNotificationRpcs.requests.get(notification.tag)
762 if (rpc === undefined) {
763 return Effect.die(
764 `MCP protocol ${protocolVersion} does not define server notification ${notification.tag}`
765 )
766 }
767 return selectedProtocol.payloadCodecs(rpc).encode(notification.payload).pipe(
768 Effect.orDie,
769 Effect.flatMap((payload) =>
770 protocol.send(clientId, {
771 _tag: "Request",
772 id: "",
773 tag: notification.tag,
774 payload,
775 headers: [],
776 isNotification: true
777 })
778 )
779 )
780 })
781 const cancelRequest = (clientId: number, requestId: RpcMessage.RequestId) => {
782 const requests = activeRequests.get(clientId)
783 const key = requestKey(requestId)
784 const request = requests?.get(key)
785 if (request === undefined) {
786 return false
787 }
788 requests!.set(key, { ...request, cancelled: true })
789 return true
790 }
791 const terminateSubscription = (
792 protocolVersion: string,
793 clientId: number,
794 requestId: RpcMessage.RequestId,
795 reason: string
796 ): Effect.Effect<void> =>
797 Effect.suspend(() => {
798 if (!cancelRequest(clientId, requestId)) {
799 return Effect.void
800 }
801 return isHttp
802 ? protocol.end(clientId)
803 : sendNotification(protocolVersion, clientId, {
804 tag: "notifications/cancelled",
805 payload: { requestId, reason }
806 })
807 })
808 const handlers = yield* runtime.installHandlers({
809 core: internalState.get(server)!.core,
810 subscribeServerNotifications: PubSub.subscribe(serverNotifications),
811 ...(!protocol.supportsNotifications ? {} : {
812 sendNotification,
813 markSubscriptionCancelled: (clientId: number, requestId: RpcMessage.RequestId) =>
814 Effect.sync(() => {
815 cancelRequest(clientId, requestId)
816 }),
817 terminateSubscription
818 }),
819 defaultLogLevel,
820 serverInfo: options
821 })
822
823 const clients = yield* RcMap.make({
824 lookup: Effect.fnUntraced(function*(key: McpClientKey) {
825 const selectedProtocol = runtime.selectProtocol(key.profile.protocolVersion)
826 let write!: (message: RpcMessage.FromServerEncoded) => Effect.Effect<void>
827 const reverseProtocol = yield* RpcClient.Protocol.make(Effect.fnUntraced(function*(writeResponse) {
828 let cid = 0
829 write = (message) => writeResponse(cid, message)
830 return {
831 send(id, request, _transferables) {
832 cid = id
833 if (request._tag === "Request") {
834 const requestId = requestKey(request.id)
835 const waiting = reverseRequestClients.get(requestId) ?? []
836 waiting.push(key)
837 reverseRequestClients.set(requestId, waiting)
838 return protocol.send(key.clientId, {
839 _tag: "Request",
840 id: request.id,
841 tag: request.tag,
842 payload: request.payload,
843 headers: []
844 }).pipe(Effect.onError(() => Effect.sync(() => removeReverseRequestClient(requestId, key))))
845 }
846 if (request._tag === "Interrupt") {
847 // A cancelled reverse request may never receive a reply.
848 removeReverseRequestClient(requestKey(request.requestId), key)
849 }
850 // Ack & co are not part of FromServerEncoded, but the JSON-RPC
851 // serializer encodes them symmetrically for reverse control flow
852 return protocol.send(key.clientId, request as any)
853 },
854 supportsAck: true,
855 supportsTransferables: false,
856 supportsStructuredClone: false,
857 codecFor: protocol.codecFor
858 }
859 }))
860 const client = yield* selectedProtocol.makeReverseClient(key.profile).pipe(
861 Effect.provideService(RpcClient.Protocol, reverseProtocol)
862 )
863
864 return { client, write } as const
865 }),
866 idleTimeToLive: 10000
867 })
868
869 const clientMiddleware = McpServerClientMiddleware.of((effect, { client, headers, payload, rpc }) => {
870 const session = runtime.resolveRequest(client.id, headers)
871 const isInitialize = rpc._tag.endsWith("/initialize")
872 if (!isInitialize && !session) {
873 const fiber = Fiber.getCurrent()!
874 const httpRequest = Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest)
875 if (httpRequest) {
876 appendPreResponseHandlerUnsafe(
877 httpRequest,
878 () =>
879 Effect.succeed(
880 HttpServerResponse.empty({
881 status: headers[MCP_SESSION_ID_HEADER] === undefined ? 400 : 404
882 })
883 )
884 )
885 }
886 return Effect.die(new Error(`Mcp-Session-Id does not exist`))
887 }
888 // RPC middleware erases the correlation between the initialize tag
889 // and its decoded payload. Restore it once after non-initialize requests
890 // without a session have been rejected above.
891 const initializePayload = session?.initializePayload ?? payload as typeof McpSchema.Initialize.payloadSchema.Type
892 const clientState = clientStates.get(client.id)
893 const selectedProtocol = session?.protocol ??
894 clientState?.protocol ??
895 runtime.protocolForInternalTag(rpc._tag)
896 if (!isProtocolVersion(selectedProtocol.protocolVersion) || selectedProtocol.protocolVersion === "2026-07-28") {
897 return Effect.die(`Unsupported selected MCP protocol version: ${selectedProtocol.protocolVersion}`)
898 }
899 const profile: McpCore.NegotiatedProtocolProfile = session?.negotiatedProfile ?? {
900 protocolVersion: selectedProtocol.protocolVersion,
901 clientCapabilities: initializePayload.capabilities,
902 clientInfo: initializePayload.clientInfo
903 }
904 if (clientState !== undefined) clientState.profile = profile
905 const requestContext = McpRequestContext.of({
906 clientId: client.id,
907 protocolVersion: profile.protocolVersion,
908 clientCapabilities: profile.clientCapabilities,
909 clientInfo: profile.clientInfo,
910 requestMetadata: initializePayload._meta
911 })
912 return Effect.provideService(
913 Effect.provideService(
914 Effect.provideService(
915 effect,
916 McpServerClient,
917 McpServerClient.of({
918 clientId: client.id,
919 protocolVersion: profile.protocolVersion,
920 clientCapabilities: profile.clientCapabilities,
921 clientInfo: profile.clientInfo,
922 initializePayload,
923 requestMetadata: Predicate.isReadonlyObject(payload) && Predicate.isReadonlyObject(payload._meta)
924 ? payload._meta as Schema.JsonObject
925 : undefined,
926 getClient: RcMap.get(
927 clients,
928 new McpClientKey({
929 clientId: client.id,
930 profile
931 })
932 ).pipe(
933 Effect.map(({ client }) => client)
934 )
935 })
936 ),
937 McpRequestContext,
938 requestContext
939 ),
940 CurrentLogLevel,
941 runtime.effectLogLevel(client.id, headers, defaultLogLevel)
942 )
943 })
944
945 const patchedProtocol = RpcServer.Protocol.of({
946 ...protocol,
947 send: (clientId, response) => {
948 if (response._tag === "Exit") {
949 const requests = activeRequests.get(clientId)
950 const key = requestKey(response.requestId)
951 const cancelled = requests?.get(key)?.cancelled
952 if (requests !== undefined && requests.delete(key) && requests.size === 0) {
953 activeRequests.delete(clientId)
954 }
955 if (cancelled === true) {
956 if (transport === "custom") return Effect.void
957 cancelledResponses.set(response, response.requestId)
958 return protocol.send(clientId, response)
959 }
960 if (
961 response.exit._tag === "Failure" &&
962 !response.exit.cause.some((failure) => failure._tag === "Fail")
963 ) {
964 return protocol.send(clientId, {
965 _tag: "Exit",
966 requestId: response.requestId,
967 exit: {
968 _tag: "Failure",
969 cause: [{
970 _tag: "Fail",
971 error: new InternalError({ message: "Internal error" })
972 }]
973 }
974 })
975 }
976 }
977 return protocol.send(clientId, response)
978 },
979 run: (f) =>
980 protocol.run((clientId, request_) => {
981 const fiber = Fiber.getCurrent()!
982 const request = request_ as unknown as
983 | RpcMessage.FromServerEncoded
984 | RpcMessage.FromClientEncoded
985 const httpRequest = isHttp
986 ? Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest)
987 : undefined
988 if (httpRequest !== undefined && request._tag !== "Eof") {
989 appendPreResponseHandlerUnsafe(httpRequest, (_, response) =>
990 Effect.succeed(
991 response.status === 200 &&
992 response.body._tag === "Uint8Array" &&
993 response.body.contentLength === 0
994 ? HttpServerResponse.empty({
995 headers: Headers.remove(response.headers, "content-type"),
996 status: 202
997 })
998 : response
999 ))
1000 }
1001 switch (request._tag) {
1002 case "Request": {
1003 const headers = isHttp
1004 ? Context.getUnsafe(
1005 Fiber.getCurrent()!.context,
1006 HttpServerRequest.HttpServerRequest
1007 ).headers
1008 : Headers.fromInput(request.headers)
1009 const cancellationRequest = request.tag === "notifications/cancelled" &&
1010 typeof request.payload === "object" && request.payload !== null && "requestId" in request.payload &&
1011 (typeof request.payload.requestId === "string" || typeof request.payload.requestId === "number")
1012 ? activeRequests.get(clientId)?.get(requestKey(request.payload.requestId))
1013 : undefined
1014 const prepare = cancellationRequest === undefined
1015 ? runtime.prepareRequest(clientId, headers, request)
1016 : Effect.succeed(cancellationRequest.prepared)
1017 return Effect.gen(function*() {
1018 const prepared = yield* prepare
1019 const clientState = clientStates.get(clientId)
1020 if (isHttp && clientState === undefined) {
1021 // HTTP EOF only ends the request body; handlers and streamed replies may still be running.
1022 yield* Scope.addFinalizer(
1023 Context.getUnsafe(fiber.context, Scope.Scope),
1024 Effect.sync(() => disconnectClient(clientId))
1025 )
1026 }
1027 const session = prepared.binding
1028 const selectedProtocol = prepared.protocol
1029 // Select before dated payload decoding: legacy requests use
1030 // their session's adapter; modern requests carry their own version.
1031 clientStates.set(clientId, {
1032 protocol: selectedProtocol,
1033 profile: prepared.profile ?? clientState?.profile,
1034 sessionHeaders: isHttp && headers[MCP_SESSION_ID_HEADER] !== undefined
1035 ? Headers.fromInput({ [MCP_SESSION_ID_HEADER]: headers[MCP_SESSION_ID_HEADER] })
1036 : clientState?.sessionHeaders
1037 })
1038 if (request.tag === MCP_INVALID_BATCH_METHOD) {
1039 return yield* sendRequestError(
1040 clientId,
1041 request.id,
1042 new InvalidRequest({ message: "JSON-RPC batches are not supported" })
1043 )
1044 }
1045 if (httpRequest !== undefined && session !== undefined) {
1046 appendPreResponseHandlerUnsafe(httpRequest, (_, res) =>
1047 Effect.succeed(
1048 HttpServerResponse.setHeader(
1049 res,
1050 MCP_PROTOCOL_VERSION_HEADER,
1051 session.protocol.protocolVersion
1052 )
1053 ))
1054 }
1055 const routedRequest = runtime.routeClientRequest(selectedProtocol, request)
1056 const rpc = runtime.clientRpcs.requests.get(routedRequest.tag)
1057 const isClientNotification = selectedProtocol.clientNotificationRpcs.requests.has(request.tag)
1058 if (rpc && isClientNotification && request.isNotification) {
1059 if (!session && selectedProtocol.runtime._tag === "Stateful") {
1060 if (httpRequest) {
1061 appendPreResponseHandlerUnsafe(
1062 httpRequest,
1063 () =>
1064 Effect.succeed(
1065 HttpServerResponse.empty({
1066 status: headers[MCP_SESSION_ID_HEADER] === undefined ? 400 : 404
1067 })
1068 )
1069 )
1070 }
1071 return
1072 }
1073 const decode = selectedProtocol.payloadCodecs(rpc).decode(request.payload)
1074 return yield* Effect.gen(function*() {
1075 const payload = yield* decode
1076 if (request.tag === "notifications/cancelled") {
1077 const cancellation = yield* selectedProtocol.normalizeCancellation(payload)
1078 const key = requestKey(cancellation.requestId)
1079 let ownerClientId = clientId
1080 if (isHttp) {
1081 if (session === undefined) {
1082 return
1083 }
1084 // Each HTTP POST has its own transport ID, but cancellation belongs to the session.
1085 const owner = Array.from(activeRequests).find(([, requests]) =>
1086 requests.get(key)?.prepared.binding === session
1087 )
1088 if (owner === undefined) {
1089 return
1090 }
1091 ownerClientId = owner[0]
1092 }
1093 if (!cancelRequest(ownerClientId, cancellation.requestId)) {
1094 return
1095 }
1096 return yield* f(ownerClientId, {
1097 _tag: "Interrupt",
1098 requestId: cancellation.requestId
1099 })
1100 }
1101 const handler = handlers.mapUnsafe.get(rpc.key) as Rpc.Handler<string> | undefined
1102 const handled = handler
1103 ? handler.handler(payload, {
1104 rpc,
1105 requestId: RpcMessage.RequestId(request.id),
1106 client: new Rpc.ServerClient(clientId),
1107 headers
1108 }) as any as Effect.Effect<void>
1109 : Effect.void
1110 return yield* prepared.requestContext === undefined
1111 ? handled
1112 : Effect.provideService(handled, McpRequestContext, prepared.requestContext)
1113 }).pipe(Effect.ignoreCause)
1114 }
1115 if (!rpc || isClientNotification) {
1116 if (request.isNotification) {
1117 return
1118 }
1119 return yield* sendRequestError(
1120 clientId,
1121 request.id,
1122 new MethodNotFound({ message: `Method not found: ${request.tag}` })
1123 )
1124 }
1125 const decoded = yield* Effect.result(selectedProtocol.payloadCodecs(rpc).decode(request.payload))
1126 if (Result.isFailure(decoded)) {
1127 return yield* request.isNotification
1128 ? Effect.void
1129 : sendRequestError(clientId, request.id, new InvalidParams({ message: "Invalid method parameters" }))
1130 }
1131 if (request.isNotification !== true) {
1132 const requests = activeRequests.get(clientId) ?? new Map<string, ActiveRequest>()
1133 requests.set(requestKey(request.id), { prepared, cancelled: false })
1134 activeRequests.set(clientId, requests)
1135 }
1136 const handled = f(clientId, routedRequest)
1137 return yield* prepared.requestContext === undefined
1138 ? handled
1139 : Effect.provideService(handled, McpRequestContext, prepared.requestContext)
1140 }).pipe(
1141 Effect.catch((error) =>
1142 request.isNotification
1143 ? Effect.void
1144 : sendRequestError(
1145 clientId,
1146 request.id,
1147 Predicate.isTagged(error, "ProtocolError")
1148 ? McpProtocolInternal.ProtocolError.fromFeature(error)
1149 : new InvalidParams({ message: "Invalid request metadata" })
1150 )
1151 )
1152 )
1153 }
1154 case "Ping":
1155 case "Ack":
1156 case "Interrupt":
1157 return f(clientId, request)
1158 case "Eof":
1159 if (!isHttp) {
1160 disconnectClient(clientId)
1161 }
1162 return f(clientId, request)
1163 case "Pong":
1164 case "Exit":
1165 case "Chunk":
1166 case "ClientProtocolError":
1167 case "Defect": {
1168 const requestId = request._tag === "Exit"
1169 ? requestKey(request.requestId)
1170 : undefined
1171 const session = isHttp
1172 ? runtime.resolveRequest(
1173 clientId,
1174 Context.getUnsafe(Fiber.getCurrent()!.context, HttpServerRequest.HttpServerRequest).headers
1175 )
1176 : undefined
1177 const waiting = requestId === undefined ? undefined : reverseRequestClients.get(requestId)
1178 const reverseKey = waiting?.find((key) =>
1179 isHttp ? key.profile === session?.negotiatedProfile : key.clientId === clientId
1180 )
1181 if (request._tag === "Exit" && reverseKey === undefined) {
1182 return Effect.void
1183 }
1184 if (reverseKey !== undefined && requestId !== undefined) {
1185 removeReverseRequestClient(requestId, reverseKey)
1186 }
1187 const targetClientId = reverseKey?.clientId ?? clientId
1188 const clientState = clientStates.get(targetClientId)
1189 const selectedProtocol = clientState?.protocol ?? runtime.protocols[0]
1190 const profile = reverseKey?.profile ?? clientState?.profile ?? {
1191 protocolVersion: selectedProtocol.protocolVersion,
1192 clientCapabilities: {},
1193 clientInfo: { name: "unknown", version: "unknown" }
1194 }
1195 return RcMap.get(
1196 clients,
1197 new McpClientKey({
1198 clientId: targetClientId,
1199 profile
1200 })
1201 ).pipe(
1202 Effect.flatMap(({ write }) => write(request)),
1203 Effect.scoped
1204 )
1205 }
1206 }
1207 })
1208 })
1209
1210 const { notificationDelivery, notifications } = internalState.get(server)!
1211 yield* Effect.acquireRelease(
1212 Effect.sync(() => {
1213 notificationDelivery.consumers++
1214 }),
1215 () =>
1216 Effect.sync(() => {
1217 notificationDelivery.consumers--
1218 })
1219 )
1220 const notificationTails = new Map<number, Deferred.Deferred<void>>()
1221 yield* Queue.take(notifications).pipe(
1222 Effect.flatMap(Effect.fnUntraced(function*(queued) {
1223 const { delivered, notification, targetClientId, requestContext, requestHeaders } = queued
1224 if (McpProtocolInternal.isSubscriptionServerNotification(notification)) {
1225 yield* PubSub.publish(serverNotifications, { notification, targetClientId })
1226 }
1227 const clientIds = yield* patchedProtocol.clientIds
1228 for (const clientId of clientStates.keys()) {
1229 if (!clientIds.has(clientId)) {
1230 clientStates.delete(clientId)
1231 // HTTP UUID sessions are stored separately and outlive request-scoped client IDs.
1232 runtime.disconnect(clientId)
1233 }
1234 }
1235 const requestNotification = notification._tag === "LoggingMessage" || notification._tag === "Progress"
1236 const deliveryClientIds = requestNotification && requestContext !== undefined
1237 ? [requestContext.clientId]
1238 : targetClientId === undefined
1239 ? isHttp ? clientStates.keys() : runtime.deliveryClientIds()
1240 : [targetClientId]
1241 const deliveries: Array<Effect.Effect<void>> = []
1242 let hasOriginDelivery = false
1243 for (const clientId of deliveryClientIds) {
1244 if (targetClientId !== undefined && clientId !== targetClientId) {
1245 continue
1246 }
1247 if (!clientIds.has(clientId)) {
1248 runtime.disconnect(clientId)
1249 continue
1250 }
1251 // This must stay below stale-client cleanup so transports without
1252 // notification support still prune initializedClients.
1253 if (!patchedProtocol.supportsNotifications) {
1254 continue
1255 }
1256 const selectedProtocol = requestNotification && requestContext !== undefined
1257 ? runtime.selectProtocol(requestContext.protocolVersion)
1258 : clientStates.get(clientId)?.protocol
1259 if (!selectedProtocol) {
1260 continue
1261 }
1262 // Reserve delivery order before forking so one blocked client cannot hold up another.
1263 const previous = notificationTails.get(clientId)
1264 const isOrigin = clientId === (targetClientId ?? requestContext?.clientId)
1265 hasOriginDelivery ||= isOrigin
1266 // Await delivery to the explicit target or originating request, not unrelated broadcast recipients.
1267 const completed = isOrigin ? delivered : Deferred.makeUnsafe<void>()
1268 notificationTails.set(clientId, completed)
1269 const delivery = Effect.gen(function*() {
1270 if (previous !== undefined) yield* Deferred.await(previous)
1271 const projected = yield* selectedProtocol.projectNotification(notification)
1272 if (projected === undefined) {
1273 return
1274 }
1275 if (
1276 notification._tag === "Progress" && selectedProtocol.runtime._tag === "Stateless" &&
1277 requestContext?.requestMetadata?.progressToken !== notification.progressToken
1278 ) {
1279 return
1280 }
1281 if (notification._tag === "LoggingMessage" && selectedProtocol.runtime._tag === "Stateless") {
1282 const metadata = requestContext?.requestMetadata
1283 const level = Predicate.hasProperty(metadata, "io.modelcontextprotocol/logLevel")
1284 ? metadata["io.modelcontextprotocol/logLevel"]
1285 : undefined
1286
1287 if (
1288 requestContext?.clientId !== clientId ||
1289 !isLoggingLevel(level) ||
1290 McpSchema.LoggingLevel.literals.indexOf(notification.level) <
1291 McpSchema.LoggingLevel.literals.indexOf(level)
1292 ) {
1293 return
1294 }
1295 } else if (
1296 !runtime.canDeliver(
1297 clientId,
1298 (requestContext?.clientId === clientId ? requestHeaders : undefined) ??
1299 clientStates.get(clientId)?.sessionHeaders ?? Headers.empty,
1300 notification,
1301 defaultLogLevel
1302 )
1303 ) {
1304 return
1305 }
1306 const rpc = selectedProtocol.serverNotificationRpcs.requests.get(projected.tag)
1307 if (!rpc) {
1308 return
1309 }
1310 const encoded = yield* selectedProtocol.payloadCodecs(rpc).encode(projected.payload)
1311 yield* patchedProtocol.send(clientId, {
1312 _tag: "Request",
1313 id: "",
1314 tag: projected.tag,
1315 payload: encoded,
1316 headers: [],
1317 isNotification: true
1318 })
1319 }).pipe(
1320 Effect.ignoreCause,
1321 Effect.ensuring(Effect.sync(() => {
1322 Deferred.doneUnsafe(completed, Exit.void)
1323 if (notificationTails.get(clientId) === completed) notificationTails.delete(clientId)
1324 }))
1325 )
1326 deliveries.push(delivery)
1327 }
1328 if (!hasOriginDelivery) yield* Deferred.succeed(delivered, undefined)
1329 yield* Effect.all(deliveries, { concurrency: "unbounded", discard: true }).pipe(
1330 Effect.forkScoped({ startImmediately: true })
1331 )
1332 })),
1333 Effect.ignoreCause,
1334 Effect.forever,
1335 Effect.forkScoped
1336 )
1337
1338 return yield* RpcServer.make(runtime.clientRpcs, {
1339 spanPrefix: "McpServer",
1340 disableFatalDefects: true
1341 }).pipe(
1342 Effect.provideService(RpcServer.Protocol, patchedProtocol),
1343 Effect.provideService(McpServerClientMiddleware, clientMiddleware),
1344 Effect.provide(handlers)
1345 )
1346}, Effect.scoped)
1347
1348/**
1349 * Creates a layer that starts an MCP server over an existing
1350 * `RpcServer.Protocol` and provides `McpServer`. Request handlers receive
1351 * `McpRequestContext`; initialized legacy requests additionally receive
1352 * `McpServerClient`.
1353 *
1354 * **When to use**
1355 *
1356 * Use when you already have a custom or externally provided
1357 * `RpcServer.Protocol` and want to start an MCP server as part of a layer
1358 * graph.
1359 *
1360 * **Details**
1361 *
1362 * The returned layer forks `run(options)` in the layer scope and merges
1363 * `McpServer.layer`, so registration layers can use the `McpServer` service
1364 * while the server is running.
1365 *
1366 * **Gotchas**
1367 *
1368 * Unlike `layerStdio` and `layerHttp`, this layer does not install a concrete
1369 * transport. The surrounding layer graph must provide `RpcServer.Protocol`.
1370 *
1371 * @see {@link run} for the effect form used by this layer
1372 * @see {@link layerStdio} for a stdio-backed layer that installs the MCP protocol and NDJSON-RPC serialization
1373 * @see {@link layerHttp} for an HTTP-backed layer that registers with `HttpRouter` and installs JSON-RPC serialization
1374 *
1375 * @stability unstable
1376 * @category layers
1377 * @since 4.0.0
1378 */
1379export const layer = (options: {
1380 readonly name: string
1381 readonly version: string
1382 readonly instructions?: string | undefined
1383 readonly description?: string | undefined
1384 readonly websiteUrl?: string | undefined
1385 readonly icons?: ReadonlyArray<McpSchema.Icon> | undefined
1386 readonly protocols: Arr.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>
1387 readonly extensions?: ServerExtensions | undefined
1388}): Layer.Layer<McpServer | McpServerClient, Cause.IllegalArgumentError, RpcServer.Protocol> =>
1389 layerWithRuntime(options, "custom").pipe(
1390 Layer.provide(McpRuntime.layer(options.protocols))
1391 )
1392
1393const layerWithRuntime = (options: {
1394 readonly name: string
1395 readonly version: string
1396 readonly instructions?: string | undefined
1397 readonly description?: string | undefined
1398 readonly websiteUrl?: string | undefined
1399 readonly icons?: ReadonlyArray<McpSchema.Icon> | undefined
1400 readonly extensions?: ServerExtensions | undefined
1401}, transport: "custom" | "http" | "stdio"): Layer.Layer<
1402 McpServer | McpServerClient,
1403 never,
1404 RpcServer.Protocol | McpRuntime.ServerRuntime
1405> =>
1406 Layer.effectDiscard(
1407 Effect.gen(function*() {
1408 const runtime = yield* McpRuntime.ServerRuntime
1409 yield* Effect.forkScoped(runWithRuntime(options, runtime, transport))
1410 })
1411 ).pipe(
1412 Layer.provideMerge(McpServer.layer)
1413 )
1414
1415/**
1416 * Creates a layer that runs an MCP server over standard input and output.
1417 *
1418 * **When to use**
1419 *
1420 * Use when an MCP client launches the server as a subprocess and communicates
1421 * through newline-delimited JSON-RPC messages.
1422 *
1423 * **Details**
1424 *
1425 * The selected protocol adapter controls the dated RPC schemas and JSON-RPC
1426 * batch policy. The layer provides `McpServer`, supplies request-scoped
1427 * `McpRequestContext` and legacy `McpServerClient` services to handlers, and
1428 * requires `Stdio`.
1429 *
1430 * @see {@link layer} for running over an existing `RpcServer.Protocol`
1431 * @see {@link layerHttp} for the single-endpoint HTTP transport
1432 *
1433 * @stability unstable
1434 * @category layers
1435 * @since 4.0.0
1436 */
1437export const layerStdio = (options: {
1438 readonly name: string
1439 readonly version: string
1440 readonly instructions?: string | undefined
1441 readonly description?: string | undefined
1442 readonly websiteUrl?: string | undefined
1443 readonly icons?: ReadonlyArray<McpSchema.Icon> | undefined
1444 readonly protocols: Arr.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>
1445 readonly extensions?: ServerExtensions | undefined
1446}): Layer.Layer<McpServer | McpServerClient, Cause.IllegalArgumentError, Stdio> =>
1447 layerWithRuntime(options, "stdio").pipe(
1448 Layer.provide(McpRuntime.layer(options.protocols)),
1449 Layer.provide(RpcServer.layerProtocolStdio),
1450 Layer.provide(Layer.succeed(
1451 RpcSerialization.RpcSerialization,
1452 mcpStdioSerialization(options.protocols)
1453 ))
1454 )
1455
1456const mcpStdioSerialization = (
1457 protocols: Arr.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>
1458): RpcSerialization.RpcSerialization["Service"] => {
1459 const serialization = mcpJsonRpcSerialization({
1460 contentType: "application/json-rpc"
1461 })
1462 return RpcSerialization.RpcSerialization.of({
1463 contentType: serialization.contentType,
1464 includesFraming: true,
1465 codecFor: serialization.codecFor,
1466 makeUnsafe: () => {
1467 const frames = RpcSerialization.ndjson.makeUnsafe()
1468 const parser = serialization.makeUnsafe()
1469 let selectedProtocol: McpProtocol.ProtocolAdapter | undefined
1470 return {
1471 decode: (data) => {
1472 const decoded: Array<unknown> = []
1473 for (const frame of frames.decode(data)) {
1474 if (Array.isArray(frame)) {
1475 const acceptsBatch = selectedProtocol?.runtime.transport.jsonRpc.acceptsBatches === true
1476 if (
1477 !acceptsBatch ||
1478 frame.length === 0 ||
1479 frame.some(McpRuntime.hasRequestProtocolVersion) ||
1480 frame.some(isInitializeJsonRpcMessage)
1481 ) {
1482 decoded.push({
1483 _tag: "Request",
1484 id: null,
1485 tag: MCP_INVALID_BATCH_METHOD,
1486 payload: null,
1487 headers: []
1488 })
1489 continue
1490 }
1491 } else if (isInitializeJsonRpcMessage(frame)) {
1492 const offered = getJsonRpcProtocolVersion(frame)
1493 selectedProtocol = McpRuntime.selectStatefulProtocol(protocols, offered)
1494 }
1495 decoded.push(...parser.decode(JSON.stringify(frame)))
1496 }
1497 return decoded
1498 },
1499 encode: (response) => {
1500 const invalidBatchExit = decodeInvalidBatchExit(response)
1501 if (Result.isSuccess(invalidBatchExit)) {
1502 return JSON.stringify({
1503 jsonrpc: "2.0",
1504 id: null,
1505 error: {
1506 code: McpSchema.INVALID_REQUEST_ERROR_CODE,
1507 message: "JSON-RPC batches are not supported"
1508 }
1509 }) + "\n"
1510 }
1511 const encoded = parser.encode(response)
1512 return encoded === undefined ? undefined : `${encoded}\n`
1513 }
1514 }
1515 }
1516 })
1517}
1518
1519/**
1520 * Registers a Streamable HTTP MCP endpoint at `options.path`.
1521 *
1522 * **When to use**
1523 *
1524 * Use to expose an MCP server through an existing `HttpRouter`.
1525 *
1526 * **Details**
1527 *
1528 * POST serves JSON-RPC and accepted notification-only requests return `202`.
1529 * Modern routing-header mismatches and unsupported protocol versions return
1530 * JSON-RPC errors with status `400`; unknown modern RPC methods return a
1531 * JSON-RPC method-not-found error with status `404`. Unsupported HTTP methods
1532 * return `405`. Requests carrying an `Origin` header are rejected unless the
1533 * exact origin appears in `allowedOrigins`; Origin-less non-browser clients
1534 * remain valid. The surrounding HTTP server remains responsible for binding
1535 * to an appropriate interface and installing authentication.
1536 *
1537 * `layerHttp` always implements the single-endpoint Streamable HTTP topology.
1538 * Using `v2024_11_05` here is a custom compatibility transport for that
1539 * revision's schema. It does not implement the historical two-endpoint
1540 * HTTP+SSE transport, GET SSE, event resumption, session expiry, or client
1541 * session termination.
1542 *
1543 * @see {@link layerStdio} for exposing the server over stdio
1544 * @see {@link layer} for the base MCP server layer without a transport protocol
1545 *
1546 * @stability unstable
1547 * @category layers
1548 * @since 4.0.0
1549 */
1550export const layerHttp = (options: {
1551 readonly name: string
1552 readonly version: string
1553 readonly instructions?: string | undefined
1554 readonly description?: string | undefined
1555 readonly websiteUrl?: string | undefined
1556 readonly icons?: ReadonlyArray<McpSchema.Icon> | undefined
1557 readonly path: HttpRouter.PathInput
1558 readonly protocols: Arr.NonEmptyReadonlyArray<McpProtocol.ProtocolAdapter>
1559 readonly extensions?: ServerExtensions | undefined
1560 readonly allowedOrigins?: ReadonlyArray<string> | undefined
1561}): Layer.Layer<McpServer | McpServerClient, Cause.IllegalArgumentError, HttpRouter.HttpRouter> => {
1562 const runtime = McpRuntime.layer(options.protocols)
1563 const methodNotAllowedResponse = HttpServerResponse.empty({
1564 status: 405,
1565 headers: { allow: "POST" }
1566 })
1567 const methodNotAllowed = (request: HttpServerRequest.HttpServerRequest) =>
1568 isAllowedMcpOrigin(request, options.allowedOrigins)
1569 ? Effect.succeed(methodNotAllowedResponse)
1570 : Effect.succeed(HttpServerResponse.empty({ status: 403 }))
1571 const routes = Layer.mergeAll(
1572 HttpRouter.add("GET", options.path, methodNotAllowed),
1573 HttpRouter.add("PUT", options.path, methodNotAllowed),
1574 HttpRouter.add("PATCH", options.path, methodNotAllowed),
1575 HttpRouter.add("DELETE", options.path, methodNotAllowed),
1576 HttpRouter.add("OPTIONS", options.path, methodNotAllowed)
1577 )
1578 return Layer.merge(layerWithRuntime(options, "http"), routes).pipe(
1579 Layer.provide(layerMcpProtocolHttp(options)),
1580 Layer.provide(runtime),
1581 Layer.provide(RpcSerialization.layerJsonRpc())
1582 )
1583}
1584
1585const layerMcpProtocolHttp = (options: {
1586 readonly path: HttpRouter.PathInput
1587 readonly allowedOrigins?: ReadonlyArray<string> | undefined
1588}): Layer.Layer<
1589 RpcServer.Protocol,
1590 never,
1591 McpRuntime.ServerRuntime | HttpRouter.HttpRouter
1592> =>
1593 Layer.effect(RpcServer.Protocol)(Effect.gen(function*() {
1594 const runtime = yield* McpRuntime.ServerRuntime
1595 const { httpEffect, protocol } = yield* RpcServer.makeProtocolWithHttpEffect().pipe(
1596 Effect.provideService(RpcSerialization.RpcSerialization, mcpHttpSerialization)
1597 )
1598 const router = yield* HttpRouter.HttpRouter
1599 yield* router.add("POST", options.path, (request) => {
1600 if (!isAllowedMcpOrigin(request, options.allowedOrigins)) {
1601 return Effect.succeed(HttpServerResponse.empty({ status: 403 }))
1602 }
1603 if (mcpMediaTypes(request.headers["content-type"])[0] !== "application/json") {
1604 return Effect.succeed(HttpServerResponse.empty({ status: 415 }))
1605 }
1606 const accepted = mcpMediaTypes(request.headers["accept"])
1607 if (!accepted.includes("application/json") || !accepted.includes("text/event-stream")) {
1608 return Effect.succeed(HttpServerResponse.empty({ status: 406 }))
1609 }
1610 return Effect.gen(function*() {
1611 const parsed = yield* Effect.result(
1612 Effect.flatMap(request.text, Schema.decodeEffect(Schema.UnknownFromJsonString))
1613 )
1614 const admission = runtime.admitHttp(request.headers, parsed)
1615 if (admission._tag === "Rejected") {
1616 return admission.response
1617 }
1618 const response = Effect.map(httpEffect, (response) => {
1619 // Completed responses may already contain notifications followed by the result.
1620 const hasMultipleMessages = response.body._tag === "Uint8Array" &&
1621 response.body.body.subarray(0, -1).includes(10)
1622 return admission.isSubscription || response.body._tag === "Stream" || hasMultipleMessages
1623 ? toServerSentEvents(response, admission.isSubscription)
1624 : response
1625 })
1626 return yield* admission.acknowledge
1627 ? Effect.catchCause(response, () => Effect.succeed(HttpServerResponse.empty({ status: 202 })))
1628 : response
1629 })
1630 })
1631 return protocol
1632 }))
1633
1634const mcpHttpSerialization: RpcSerialization.RpcSerialization["Service"] = (() => {
1635 const serialization = mcpJsonRpcSerialization()
1636 return RpcSerialization.RpcSerialization.of({
1637 contentType: serialization.contentType,
1638 includesFraming: true,
1639 codecFor: serialization.codecFor,
1640 makeUnsafe: () => {
1641 const parser = serialization.makeUnsafe()
1642 return {
1643 decode: parser.decode,
1644 encode: (response) => {
1645 const encoded = parser.encode(response)
1646 // Preserve message boundaries when the HTTP transport joins buffered chunks.
1647 return typeof encoded === "string" ? `${encoded}\n` : encoded
1648 }
1649 }
1650 }
1651 })
1652})()
1653
1654const isEncodedMcpFailureCause = Schema.is(Schema.TaggedStruct("Cause", {
1655 data: Schema.Tuple([Schema.TaggedStruct("Fail", {
1656 error: Schema.toEncoded(McpSchema.McpError)
1657 })])
1658}))
1659
1660const normalizeMcpJsonRpcResponse = (response: unknown): unknown => {
1661 if (Array.isArray(response)) {
1662 return response.map(normalizeMcpJsonRpcResponse)
1663 }
1664 if (!Predicate.isReadonlyObject(response) || !Predicate.hasProperty(response, "error")) {
1665 return response
1666 }
1667 return isEncodedMcpFailureCause(response.error)
1668 ? { ...response, error: response.error.data[0].error }
1669 : response
1670}
1671
1672function mcpJsonRpcSerialization(options?: {
1673 readonly contentType?: string | undefined
1674}): RpcSerialization.RpcSerialization["Service"] {
1675 const serialization = RpcSerialization.jsonRpc(options)
1676 return RpcSerialization.RpcSerialization.of({
1677 ...serialization,
1678 makeUnsafe: () => {
1679 const parser = serialization.makeUnsafe()
1680 const cancelledIds = new Set<string | number>()
1681 return {
1682 decode: parser.decode,
1683 encode: (response) => {
1684 if (Predicate.isReadonlyObject(response)) {
1685 const cancelledId = cancelledResponses.get(response)
1686 if (cancelledId !== undefined) {
1687 cancelledResponses.delete(response)
1688 cancelledIds.add(cancelledId)
1689 }
1690 }
1691 const encoded = parser.encode(response)
1692 if (typeof encoded !== "string") return encoded
1693 if (cancelledIds.size === 0 && !encoded.includes("\"_tag\":\"Cause\"")) return encoded
1694 let message: unknown = JSON.parse(encoded)
1695 if (cancelledIds.size > 0) {
1696 // Count cancelled completions toward the batch, but never write their responses.
1697 // https://modelcontextprotocol.io/specification/2025-03-26/basic/utilities/cancellation
1698 const keep = (message: unknown) =>
1699 !(Predicate.isReadonlyObject(message) && !Predicate.hasProperty(message, "method") &&
1700 (typeof message.id === "string" || typeof message.id === "number") && cancelledIds.delete(message.id))
1701 if (Array.isArray(message)) {
1702 const remaining = message.filter(keep)
1703 if (remaining.length === 0) return undefined
1704 message = remaining
1705 } else if (!keep(message)) {
1706 return undefined
1707 }
1708 }
1709 return JSON.stringify(normalizeMcpJsonRpcResponse(message))
1710 }
1711 }
1712 }
1713 })
1714}
1715
1716const toServerSentEvents = (response: HttpServerResponse.HttpServerResponse, keepAlive: boolean) => {
1717 const encoder = new TextEncoder()
1718 const decoder = new TextDecoder()
1719 const frame = (data: Uint8Array) =>
1720 encoder.encode(decoder.decode(data).trimEnd().split("\n").map((message) => `data: ${message}\n\n`).join(""))
1721 const options = {
1722 status: response.status,
1723 statusText: response.statusText,
1724 headers: Headers.remove(response.headers, "content-type"),
1725 cookies: response.cookies,
1726 contentType: "text/event-stream"
1727 }
1728 if (response.body._tag === "Stream") {
1729 const events = response.body.stream.pipe(Stream.map(frame))
1730 if (!keepAlive) {
1731 return HttpServerResponse.stream(events, options)
1732 }
1733 // Keep a native timer pending so workerd does not treat idle subscriptions as hung requests.
1734 const keepAliveComments = Stream.tick("15 seconds").pipe(
1735 Stream.drop(1),
1736 Stream.map(() => encoder.encode(": keepalive\n\n"))
1737 )
1738 return HttpServerResponse.stream(Stream.merge(events, keepAliveComments, { haltStrategy: "left" }), options)
1739 }
1740 if (response.body._tag === "Uint8Array") {
1741 return HttpServerResponse.uint8Array(frame(response.body.body), options)
1742 }
1743 return HttpServerResponse.setHeader(response, "content-type", "text/event-stream")
1744}
1745
1746const isAllowedMcpOrigin = (
1747 request: HttpServerRequest.HttpServerRequest,
1748 allowedOrigins: ReadonlyArray<string> | undefined
1749): boolean => {
1750 const origin = request.headers["origin"]
1751 return origin === undefined || (allowedOrigins ?? []).includes(origin)
1752}
1753
1754const mcpMediaTypes = (header: string | undefined): ReadonlyArray<string> =>
1755 header === undefined
1756 ? []
1757 : header.split(",").flatMap((part) => {
1758 const [mediaType, ...parameters] = part.split(";")
1759 const quality = parameters
1760 .map((parameter) => parameter.trim().toLowerCase())
1761 .find((parameter) => parameter.startsWith("q="))
1762 if (quality !== undefined) {
1763 const value = Number(quality.slice(2))
1764 if (!Number.isFinite(value) || value <= 0 || value > 1) {
1765 return []
1766 }
1767 }
1768 return [mediaType.trim().toLowerCase()]
1769 })
1770
1771const InitializeJsonRpcMessage = Schema.Struct({
1772 method: Schema.Literal("initialize")
1773})
1774
1775const isInitializeJsonRpcMessage = (message: unknown): boolean =>
1776 Result.isSuccess(Schema.decodeUnknownResult(InitializeJsonRpcMessage)(message))
1777
1778const JsonRpcProtocolVersion = Schema.Struct({
1779 params: Schema.Struct({
1780 protocolVersion: Schema.String
1781 })
1782})
1783
1784const getJsonRpcProtocolVersion = (message: unknown): string | undefined => {
1785 const decoded = Schema.decodeUnknownResult(JsonRpcProtocolVersion)(message)
1786 return Result.isSuccess(decoded) ? decoded.success.params.protocolVersion : undefined
1787}
1788
1789const INTERNAL_TOOL_ERROR_MESSAGE = "Tool execution failed due to an internal server error."
1790
1791const toolErrorResult = (message: string): CallToolResult =>
1792 new CallToolResult({
1793 isError: true,
1794 content: [{ type: "text", text: message }]
1795 })
1796
1797const toolResultContent = (encoded: unknown): CallToolResult["content"] =>
1798 encoded === undefined ? [] : [{ type: "text", text: JSON.stringify(encoded) }]
1799
1800/**
1801 * Registers a `Toolkit` with the `McpServer`.
1802 *
1803 * @stability unstable
1804 * @category handlers
1805 * @since 4.0.0
1806 */
1807export const registerToolkit: <Tools extends Record<string, Tool.Any>>(
1808 toolkit: Toolkit.Toolkit<Tools>
1809) => Effect.Effect<
1810 void,
1811 never,
1812 McpServer | Tool.HandlersFor<Tools> | Exclude<Tool.HandlerServices<Tools>, McpRequestContext>
1813> = Effect.fnUntraced(function*<Tools extends Record<string, Tool.Any>>(
1814 toolkit: Toolkit.Toolkit<Tools>
1815) {
1816 const registry = yield* McpServer
1817 const built = yield* (toolkit as any as Effect.Effect<
1818 Toolkit.WithHandler<Tools>,
1819 never,
1820 Exclude<Tool.HandlersFor<Tools>, McpRequestContext>
1821 >).pipe(Effect.updateContext((context: Context.Context<Exclude<Tool.HandlersFor<Tools>, McpRequestContext>>) => {
1822 // Toolkit handlers also retain the context in which their layer was built.
1823 const services = new Map(context.mapUnsafe)
1824 for (const tool of Object.values(toolkit.tools)) {
1825 const handler = services.get(tool.id) as Tool.Handler<string> | undefined
1826 if (handler !== undefined) {
1827 services.set(tool.id, { ...handler, context: omitRequestServices(handler.context) })
1828 }
1829 }
1830 return Context.makeUnsafe(services)
1831 }))
1832 const services = omitRequestServices(yield* Effect.context<never>())
1833 const reportCause = (cause: Cause.Cause<unknown>) => Effect.provideContext(ErrorReporter.report(cause), services)
1834 // Interruption propagates; anything else is logged, reported and scrubbed.
1835 const internalToolError = (cause: Cause.Cause<unknown>) => {
1836 const failure = Cause.findFail(cause)
1837 return Result.isFailure(failure) && !Cause.hasDies(cause)
1838 ? Effect.failCause(failure.failure)
1839 : Effect.logError(cause).pipe(
1840 Effect.andThen(reportCause(cause)),
1841 Effect.as(toolErrorResult(INTERNAL_TOOL_ERROR_MESSAGE))
1842 )
1843 }
1844 const registrations: Array<Parameters<typeof registry.addTool>[0]> = []
1845 for (const tool of Object.values(built.tools)) {
1846 const strict = Tool.getStrictMode(tool) === true
1847 const rawJsonSchema = Tool.isDynamic(tool) ? tool.jsonSchema : undefined
1848 if (strict && rawJsonSchema !== undefined) {
1849 return yield* Effect.die(
1850 `McpServer cannot strictly validate the raw JSON Schema for tool '${tool.name}'; use an Effect Schema instead`
1851 )
1852 }
1853 const decodeOptions: SchemaAST.ParseOptions = {
1854 onExcessProperty: strict ? "error" : "ignore",
1855 errors: "all"
1856 }
1857 const annotations = tool.annotations
1858 const toolMeta = Context.getOrUndefined(annotations, Tool.Meta)
1859 const isDeclaredFailure = Schema.is(tool.failureSchema)
1860 const encodeFailure = Schema.encodeUnknownEffect(tool.failureSchema) as (
1861 error: unknown
1862 ) => Effect.Effect<unknown, Schema.SchemaError, Tool.HandlerServices<Tools[keyof Tools]>>
1863 const declaredFailureResult = (error: unknown) =>
1864 error instanceof Error && error.message !== ""
1865 ? Effect.succeed(toolErrorResult(error.message))
1866 : Effect.map(encodeFailure(error), (encoded) =>
1867 new CallToolResult({ isError: true, content: toolResultContent(encoded) }))
1868 const handleCause = (cause: Cause.Cause<unknown>) => {
1869 const failure = Cause.findFail(cause)
1870 if (Result.isSuccess(failure)) {
1871 const error = failure.success.error
1872 const origin = Context.get(Cause.reasonAnnotations(failure.success), Toolkit.FailureOrigin)
1873 if (origin === "parameters" && isParameterValidationError(error)) {
1874 return Effect.fail(new InvalidParams({ message: error.reason.message }))
1875 }
1876 if (origin === "handler" && isDeclaredFailure(error)) {
1877 return Effect.catchCause(declaredFailureResult(error), internalToolError)
1878 }
1879 }
1880 return internalToolError(cause)
1881 }
1882 const outputSchema = yield* Schema.decodeUnknownEffect(McpSchema.ToolOutputJson)(
1883 toolJsonSchema(tool.successSchema, false)
1884 ).pipe(Effect.orDie)
1885 const inputSchema = yield* Schema.decodeUnknownEffect(ToolJson)(
1886 rawJsonSchema ?? toolJsonSchema(tool.parametersSchema, strict)
1887 ).pipe(
1888 Effect.catchTag("SchemaError", (error) =>
1889 Effect.die(
1890 rawJsonSchema !== undefined
1891 ? `McpServer cannot register tool '${tool.name}': its raw JSON Schema must have an object root ` +
1892 `(type: "object").\n\n${error.message}`
1893 : `McpServer cannot register tool '${tool.name}': its parameters must encode to a JSON Schema with an ` +
1894 `object root (type: "object"), such as a Schema.Struct. Use Tool.EmptyParams for a tool without ` +
1895 `parameters.\n\n${error.message}`
1896 ))
1897 )
1898 const mcpTool = new McpTool({
1899 name: tool.name,
1900 description: Tool.getDescription(tool),
1901 inputSchema,
1902 outputSchema,
1903 annotations: {
1904 ...(Context.getOption(tool.annotations, Tool.Title).pipe(
1905 Option.map((title) => ({ title })),
1906 Option.getOrUndefined
1907 )),
1908 readOnlyHint: Context.get(tool.annotations, Tool.Readonly),
1909 destructiveHint: Context.get(tool.annotations, Tool.Destructive),
1910 idempotentHint: Context.get(tool.annotations, Tool.Idempotent),
1911 openWorldHint: Context.get(tool.annotations, Tool.OpenWorld)
1912 },
1913 _meta: toolMeta
1914 })
1915 registrations.push({
1916 tool: mcpTool,
1917 annotations,
1918 handle(payload) {
1919 return built.handle(tool.name as keyof Tools, payload ?? {}, undefined, decodeOptions).pipe(
1920 Stream.unwrap,
1921 Stream.runLast,
1922 Effect.flatMap(Effect.fromOption),
1923 Effect.flatMap((result) =>
1924 // Declared failures return their encoded payload; anything else is classified by origin.
1925 result.isFailure && result.failureOrigin !== "handler"
1926 ? Effect.failCause(Cause.annotate(
1927 Cause.fail(result.result),
1928 Context.make(Toolkit.FailureOrigin, result.failureOrigin ?? "result")
1929 ))
1930 : Effect.succeed(
1931 new CallToolResult({
1932 isError: result.isFailure,
1933 structuredContent: result.isFailure ? undefined : result.encodedResult,
1934 content: toolResultContent(result.encodedResult)
1935 })
1936 )
1937 ),
1938 Effect.catchCause(handleCause),
1939 Effect.provideContext(services as Context.Context<Tool.HandlerServices<Tools[keyof Tools]>>)
1940 )
1941 }
1942 })
1943 }
1944 for (const registration of registrations) {
1945 yield* registry.addTool(registration)
1946 }
1947})
1948
1949const isParameterValidationError = (
1950 error: unknown
1951): error is AiError.AiError & { readonly reason: AiError.ToolParameterValidationError } =>
1952 AiError.isAiError(error) && error.reason._tag === "ToolParameterValidationError"
1953
1954// MCP requires an object root, so a top-level `$ref` is inlined.
1955const toolJsonSchema = (schema: Schema.Constraint, strict: boolean): JsonSchema.JsonSchema => {
1956 const document = InternalStructuredOutput.resolveTopLevelReference(
1957 Schema.toJsonSchemaDocument(schema, { onExcessProperty: strict ? "error" : "ignore" })
1958 )
1959 return Object.keys(document.definitions).length === 0
1960 ? document.schema
1961 : { ...document.schema, $defs: document.definitions }
1962}
1963
1964/**
1965 * Registers an `AiToolkit` with the `McpServer`.
1966 *
1967 * @stability unstable
1968 * @category layers
1969 * @since 4.0.0
1970 */
1971export const toolkit = <Tools extends Record<string, Tool.Any>>(
1972 toolkit: Toolkit.Toolkit<Tools>
1973): Layer.Layer<
1974 never,
1975 never,
1976 Tool.HandlersFor<Tools> | Exclude<Tool.HandlerServices<Tools>, McpRequestContext>
1977> =>
1978 Layer.effectDiscard(registerToolkit(toolkit)).pipe(
1979 Layer.provide(McpServer.layer)
1980 )
1981
1982/**
1983 * Utility type that validates a completion-handler record against the allowed
1984 * parameter keys.
1985 *
1986 * @stability unstable
1987 * @category utility types
1988 * @since 4.0.0
1989 */
1990export type ValidateCompletions<Completions, Keys extends string> =
1991 & Completions
1992 & {
1993 readonly [K in keyof Completions]: K extends Keys ? (
1994 input: string,
1995 context: CompletionContext
1996 ) => any
1997 : never
1998 }
1999
2000/**
2001 * Completion-handler map for a resource URI template.
2002 *
2003 * **Details**
2004 *
2005 * Each schema interpolation contributes a parameter key, using an explicit
2006 * `Param` name when present or `paramN` otherwise, and each handler returns
2007 * candidate values for that parameter.
2008 *
2009 * @stability unstable
2010 * @category models
2011 * @since 4.0.0
2012 */
2013export type ResourceCompletions<Schemas extends ReadonlyArray<Schema.Constraint>> = {
2014 readonly [
2015 K in Extract<keyof Schemas, `${number}`> as Schemas[K] extends Param<infer Id, infer _S> ? Id
2016 : `param${K}`
2017 ]: (
2018 input: string,
2019 context: CompletionContext
2020 ) => Effect.Effect<Array<Schemas[K]["Type"]>, any, any>
2021}
2022
2023/**
2024 * Registers an MCP resource or resource template from an Effect program.
2025 *
2026 * **When to use**
2027 *
2028 * Use when you are already inside an Effect program with an `McpServer`
2029 * service and need to add a concrete resource or URI-template resource
2030 * directly.
2031 *
2032 * @see {@link resource} for the layer-based resource registration wrapper
2033 *
2034 * @stability unstable
2035 * @category handlers
2036 * @since 4.0.0
2037 */
2038export const registerResource: {
2039 <E, R>(options: {
2040 readonly uri: string
2041 readonly name: string
2042 readonly description?: string | undefined
2043 readonly mimeType?: string | undefined
2044 readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
2045 readonly priority?: number | undefined
2046 readonly content: Effect.Effect<
2047 ReadResourceResult | string | Uint8Array,
2048 E,
2049 R
2050 >
2051 readonly annotations?: Context.Context<never> | undefined
2052 }): Effect.Effect<void, never, Exclude<R, McpRequestContext> | McpServer>
2053 <const Schemas extends ReadonlyArray<Schema.Constraint>>(segments: TemplateStringsArray, ...schemas: Schemas): <
2054 E,
2055 R,
2056 const Completions extends Partial<ResourceCompletions<Schemas>> = {}
2057 >(options: {
2058 readonly name: string
2059 readonly description?: string | undefined
2060 readonly mimeType?: string | undefined
2061 readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
2062 readonly priority?: number | undefined
2063 readonly completion?: ValidateCompletions<Completions, keyof ResourceCompletions<Schemas>> | undefined
2064 readonly content: (uri: string, ...params: { readonly [K in keyof Schemas]: Schemas[K]["Type"] }) => Effect.Effect<
2065 ReadResourceResult | string | Uint8Array,
2066 E,
2067 R
2068 >
2069 readonly annotations?: Context.Context<never> | undefined
2070 }) => Effect.Effect<
2071 void,
2072 never,
2073 | Exclude<
2074 | Schemas[number]["DecodingServices"]
2075 | Schemas[number]["EncodingServices"]
2076 | R
2077 | (Completions[keyof Completions] extends (input: string) => infer Ret ?
2078 Ret extends Effect.Effect<infer _A, infer _E, infer _R> ? _R : never
2079 : never),
2080 McpRequestContext
2081 >
2082 | McpServer
2083 >
2084} = function() {
2085 if (arguments.length === 1) {
2086 const options = arguments[0] as {
2087 readonly uri: string
2088 readonly name: string
2089 readonly description?: string | undefined
2090 readonly mimeType?: string | undefined
2091 readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
2092 readonly priority?: number | undefined
2093 readonly content: Effect.Effect<ReadResourceResult | string | Uint8Array, any, any>
2094 readonly annotations?: Context.Context<never> | undefined
2095 }
2096 return Effect.gen(function*() {
2097 const services = omitRequestServices(yield* Effect.context<any>())
2098 const registry = yield* McpServer
2099 yield* registry.addResource({
2100 resource: new Resource({
2101 ...options,
2102 annotations: options
2103 }),
2104 handle: options.content.pipe(
2105 Effect.provideContext(services),
2106 Effect.map((content) => resolveResourceContent(options.uri, content)),
2107 Effect.catchCause((cause) => {
2108 const prettyError = Cause.prettyErrors(cause)[0]
2109 return Effect.fail(new InternalError({ message: prettyError.message }))
2110 })
2111 ),
2112 annotations: options.annotations ?? Context.empty()
2113 })
2114 })
2115 }
2116 const {
2117 params,
2118 routerPath,
2119 schema,
2120 uriPath
2121 } = compileUriTemplate(...(arguments as any as [any, any]))
2122 return Effect.fnUntraced(function*<E, R>(options: {
2123 readonly name: string
2124 readonly description?: string | undefined
2125 readonly mimeType?: string | undefined
2126 readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
2127 readonly priority?: number | undefined
2128 readonly completion?:
2129 | Record<
2130 string,
2131 (
2132 input: string,
2133 context: CompletionContext
2134 ) => Effect.Effect<any>
2135 >
2136 | undefined
2137 readonly content: (uri: string, ...params: Array<any>) => Effect.Effect<
2138 ReadResourceResult | string | Uint8Array,
2139 E,
2140 R
2141 >
2142 readonly annotations?: Context.Context<never> | undefined
2143 }) {
2144 const services = omitRequestServices(yield* Effect.context<any>())
2145 const registry = yield* McpServer
2146 const decode = Schema.decodeUnknownEffect(schema)
2147 const template = new ResourceTemplate({
2148 ...options,
2149 uriTemplate: uriPath,
2150 annotations: options!
2151 })
2152 const completions: Record<
2153 string,
2154 (
2155 input: string,
2156 context: CompletionContext
2157 ) => Effect.Effect<CompleteResult, InternalError>
2158 > = Object.create(null)
2159 for (const [param, handle] of Object.entries(options.completion ?? {})) {
2160 const encodeArray = Schema.encodeUnknownEffect(Schema.Array(params[param]))
2161 const handler = (
2162 input: string,
2163 context: CompletionContext
2164 ) =>
2165 handle(input, context).pipe(
2166 Effect.flatMap(encodeArray),
2167 Effect.map((values) => ({
2168 completion: {
2169 values: values as Array<string>,
2170 total: values.length,
2171 hasMore: false
2172 }
2173 })),
2174 Effect.catchCause((cause) => {
2175 const prettyError = Cause.prettyErrors(cause)[0]
2176 return Effect.fail(new InternalError({ message: prettyError.message }))
2177 }),
2178 Effect.provideContext(services)
2179 )
2180 completions[param] = handler
2181 }
2182 yield* registry.addResourceTemplate({
2183 template,
2184 routerPath,
2185 completions,
2186 annotations: options.annotations ?? Context.empty(),
2187 handle: (uri, params) =>
2188 decode(params).pipe(
2189 Effect.mapError((error) => new InvalidParams({ message: error.message })),
2190 Effect.flatMap((params: any) =>
2191 options.content(uri, ...params).pipe(
2192 Effect.map((content) => resolveResourceContent(uri, content)),
2193 Effect.catchCause((cause) => {
2194 const prettyError = Cause.prettyErrors(cause)[0]
2195 return Effect.fail(new InternalError({ message: prettyError.message }))
2196 })
2197 )
2198 ),
2199 Effect.provideContext(services)
2200 )
2201 })
2202 })
2203} as any
2204
2205/**
2206 * Creates a layer that registers an MCP resource or resource template.
2207 *
2208 * **When to use**
2209 *
2210 * Use to compose resource registration into an MCP server layer.
2211 *
2212 * @see {@link registerResource} for the Effect-level resource registration API
2213 *
2214 * @stability unstable
2215 * @category layers
2216 * @since 4.0.0
2217 */
2218export const resource: {
2219 <E, R>(options: {
2220 readonly uri: string
2221 readonly name: string
2222 readonly description?: string | undefined
2223 readonly mimeType?: string | undefined
2224 readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
2225 readonly priority?: number | undefined
2226 readonly content: Effect.Effect<
2227 ReadResourceResult | string | Uint8Array,
2228 E,
2229 R
2230 >
2231 }): Layer.Layer<never, never, Exclude<R, McpRequestContext>>
2232 <const Schemas extends ReadonlyArray<Schema.Constraint>>(segments: TemplateStringsArray, ...schemas: Schemas): <
2233 E,
2234 R,
2235 const Completions extends Partial<ResourceCompletions<Schemas>> = {}
2236 >(options: {
2237 readonly name: string
2238 readonly description?: string | undefined
2239 readonly mimeType?: string | undefined
2240 readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined
2241 readonly priority?: number | undefined
2242 readonly completion?: ValidateCompletions<Completions, keyof ResourceCompletions<Schemas>> | undefined
2243 readonly content: (uri: string, ...params: { readonly [K in keyof Schemas]: Schemas[K]["Type"] }) => Effect.Effect<
2244 ReadResourceResult | string | Uint8Array,
2245 E,
2246 R
2247 >
2248 }) => Layer.Layer<
2249 never,
2250 never,
2251 Exclude<
2252 | R
2253 | (Completions[keyof Completions] extends (input: string) => infer Ret ?
2254 Ret extends Effect.Effect<infer _A, infer _E, infer _R> ? _R : never
2255 : never),
2256 McpRequestContext
2257 >
2258 >
2259} = function() {
2260 if (arguments.length === 1) {
2261 return Layer.effectDiscard(registerResource(arguments[0])).pipe(
2262 Layer.provide(McpServer.layer)
2263 )
2264 }
2265 const register = registerResource(...(arguments as any as [any, any]))
2266 return (options: any) =>
2267 Layer.effectDiscard(register(options)).pipe(
2268 Layer.provide(McpServer.layer)
2269 )
2270} as any
2271
2272/**
2273 * Registers an MCP prompt from an Effect program.
2274 *
2275 * **When to use**
2276 *
2277 * Use when you are already inside an Effect program with an `McpServer`
2278 * service and need to add a prompt handler directly.
2279 *
2280 * **Details**
2281 *
2282 * Parameters are decoded with the supplied schema, completion handlers encode
2283 * per-parameter suggestions, and string prompt content is converted into a user
2284 * text message.
2285 *
2286 * @see {@link prompt} for the layer-based prompt registration wrapper
2287 *
2288 * @stability unstable
2289 * @category handlers
2290 * @since 4.0.0
2291 */
2292export const registerPrompt = <
2293 E,
2294 R,
2295 Params extends Schema.Struct.Fields = {},
2296 const Completions extends {
2297 readonly [K in keyof Params]?: (
2298 input: string,
2299 context: CompletionContext
2300 ) => Effect.Effect<Array<Params[K]["Type"]>, any, any>
2301 } = {}
2302>(
2303 options: {
2304 readonly name: string
2305 readonly title?: string | undefined
2306 readonly description?: string | undefined
2307 readonly parameters?: Params | undefined
2308 readonly completion?: ValidateCompletions<Completions, Extract<keyof Params, string>> | undefined
2309 readonly content: (
2310 params: Schema.Struct.Type<Params>
2311 ) => Effect.Effect<Array<PromptMessage> | string, E, R>
2312 readonly annotations?: Context.Context<never> | undefined
2313 }
2314): Effect.Effect<
2315 void,
2316 never,
2317 Exclude<Schema.Struct.DecodingServices<Params> | R, McpRequestContext> | McpServer
2318> => {
2319 const args = Arr.empty<PromptArgument>()
2320 const props: Record<string, Schema.Constraint> = options.parameters ?? {}
2321 for (const [name, prop] of Object.entries(props)) {
2322 args.push({
2323 name,
2324 description: SchemaAST.resolveDescription(prop.ast),
2325 required: !SchemaAST.isOptional(prop.ast)
2326 })
2327 }
2328 const prompt = new Prompt({
2329 name: options.name,
2330 title: options.title,
2331 description: options.description,
2332 arguments: args
2333 })
2334 const decode = options.parameters
2335 ? Schema.decodeEffect(Schema.Struct(props))
2336 : () => Effect.succeed({} as Params)
2337 const completion: Record<
2338 string,
2339 (
2340 input: string,
2341 context: CompletionContext
2342 ) => Effect.Effect<any>
2343 > = options.completion ?? {}
2344 return Effect.gen(function*() {
2345 const registry = yield* McpServer
2346 const services = omitRequestServices(
2347 yield* Effect.context<
2348 Exclude<R | Schema.Struct.DecodingServices<Params>, McpRequestContext>
2349 >()
2350 )
2351 const completions: Record<
2352 string,
2353 (
2354 input: string,
2355 context: CompletionContext
2356 ) => Effect.Effect<CompleteResult, InternalError, McpRequestContext>
2357 > = Object.create(null)
2358 for (const [param, handle] of Object.entries(completion)) {
2359 const encodeArray = Schema.encodeEffect(Schema.Array(props[param]))
2360 const handler = (
2361 input: string,
2362 context: CompletionContext
2363 ) =>
2364 handle(input, context).pipe(
2365 Effect.flatMap(encodeArray),
2366 Effect.map((values) => ({
2367 completion: {
2368 values: values as Array<string>,
2369 total: values.length,
2370 hasMore: false
2371 }
2372 })),
2373 Effect.catchCause((cause) => {
2374 const prettyError = Cause.prettyErrors(cause)[0]
2375 return Effect.fail(new InternalError({ message: prettyError.message }))
2376 }),
2377 Effect.provide(services)
2378 )
2379 completions[param] = handler as any
2380 }
2381 yield* registry.addPrompt({
2382 prompt,
2383 completions,
2384 annotations: options.annotations ?? Context.empty(),
2385 handle: (params) =>
2386 decode(params).pipe(
2387 Effect.mapError((error) => new InvalidParams({ message: error.message })),
2388 Effect.flatMap((params) =>
2389 options.content(params as any).pipe(
2390 Effect.catchCause((cause) => {
2391 const prettyError = Cause.prettyErrors(cause)[0]
2392 return Effect.fail(new InternalError({ message: prettyError.message }))
2393 })
2394 )
2395 ),
2396 Effect.map((messages) => {
2397 messages = typeof messages === "string" ?
2398 [{
2399 role: "user",
2400 content: TextContent.make({ text: messages })
2401 }] :
2402 messages
2403 return new GetPromptResult({ messages, description: prompt.description })
2404 }),
2405 Effect.provideContext(services as Context.Context<unknown>)
2406 )
2407 })
2408 })
2409}
2410
2411/**
2412 * Creates a layer that registers an MCP prompt.
2413 *
2414 * **When to use**
2415 *
2416 * Use to compose prompt registration into an MCP server layer.
2417 *
2418 * **Details**
2419 *
2420 * Parameters are decoded with the supplied schema, completion handlers encode
2421 * per-parameter suggestions, and string prompt content is converted into a user
2422 * text message.
2423 *
2424 * @see {@link registerPrompt} for the Effect-level prompt registration API
2425 *
2426 * @stability unstable
2427 * @category layers
2428 * @since 4.0.0
2429 */
2430export const prompt = <
2431 E,
2432 R,
2433 Params extends Schema.Struct.Fields = {},
2434 const Completions extends {
2435 readonly [K in keyof Params]?: (
2436 input: string,
2437 context: CompletionContext
2438 ) => Effect.Effect<Array<Params[K]["Type"]>, any, any>
2439 } = {}
2440>(
2441 options: {
2442 readonly name: string
2443 readonly title?: string | undefined
2444 readonly description?: string | undefined
2445 readonly parameters?: Params | undefined
2446 readonly completion?: ValidateCompletions<Completions, Extract<keyof Params, string>> | undefined
2447 readonly content: (
2448 params: Schema.Struct.Type<Params>
2449 ) => Effect.Effect<Array<PromptMessage> | string, E, R>
2450 readonly annotations?: Context.Context<never> | undefined
2451 }
2452): Layer.Layer<
2453 never,
2454 never,
2455 Exclude<Schema.Struct.DecodingServices<Params> | R, McpRequestContext>
2456> =>
2457 Layer.effectDiscard(registerPrompt(options)).pipe(
2458 Layer.provide(McpServer.layer)
2459 )
2460
2461/**
2462 * Collects structured input from the current MCP client and decodes the
2463 * accepted response with `schema`.
2464 *
2465 * **Details**
2466 *
2467 * Accepted content is decoded with the supplied schema, declined requests fail
2468 * with `ElicitationDeclined`, and canceled requests interrupt the effect.
2469 *
2470 * @stability unstable
2471 * @category accessors
2472 * @since 4.0.0
2473 */
2474export const elicit: <S extends Schema.ConstraintEncoder<Record<string, unknown>, unknown>>(options: {
2475 readonly message: string
2476 readonly schema: S
2477}) => Effect.Effect<
2478 S["Type"],
2479 ElicitationDeclined,
2480 McpServerClient | S["DecodingServices"]
2481> = Effect.fnUntraced(function*<S extends Schema.ConstraintEncoder<Record<string, unknown>, unknown>>(options: {
2482 readonly message: string
2483 readonly schema: S
2484}) {
2485 const { getClient } = yield* McpServerClient
2486 const client = yield* getClient
2487 const schema = options.schema
2488 const request = yield* Schema.decodeUnknownEffect(McpSchema.ElicitRequestFormParams)({
2489 mode: "form",
2490 message: options.message,
2491 requestedSchema: toolJsonSchema(schema, false)
2492 }).pipe(Effect.orDie)
2493 const res = yield* client.elicit(request).pipe(
2494 Effect.catchCause((cause) => Effect.fail(new ElicitationDeclined({ cause: Cause.squash(cause), request })))
2495 )
2496 switch (res.action) {
2497 case "accept":
2498 return yield* Effect.orDie(Schema.decodeUnknownEffect(schema)(res.content))
2499 case "cancel":
2500 return yield* Effect.interrupt
2501 case "decline":
2502 return yield* new ElicitationDeclined({ request })
2503 }
2504}, Effect.scoped)
2505
2506/**
2507 * Accesses the current client's capabilities.
2508 *
2509 * @stability unstable
2510 * @category accessors
2511 * @since 4.0.0
2512 */
2513export const clientCapabilities: Effect.Effect<
2514 ClientCapabilities,
2515 never,
2516 McpRequestContext
2517> = McpRequestContext.useSync((_) => _.clientCapabilities)
2518
2519// -----------------------------------------------------------------------------
2520// Internal
2521// -----------------------------------------------------------------------------
2522
2523const makeUriMatcher = <A>() => {
2524 const router = FindMyWay.make<A>({
2525 ignoreTrailingSlash: true,
2526 ignoreDuplicateSlashes: true,
2527 caseSensitive: true
2528 })
2529 const add = (uri: string, value: A) => {
2530 router.on("GET", `/${uri}`, value)
2531 }
2532 const find = (uri: string) => router.find("GET", `/${uri}`)
2533
2534 return { add, find } as const
2535}
2536
2537const compileUriTemplate = (segments: TemplateStringsArray, ...schemas: ReadonlyArray<Schema.Constraint>) => {
2538 let routerPath = segments[0].replace(":", "::")
2539 let uriPath = segments[0]
2540 const params: Record<string, Schema.Top> = Object.create(null)
2541 let pathSchema = Schema.Tuple([]) as Schema.Top
2542 if (schemas.length > 0) {
2543 const arr: Array<Schema.Top> = []
2544 for (let i = 0; i < schemas.length; i++) {
2545 const toCodecStringTree = Schema.toCodecStringTree(schemas[i])
2546 const segment = segments[i + 1]
2547 const key = String(i)
2548 arr.push(toCodecStringTree)
2549 routerPath += `:${key}${segment.replace(":", "::")}`
2550 const schema = schemas[i]
2551 const paramName = isParam(schema) ? (schema as Param<string, Schema.Top>).name : `param${key}`
2552 params[paramName] = toCodecStringTree
2553 uriPath += `{${paramName}}${segment}`
2554 }
2555 pathSchema = Schema.Tuple(arr)
2556 }
2557 return {
2558 routerPath,
2559 uriPath,
2560 schema: pathSchema,
2561 params
2562 } as const
2563}
2564
2565const resolveResourceContent = (
2566 uri: string,
2567 content: ReadResourceResult | string | Uint8Array
2568): ReadResourceResult => {
2569 if (typeof content === "string") {
2570 return {
2571 contents: [{
2572 uri,
2573 text: content
2574 }]
2575 }
2576 } else if (content instanceof Uint8Array) {
2577 return {
2578 contents: [{
2579 uri,
2580 blob: content
2581 }]
2582 }
2583 }
2584 return content
2585}
2586
2587const InvalidBatchExit = Schema.TaggedStruct("Exit", {
2588 requestId: Schema.Null,
2589 exit: Schema.TaggedStruct("Failure", {
2590 cause: Schema.Unknown
2591 })
2592})
2593
2594const decodeInvalidBatchExit = Schema.decodeUnknownResult(InvalidBatchExit)
2595
2596const isProtocolVersion = (version: string): version is McpProtocol.ProtocolVersion =>
2597 version === "2024-11-05" ||
2598 version === "2025-03-26" ||
2599 version === "2025-06-18" ||
2600 version === "2025-11-25" ||
2601 version === "2026-07-28"