| 1 | /** |
| 2 | * MCP v2026-07-28 wire projections. |
| 3 | * |
| 4 | * @internal |
| 5 | */ |
| 6 | import * as Deferred from "../../../Deferred.ts" |
| 7 | import * as Effect from "../../../Effect.ts" |
| 8 | import * as Base64 from "../../../encoding/Base64.ts" |
| 9 | import { appendPreResponseHandlerUnsafe } from "../../../http/HttpEffect.ts" |
| 10 | import * as HttpServerRequest from "../../../http/HttpServerRequest.ts" |
| 11 | import * as HttpServerResponse from "../../../http/HttpServerResponse.ts" |
| 12 | import * as Match from "../../../Match.ts" |
| 13 | import * as Option from "../../../Option.ts" |
| 14 | import * as Predicate from "../../../Predicate.ts" |
| 15 | import * as PubSub from "../../../PubSub.ts" |
| 16 | import * as Queue from "../../../Queue.ts" |
| 17 | import * as Record from "../../../Record.ts" |
| 18 | import type * as Rpc from "../../../rpc/Rpc.ts" |
| 19 | import type * as RpcMessage from "../../../rpc/RpcMessage.ts" |
| 20 | import * as Schema from "../../../Schema.ts" |
| 21 | import * as Scope from "../../../Scope.ts" |
| 22 | import type * as Types from "../../../Types.ts" |
| 23 | import type * as PublicMcpProtocol from "../../McpProtocol.ts" |
| 24 | import * as PublicMcpSchema from "../../McpSchema.ts" |
| 25 | import * as McpCore from "../mcpCore.ts" |
| 26 | import * as McpProtocol from "../mcpProtocol.ts" |
| 27 | import * as McpSchema from "../mcpSchema/v2026_07_28.ts" |
| 28 | |
| 29 | const InputResponses = Schema.Record(Schema.String, Schema.JsonObject) |
| 30 | const decodeRequestMetadata = Schema.decodeUnknownEffect(McpSchema.RequestMetaObject) |
| 31 | const decodeCancellation = Schema.decodeUnknownEffect(McpSchema.CancelledNotification.payloadSchema) |
| 32 | const isJson = Schema.is(Schema.Json) |
| 33 | const encodeImplementation = Schema.encodeEffect(McpSchema.Implementation) |
| 34 | const MAX_PENDING_SUBSCRIPTION_NOTIFICATIONS = 64 |
| 35 | |
| 36 | type ObjectWithUndefined = Readonly<Record<string, unknown>> |
| 37 | |
| 38 | const 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 | |
| 48 | const 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 | */ |
| 73 | export 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 | */ |
| 83 | export 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 | |
| 93 | const 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 | |
| 104 | const 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 | |
| 115 | const decodeCallToolOutcome = Schema.decodeUnknownEffect(Schema.Union([ |
| 116 | McpSchema.CallToolResult, |
| 117 | McpSchema.InputRequiredResult |
| 118 | ])) |
| 119 | |
| 120 | /** |
| 121 | * @internal |
| 122 | */ |
| 123 | export 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 | |
| 138 | const 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 | |
| 175 | const 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 | |
| 185 | const 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 | |
| 205 | const projectResource = (resource: PublicMcpSchema.Resource) => McpSchema.Resource.make(resource) |
| 206 | const projectResourceTemplate = (resourceTemplate: PublicMcpSchema.ResourceTemplate) => |
| 207 | McpSchema.ResourceTemplate.make(resourceTemplate) |
| 208 | |
| 209 | const projectPrompt = (prompt: PublicMcpSchema.Prompt) => |
| 210 | McpProtocol.transcode(PublicMcpSchema.Prompt, McpSchema.Prompt, prompt) |
| 211 | |
| 212 | const projectTool = (tool: PublicMcpSchema.Tool) => McpProtocol.transcode(PublicMcpSchema.Tool, McpSchema.Tool, tool) |
| 213 | |
| 214 | const privateStaleCache = { |
| 215 | ttlMs: 0, |
| 216 | cacheScope: "private" |
| 217 | } satisfies { readonly ttlMs: number; readonly cacheScope: "private" } |
| 218 | |
| 219 | const 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 | |
| 231 | type ProtocolError = |
| 232 | | McpCore.ResourceNotFound |
| 233 | | McpCore.PromptNotFound |
| 234 | | McpCore.ToolError |
| 235 | | McpCore.UnsupportedByProtocol |
| 236 | | McpProtocol.ProtocolError |
| 237 | | PublicMcpSchema.McpError |
| 238 | | Schema.SchemaError |
| 239 | |
| 240 | const 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 | |
| 258 | const 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 | |
| 267 | const 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 | */ |
| 281 | export 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 | |
| 290 | const 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 | */ |
| 302 | export const handlerRpcs = McpSchema.ClientRequestRpcs.merge(McpSchema.ClientNotificationRpcs) |
| 303 | |
| 304 | /** |
| 305 | * @internal |
| 306 | */ |
| 307 | export 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 | |
| 676 | const runtime = { |
| 677 | _tag: "Stateless", |
| 678 | transport: { |
| 679 | jsonRpc: { acceptsBatches: false }, |
| 680 | http: {} |
| 681 | }, |
| 682 | profileFromRequestMetadata |
| 683 | } as const |
| 684 | |
| 685 | /** |
| 686 | * @internal |
| 687 | */ |
| 688 | export 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 | }) |