1/**
2 * MCP v2026-07-28 wire projections.
3 *
4 * @internal
5 */
6import * as Deferred from "../../../Deferred.ts"
7import * as Effect from "../../../Effect.ts"
8import * as Base64 from "../../../encoding/Base64.ts"
9import { appendPreResponseHandlerUnsafe } from "../../../http/HttpEffect.ts"
10import * as HttpServerRequest from "../../../http/HttpServerRequest.ts"
11import * as HttpServerResponse from "../../../http/HttpServerResponse.ts"
12import * as Match from "../../../Match.ts"
13import * as Option from "../../../Option.ts"
14import * as Predicate from "../../../Predicate.ts"
15import * as PubSub from "../../../PubSub.ts"
16import * as Queue from "../../../Queue.ts"
17import * as Record from "../../../Record.ts"
18import type * as Rpc from "../../../rpc/Rpc.ts"
19import type * as RpcMessage from "../../../rpc/RpcMessage.ts"
20import * as Schema from "../../../Schema.ts"
21import * as Scope from "../../../Scope.ts"
22import type * as Types from "../../../Types.ts"
23import type * as PublicMcpProtocol from "../../McpProtocol.ts"
24import * as PublicMcpSchema from "../../McpSchema.ts"
25import * as McpCore from "../mcpCore.ts"
26import * as McpProtocol from "../mcpProtocol.ts"
27import * as McpSchema from "../mcpSchema/v2026_07_28.ts"
28
29const InputResponses = Schema.Record(Schema.String, Schema.JsonObject)
30const decodeRequestMetadata = Schema.decodeUnknownEffect(McpSchema.RequestMetaObject)
31const decodeCancellation = Schema.decodeUnknownEffect(McpSchema.CancelledNotification.payloadSchema)
32const isJson = Schema.is(Schema.Json)
33const encodeImplementation = Schema.encodeEffect(McpSchema.Implementation)
34const MAX_PENDING_SUBSCRIPTION_NOTIFICATIONS = 64
35
36type ObjectWithUndefined = Readonly<Record<string, unknown>>
37
38const omitUndefined = (value: ObjectWithUndefined): Record<string, unknown> => {
39 const result: Record<string, unknown> = {}
40 for (const key in value) {
41 if (value[key] !== undefined) {
42 result[key] = value[key]
43 }
44 }
45 return result
46}
47
48const parameterHeaderMatches = (header: string | undefined, argument: unknown): boolean => {
49 if (Predicate.isNullish(argument)) {
50 return header === undefined
51 }
52 if (header === undefined) {
53 return false
54 }
55 const decoded = McpProtocol.decodeRoutingHeader(header)
56 if (decoded === undefined) {
57 return false
58 }
59 return Match.value(argument).pipe(
60 Match.when(Match.string, (argument) => decoded === argument),
61 Match.when(Match.boolean, (argument) => decoded === String(argument)),
62 Match.when(
63 Match.number,
64 (argument) => Number.isSafeInteger(argument) && decoded.trim().length > 0 && Number(decoded) === argument
65 ),
66 Match.orElse(() => false)
67 )
68}
69
70/**
71 * @internal
72 */
73export interface StatelessRequestProfile {
74 readonly protocolVersion: typeof McpSchema.protocolVersion
75 readonly clientCapabilities: typeof McpSchema.ClientCapabilities.Type
76 readonly clientInfo?: typeof McpSchema.Implementation.Type | undefined
77 readonly requestMetadata: typeof McpSchema.RequestMetaObject.Type
78}
79
80/**
81 * @internal
82 */
83export const profileFromRequestMetadata = Effect.fnUntraced(function*(metadata: unknown) {
84 const requestMetadata = yield* decodeRequestMetadata(metadata)
85 return {
86 protocolVersion: McpSchema.protocolVersion,
87 clientCapabilities: requestMetadata["io.modelcontextprotocol/clientCapabilities"],
88 clientInfo: requestMetadata["io.modelcontextprotocol/clientInfo"],
89 requestMetadata
90 } satisfies StatelessRequestProfile
91})
92
93const resultMetadata = (
94 value: ObjectWithUndefined,
95 serverInfo: Schema.JsonObject
96): Schema.JsonObject => {
97 const metadata: unknown = value._meta
98 return {
99 ...(Predicate.isReadonlyObject(metadata) ? metadata : {}),
100 "io.modelcontextprotocol/serverInfo": serverInfo
101 }
102}
103
104const projectInputRequired = (
105 outcome: McpCore.InputRequiredFields,
106 serverInfo: Schema.JsonObject
107) =>
108 omitUndefined({
109 _meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
110 resultType: "input_required",
111 inputRequests: outcome.inputRequests,
112 requestState: outcome.requestState
113 })
114
115const decodeCallToolOutcome = Schema.decodeUnknownEffect(Schema.Union([
116 McpSchema.CallToolResult,
117 McpSchema.InputRequiredResult
118]))
119
120/**
121 * @internal
122 */
123export const projectCallToolOutcome = Effect.fnUntraced(function*(
124 outcome: McpCore.OperationOutcome<ObjectWithUndefined>,
125 serverInfo: typeof McpSchema.Implementation.Type
126) {
127 const encodedServerInfo = yield* encodeImplementation(serverInfo)
128 if (outcome._tag === "Complete") {
129 return yield* decodeCallToolOutcome({
130 ...omitUndefined(outcome.value),
131 _meta: resultMetadata(outcome.value, encodedServerInfo),
132 resultType: "complete"
133 })
134 }
135 return yield* decodeCallToolOutcome(projectInputRequired(outcome, encodedServerInfo))
136})
137
138const requiredCapabilitiesForInputRequests = (
139 inputRequests: McpCore.InputRequiredFields["inputRequests"],
140 capabilities: McpCore.NegotiatedProtocolProfile["clientCapabilities"]
141): Record<string, Schema.JsonObject> => {
142 const required: Record<string, Types.Mutable<Schema.JsonObject>> = {}
143 for (const request of Object.values(inputRequests ?? {})) {
144 Match.value(request).pipe(
145 Match.when({ method: "roots/list" }, () => {
146 if (capabilities.roots === undefined) required.roots = {}
147 }),
148 Match.when({ method: "sampling/createMessage" }, (request) => {
149 const sampling = required.sampling ?? {}
150 if (McpProtocol.samplingRequestRequiresTools(request.params) && capabilities.sampling?.tools === undefined) {
151 sampling.tools = {}
152 }
153 if (
154 (request.params.includeContext === "thisServer" || request.params.includeContext === "allServers") &&
155 capabilities.sampling?.context === undefined
156 ) {
157 sampling.context = {}
158 }
159 if (capabilities.sampling === undefined || Object.keys(sampling).length > 0) required.sampling = sampling
160 }),
161 Match.when({ method: "elicitation/create" }, (request) => {
162 const mode = request.params.mode === "url" ? "url" : "form"
163 const elicitation = capabilities.elicitation
164 const supportsMode = elicitation !== undefined && (mode === "url"
165 ? elicitation.url !== undefined
166 : elicitation.form !== undefined || Object.keys(elicitation).length === 0)
167 if (!supportsMode) (required.elicitation ??= {})[mode] = {}
168 }),
169 Match.exhaustive
170 )
171 }
172 return required
173}
174
175const projectResourceContents = (
176 resource: PublicMcpSchema.TextResourceContents | PublicMcpSchema.BlobResourceContents
177) =>
178 omitUndefined({
179 uri: resource.uri,
180 mimeType: resource.mimeType,
181 _meta: resource._meta,
182 ...("text" in resource ? { text: resource.text } : { blob: Base64.encode(resource.blob) })
183 })
184
185const projectContent = Match.type<PublicMcpSchema.ContentBlock>().pipe(
186 Match.when({ type: Match.is("text", "resource_link") }, (content) => content),
187 Match.when({ type: Match.is("image", "audio") }, (content) =>
188 omitUndefined({
189 type: content.type,
190 mimeType: content.mimeType,
191 data: Base64.encode(content.data),
192 annotations: content.annotations,
193 _meta: content._meta
194 })),
195 Match.when({ type: "resource" }, (content) =>
196 omitUndefined({
197 type: "resource",
198 resource: projectResourceContents(content.resource),
199 annotations: content.annotations,
200 _meta: content._meta
201 })),
202 Match.exhaustive
203)
204
205const projectResource = (resource: PublicMcpSchema.Resource) => McpSchema.Resource.make(resource)
206const projectResourceTemplate = (resourceTemplate: PublicMcpSchema.ResourceTemplate) =>
207 McpSchema.ResourceTemplate.make(resourceTemplate)
208
209const projectPrompt = (prompt: PublicMcpSchema.Prompt) =>
210 McpProtocol.transcode(PublicMcpSchema.Prompt, McpSchema.Prompt, prompt)
211
212const projectTool = (tool: PublicMcpSchema.Tool) => McpProtocol.transcode(PublicMcpSchema.Tool, McpSchema.Tool, tool)
213
214const privateStaleCache = {
215 ttlMs: 0,
216 cacheScope: "private"
217} satisfies { readonly ttlMs: number; readonly cacheScope: "private" }
218
219const projectCompleteResult = Effect.fnUntraced(function*(
220 value: ObjectWithUndefined,
221 serverInfo: typeof McpSchema.Implementation.Type
222) {
223 const encodedServerInfo = yield* encodeImplementation(serverInfo)
224 return {
225 ...omitUndefined(value),
226 _meta: resultMetadata(value, encodedServerInfo),
227 resultType: "complete"
228 }
229})
230
231type ProtocolError =
232 | McpCore.ResourceNotFound
233 | McpCore.PromptNotFound
234 | McpCore.ToolError
235 | McpCore.UnsupportedByProtocol
236 | McpProtocol.ProtocolError
237 | PublicMcpSchema.McpError
238 | Schema.SchemaError
239
240const matchedProtocolError = Match.type<ProtocolError>().pipe(
241 Match.tag("ResourceNotFound", (error) =>
242 new McpProtocol.ProtocolError({
243 code: McpSchema.INVALID_PARAMS,
244 message: `Resource '${error.uri}' not found`,
245 data: { uri: error.uri }
246 })),
247 Match.tag(
248 "ToolNotFound",
249 "InvalidToolInput",
250 "InvalidToolContinuation",
251 "ToolExecutionError",
252 "UnsupportedByProtocol",
253 McpProtocol.ProtocolError.fromTool
254 ),
255 Match.orElse(McpProtocol.ProtocolError.fromFeature)
256)
257
258const projectError = (error: ProtocolError): McpSchema.McpError => {
259 const protocolError = matchedProtocolError(error)
260 return McpSchema.McpError.make({
261 code: protocolError.code,
262 message: protocolError.message,
263 ...(isJson(protocolError.data) ? { data: protocolError.data } : {})
264 })
265}
266
267const failWithHttpStatus = Effect.fnUntraced(function*(status: number, error: McpProtocol.ProtocolError) {
268 const request = yield* Effect.serviceOption(HttpServerRequest.HttpServerRequest)
269 if (Option.isSome(request)) {
270 appendPreResponseHandlerUnsafe(
271 request.value,
272 (_request, response) => Effect.succeed(HttpServerResponse.setStatus(response, status))
273 )
274 }
275 return yield* error
276})
277
278/**
279 * @internal
280 */
281export const normalizeCancellation = (payload: unknown) =>
282 decodeCancellation(payload).pipe(
283 Effect.map((request) => ({
284 requestId: request.requestId,
285 reason: request.reason,
286 metadata: request._meta
287 }))
288 )
289
290const unsupported = (
291 operation: PublicMcpSchema.McpReverseOperationUnsupported["operation"]
292): PublicMcpSchema.McpReverseOperationUnsupported =>
293 new PublicMcpSchema.McpReverseOperationUnsupported({
294 operation,
295 protocolVersion: McpSchema.protocolVersion,
296 reason: "MCP 2026-07-28 carries server input requests in multi round-trip results"
297 })
298
299/**
300 * @internal
301 */
302export const handlerRpcs = McpSchema.ClientRequestRpcs.merge(McpSchema.ClientNotificationRpcs)
303
304/**
305 * @internal
306 */
307export const makeHandlers = (
308 core: McpCore.McpCore,
309 _lifecycle: McpProtocol.LifecycleRuntime | undefined,
310 context: McpProtocol.HandlerInstallationContext
311) => {
312 const decodeInputResponses = Schema.decodeUnknownEffect(InputResponses)
313 const decodeDiscoverResult = Schema.decodeUnknownEffect(McpSchema.DiscoverResult)
314 const decodeListResourcesResult = Schema.decodeUnknownEffect(McpSchema.ListResourcesResult)
315 const decodeListResourceTemplatesResult = Schema.decodeUnknownEffect(McpSchema.ListResourceTemplatesResult)
316 const decodeReadResourceResult = Schema.decodeUnknownEffect(McpSchema.ReadResourceResult)
317 const decodeListPromptsResult = Schema.decodeUnknownEffect(McpSchema.ListPromptsResult)
318 const decodeGetPromptResult = Schema.decodeUnknownEffect(McpSchema.GetPromptResult)
319 const decodePromptOutcome = Schema.decodeUnknownEffect(Schema.Union([
320 McpSchema.GetPromptResult,
321 McpSchema.InputRequiredResult
322 ]))
323 const decodeCompleteResult = Schema.decodeUnknownEffect(McpSchema.CompleteResult)
324 const decodeListToolsResult = Schema.decodeUnknownEffect(McpSchema.ListToolsResult)
325 const sendNotification = context.sendNotification
326 const supportsSubscriptions = sendNotification !== undefined
327 // Older adapters accept arbitrary JSON; this binding requires extension settings objects.
328 const extensions = context.serverInfo.extensions === undefined ? undefined : Record.filter(
329 context.serverInfo.extensions,
330 (value): value is Schema.JsonObject => Predicate.isReadonlyObject(value)
331 )
332 const getDiscovery = Effect.map(context.registrationPresence, (presence) => {
333 const capabilities: Types.Mutable<typeof McpSchema.ServerCapabilities.Type> = { completions: {}, logging: {} }
334 if (extensions !== undefined) capabilities.extensions = extensions
335 if (presence.tools) capabilities.tools = { listChanged: supportsSubscriptions }
336 if (presence.resources) {
337 capabilities.resources = { listChanged: supportsSubscriptions, subscribe: supportsSubscriptions }
338 }
339 if (presence.prompts) capabilities.prompts = { listChanged: supportsSubscriptions }
340 return { supportedVersions: context.supportedVersions, capabilities, serverInfo: context.serverInfo }
341 })
342 const getInvocation = PublicMcpSchema.McpRequestContext.useSync(
343 McpProtocol.invocationFromRequestContext
344 )
345 const getInputInvocation = Effect.fnUntraced(function*(
346 request: Pick<typeof McpSchema.CallTool.payloadSchema.Type, "inputResponses" | "requestState">
347 ) {
348 const context = yield* PublicMcpSchema.McpRequestContext
349 const inputResponses = request.inputResponses === undefined
350 ? undefined
351 // The RPC payload already validated the dated response union; this decode only erases it into canonical JSON.
352 : yield* decodeInputResponses(request.inputResponses).pipe(Effect.orDie)
353 return McpProtocol.invocationFromRequestContext(PublicMcpSchema.McpRequestContext.of({
354 ...context,
355 inputResponses,
356 requestState: request.requestState
357 }))
358 })
359 const validateInputRequestCapabilities = Effect.fnUntraced(function*(
360 inputRequired: McpCore.InputRequiredFields,
361 capabilities: McpCore.NegotiatedProtocolProfile["clientCapabilities"]
362 ) {
363 const requiredCapabilities = requiredCapabilitiesForInputRequests(inputRequired.inputRequests, capabilities)
364 if (Object.keys(requiredCapabilities).length === 0) {
365 return
366 }
367 return yield* failWithHttpStatus(
368 400,
369 new McpProtocol.ProtocolError({
370 code: McpSchema.MISSING_REQUIRED_CLIENT_CAPABILITY,
371 message: "The request requires client capabilities that were not declared",
372 data: { requiredCapabilities }
373 })
374 )
375 })
376 return ({
377 "subscriptions/listen": Effect.fnUntraced(function*(
378 request: typeof McpSchema.SubscriptionsListen.payloadSchema.Type,
379 { client, requestId }: { readonly client: Rpc.ServerClient; readonly requestId: RpcMessage.RequestId }
380 ) {
381 if (sendNotification === undefined) {
382 return yield* new McpProtocol.ProtocolError({
383 code: McpSchema.METHOD_NOT_FOUND,
384 message: "Method not found: subscriptions/listen"
385 })
386 }
387 const presence = yield* context.registrationPresence
388 const eventsScope = yield* Scope.fork(yield* Effect.scope)
389 const events = yield* Scope.provide(context.subscribeServerNotifications, eventsScope)
390 const honored = {
391 ...(presence.tools && request.notifications.toolsListChanged === true
392 ? { toolsListChanged: true }
393 : {}),
394 ...(presence.prompts && request.notifications.promptsListChanged === true
395 ? { promptsListChanged: true }
396 : {}),
397 ...(presence.resources && request.notifications.resourcesListChanged === true
398 ? { resourcesListChanged: true }
399 : {}),
400 ...(presence.resources && request.notifications.resourceSubscriptions !== undefined
401 ? { resourceSubscriptions: request.notifications.resourceSubscriptions }
402 : {})
403 }
404 const subscriptionMetadata = { "io.modelcontextprotocol/subscriptionId": requestId }
405 const resourceSubscriptions = new Set(honored.resourceSubscriptions)
406 const pending = yield* Queue.dropping<PublicMcpProtocol.ProjectedNotification>(
407 MAX_PENDING_SUBSCRIPTION_NOTIFICATIONS
408 )
409 const overflowed = yield* Deferred.make<void>()
410 yield* Effect.gen(function*() {
411 while (true) {
412 const event = yield* PubSub.take(events)
413 if (event.targetClientId !== undefined && event.targetClientId !== client.id) {
414 continue
415 }
416 const notification = event.notification
417 const rpc: Rpc.AnyWithProps | undefined = Match.value(notification).pipe(
418 Match.tags({
419 ToolsChanged: () => honored.toolsListChanged === true ? McpSchema.ToolListChangedNotification : undefined,
420 PromptsChanged: () =>
421 honored.promptsListChanged === true ? McpSchema.PromptListChangedNotification : undefined,
422 ResourcesChanged: () =>
423 honored.resourcesListChanged === true ? McpSchema.ResourceListChangedNotification : undefined,
424 ResourceUpdated: (notification) =>
425 resourceSubscriptions.has(notification.uri) ? McpSchema.ResourceUpdatedNotification : undefined
426 }),
427 Match.exhaustive
428 )
429 if (rpc === undefined) {
430 continue
431 }
432 const projected = {
433 tag: rpc._tag,
434 payload: rpc.payloadSchema.make({
435 _meta: { ...notification.metadata, ...subscriptionMetadata },
436 ...(notification._tag === "ResourceUpdated" ? { uri: notification.uri } : {})
437 })
438 }
439 if (!(yield* Queue.offer(pending, projected))) {
440 break
441 }
442 }
443 }).pipe(
444 // Release the upstream listener on overflow even while transport writes remain blocked.
445 Scope.use(eventsScope),
446 Effect.andThen(Effect.suspend(() => context.markSubscriptionCancelled?.(client.id, requestId) ?? Effect.void)),
447 Effect.andThen(Deferred.succeed(overflowed, undefined)),
448 Effect.forkScoped
449 )
450 yield* sendNotification(McpSchema.protocolVersion, client.id, {
451 tag: McpSchema.SubscriptionsAcknowledgedNotification._tag,
452 payload: McpSchema.SubscriptionsAcknowledgedNotification.payloadSchema.make({
453 _meta: subscriptionMetadata,
454 notifications: honored
455 })
456 })
457 const deliver = Effect.forever(
458 Queue.take(pending).pipe(
459 Effect.flatMap((notification) => sendNotification(McpSchema.protocolVersion, client.id, notification))
460 )
461 )
462 yield* Effect.race(deliver, Deferred.await(overflowed))
463 yield* context.terminateSubscription?.(
464 McpSchema.protocolVersion,
465 client.id,
466 requestId,
467 "Pending notification limit exceeded"
468 ) ?? Effect.void
469 const encodedServerInfo = yield* encodeImplementation(context.serverInfo)
470 return McpSchema.SubscriptionsListenResult.make({
471 _meta: {
472 ...subscriptionMetadata,
473 "io.modelcontextprotocol/serverInfo": encodedServerInfo
474 },
475 resultType: "complete"
476 })
477 }, Effect.mapError(projectError)),
478 "server/discover": Effect.fnUntraced(function*(
479 _request: typeof McpSchema.Discover.payloadSchema.Type
480 ) {
481 const discovery = yield* getDiscovery
482 const result = yield* projectCompleteResult({
483 ...privateStaleCache,
484 supportedVersions: Array.from(discovery.supportedVersions),
485 capabilities: discovery.capabilities,
486 instructions: context.serverInfo.instructions
487 }, context.serverInfo)
488 return yield* decodeDiscoverResult(result)
489 }, Effect.mapError(projectError)),
490 "resources/list": Effect.fnUntraced(function*(
491 _request: typeof McpSchema.ListResources.payloadSchema.Type
492 ) {
493 const invocation = yield* getInvocation
494 const resources = yield* core.resources.list(invocation.protocol)
495 const result = yield* projectCompleteResult({
496 ...privateStaleCache,
497 resources: resources.map(projectResource)
498 }, context.serverInfo)
499 return yield* decodeListResourcesResult(result)
500 }, Effect.mapError(projectError)),
501 "resources/templates/list": Effect.fnUntraced(function*(
502 _request: typeof McpSchema.ListResourceTemplates.payloadSchema.Type
503 ) {
504 const invocation = yield* getInvocation
505 const resourceTemplates = yield* core.resources.listTemplates(invocation.protocol)
506 const result = yield* projectCompleteResult({
507 ...privateStaleCache,
508 resourceTemplates: resourceTemplates.map(projectResourceTemplate)
509 }, context.serverInfo)
510 return yield* decodeListResourceTemplatesResult(result)
511 }, Effect.mapError(projectError)),
512 "resources/read": Effect.fnUntraced(function*(request: typeof McpSchema.ReadResource.payloadSchema.Type) {
513 const invocation = yield* getInputInvocation(request)
514 const read = yield* core.resources.read(request.uri, invocation)
515 const contents = read.contents.map(projectResourceContents)
516 const result = yield* projectCompleteResult({
517 ...privateStaleCache,
518 contents,
519 _meta: read._meta
520 }, context.serverInfo)
521 return yield* decodeReadResourceResult(result)
522 }, Effect.mapError(projectError)),
523 "prompts/list": Effect.fnUntraced(function*(
524 _request: typeof McpSchema.ListPrompts.payloadSchema.Type
525 ) {
526 const presence = yield* context.registrationPresence
527 if (!presence.prompts) {
528 return yield* failWithHttpStatus(
529 404,
530 new McpProtocol.ProtocolError({
531 code: PublicMcpSchema.METHOD_NOT_FOUND_ERROR_CODE,
532 message: "Method not found"
533 })
534 )
535 }
536 const invocation = yield* getInvocation
537 const prompts = yield* core.prompts.list(invocation.protocol)
538 const projectedPrompts = yield* Effect.forEach(prompts, projectPrompt)
539 const result = yield* projectCompleteResult({
540 ...privateStaleCache,
541 prompts: projectedPrompts
542 }, context.serverInfo)
543 return yield* decodeListPromptsResult(result)
544 }, Effect.mapError(projectError)),
545 "prompts/get": Effect.fnUntraced(function*(request: typeof McpSchema.GetPrompt.payloadSchema.Type) {
546 const invocation = yield* getInputInvocation(request)
547 const outcome = yield* core.prompts.get(request.name, request.arguments ?? {}, invocation)
548 if (outcome._tag === "InputRequired") {
549 yield* validateInputRequestCapabilities(outcome, invocation.protocol.clientCapabilities)
550 const encodedServerInfo = yield* encodeImplementation(context.serverInfo)
551 return yield* decodePromptOutcome(projectInputRequired(outcome, encodedServerInfo))
552 }
553 const prompt = outcome.value
554 const messages = prompt.messages.map((message) => ({
555 role: message.role,
556 content: projectContent(message.content)
557 }))
558 const result = yield* projectCompleteResult({
559 description: prompt.description,
560 messages,
561 _meta: prompt._meta
562 }, context.serverInfo)
563 return yield* decodeGetPromptResult(result)
564 }, Effect.mapError(projectError)),
565 "completion/complete": Effect.fnUntraced(function*(
566 request: typeof McpSchema.Complete.payloadSchema.Type
567 ) {
568 const invocation = yield* getInvocation
569 const completion = yield* core.completions.complete({
570 reference: request.ref.type === "ref/prompt"
571 ? { type: "prompt", name: request.ref.name, title: request.ref.title }
572 : { type: "resourceTemplate", uriTemplate: request.ref.uri },
573 argument: request.argument,
574 context: request.context?.arguments === undefined
575 ? undefined
576 : { arguments: request.context.arguments },
577 metadata: request._meta
578 }, invocation)
579 const result = yield* projectCompleteResult({
580 completion: omitUndefined({
581 values: Array.from(completion.values),
582 total: completion.total,
583 hasMore: completion.hasMore
584 }),
585 _meta: completion.metadata
586 }, context.serverInfo)
587 return yield* decodeCompleteResult(result)
588 }, Effect.mapError(projectError)),
589 "tools/list": Effect.fnUntraced(function*(
590 _request: typeof McpSchema.ListTools.payloadSchema.Type
591 ) {
592 const invocation = yield* getInvocation
593 const tools = yield* core.tools.list(invocation.protocol)
594 const projectedTools = yield* Effect.forEach(tools, projectTool)
595 const result = yield* projectCompleteResult({
596 ...privateStaleCache,
597 tools: projectedTools
598 }, context.serverInfo)
599 return yield* decodeListToolsResult(result)
600 }, Effect.mapError(projectError)),
601 "tools/call": Effect.fnUntraced(function*(request: typeof McpSchema.CallTool.payloadSchema.Type) {
602 const invocation = yield* getInputInvocation(request)
603 const tool = (yield* core.tools.list(invocation.protocol)).find((tool) => tool.name === request.name)
604 const httpRequest = yield* Effect.serviceOption(HttpServerRequest.HttpServerRequest)
605
606 if (tool !== undefined && Option.isSome(httpRequest)) {
607 const pending: Array<{ readonly schema: unknown; readonly argument: unknown; readonly path: string }> = [
608 { schema: tool.inputSchema, argument: request.arguments, path: "" }
609 ]
610 // Only properties paths may mirror headers; composition, arrays, and references do not qualify.
611 while (pending.length > 0) {
612 const { schema, argument, path } = pending.pop()!
613 if (!Predicate.isReadonlyObject(schema)) continue
614 const annotation = schema["x-mcp-header"]
615 if (typeof annotation === "string") {
616 const headerName = `mcp-param-${annotation.toLowerCase()}`
617 if (!parameterHeaderMatches(httpRequest.value.headers[headerName], argument)) {
618 return yield* failWithHttpStatus(
619 400,
620 new McpProtocol.ProtocolError({
621 code: PublicMcpSchema.HEADER_MISMATCH_ERROR_CODE,
622 message: `${headerName} does not match argument '${path}'`
623 })
624 )
625 }
626 }
627 if (Predicate.isReadonlyObject(schema.properties)) {
628 for (const [name, property] of Object.entries(schema.properties)) {
629 pending.push({
630 schema: property,
631 argument: Predicate.isReadonlyObject(argument) && Object.hasOwn(argument, name)
632 ? argument[name]
633 : undefined,
634 path: path === "" ? name : `${path}.${name}`
635 })
636 }
637 }
638 }
639 }
640
641 const outcome = yield* core.tools.call({ name: request.name, arguments: request.arguments ?? {} }, invocation)
642 .pipe(
643 Effect.catchTag(
644 ["ToolExecutionError", "InvalidToolInput"],
645 (error) =>
646 Effect.succeed(McpCore.OperationOutcome.Complete(PublicMcpSchema.CallToolResult.make({
647 content: [PublicMcpSchema.TextContent.make({ type: "text", text: error.message })],
648 isError: true
649 })))
650 )
651 )
652 if (outcome._tag === "InputRequired") {
653 yield* validateInputRequestCapabilities(outcome, invocation.protocol.clientCapabilities)
654 return yield* projectCallToolOutcome(outcome, context.serverInfo)
655 }
656 const toolResult = outcome.value
657 const content = toolResult.content.map(projectContent)
658 return yield* projectCallToolOutcome(
659 McpCore.OperationOutcome.Complete({
660 content,
661 structuredContent: toolResult.structuredContent,
662 isError: toolResult.isError,
663 _meta: toolResult._meta
664 }),
665 context.serverInfo
666 )
667 }, Effect.mapError(projectError)),
668 "notifications/cancelled": Effect.fnUntraced(function*(
669 _request: typeof McpSchema.CancelledNotification.payloadSchema.Type
670 ) {
671 return yield* Effect.void
672 })
673 })
674}
675
676const runtime = {
677 _tag: "Stateless",
678 transport: {
679 jsonRpc: { acceptsBatches: false },
680 http: {}
681 },
682 profileFromRequestMetadata
683} as const
684
685/**
686 * @internal
687 */
688export const protocol = McpProtocol.make({
689 protocolVersion: McpSchema.protocolVersion,
690 runtime,
691 clientRpcs: McpSchema.ClientRpcs,
692 clientNotificationRpcs: McpSchema.ClientNotificationRpcs,
693 serverRequestRpcs: McpSchema.ServerRequestRpcs,
694 serverNotificationRpcs: McpSchema.ServerNotificationRpcs,
695 handlerRpcs,
696 makeHandlers,
697 toReverseClient: () => ({
698 listRoots: () => Effect.fail(unsupported("roots/list")),
699 createMessage: () => Effect.fail(unsupported("sampling/createMessage")),
700 elicit: () => Effect.fail(unsupported("elicitation/create"))
701 }),
702 projectNotification: (notification) =>
703 McpProtocol.isSubscriptionServerNotification(notification)
704 // @effect-diagnostics-next-line effectSucceedWithVoid:off the projector requires the value `undefined`
705 ? Effect.succeed(undefined)
706 : McpProtocol.makeNotificationProjector({
707 supportsProgressMessage: true
708 }, notification),
709 normalizeCancellation
710})