| 1 | /** |
| 2 | * Runtime descriptors for MCP protocol adapters. |
| 3 | * |
| 4 | * @internal |
| 5 | */ |
| 6 | import type { NonEmptyReadonlyArray } from "../../Array.ts" |
| 7 | import * as Cause from "../../Cause.ts" |
| 8 | import * as Context from "../../Context.ts" |
| 9 | import * as Effect from "../../Effect.ts" |
| 10 | import type * as Headers from "../../http/Headers.ts" |
| 11 | import { appendPreResponseHandlerUnsafe } from "../../http/HttpEffect.ts" |
| 12 | import * as HttpServerRequest from "../../http/HttpServerRequest.ts" |
| 13 | import * as HttpServerResponse from "../../http/HttpServerResponse.ts" |
| 14 | import * as Layer from "../../Layer.ts" |
| 15 | import type * as LogLevel from "../../LogLevel.ts" |
| 16 | import * as Predicate from "../../Predicate.ts" |
| 17 | import * as Result from "../../Result.ts" |
| 18 | import * as RpcGroup from "../../rpc/RpcGroup.ts" |
| 19 | import type * as RpcMessage from "../../rpc/RpcMessage.ts" |
| 20 | import type * as PublicMcpProtocol from "../McpProtocol.ts" |
| 21 | import * as PublicMcpSchema from "../McpSchema.ts" |
| 22 | import type * as McpCore from "./mcpCore.ts" |
| 23 | import * as McpProtocol from "./mcpProtocol.ts" |
| 24 | import * as McpProtocolRegistry from "./mcpProtocolRegistry.ts" |
| 25 | import * as McpStatefulRuntime from "./mcpStatefulRuntime.ts" |
| 26 | |
| 27 | const MCP_SESSION_ID_HEADER = "mcp-session-id" |
| 28 | const MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" |
| 29 | const MCP_METHOD_HEADER = "mcp-method" |
| 30 | const MCP_NAME_HEADER = "mcp-name" |
| 31 | const PROTOCOL_VERSION_METADATA_KEY = "io.modelcontextprotocol/protocolVersion" |
| 32 | const CLIENT_CAPABILITIES_METADATA_KEY = "io.modelcontextprotocol/clientCapabilities" |
| 33 | const UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE = -32022 |
| 34 | |
| 35 | const asRecord = (input: unknown): Record<string, unknown> | undefined => |
| 36 | typeof input === "object" && input !== null ? input as Record<string, unknown> : undefined |
| 37 | |
| 38 | const protocolVersionClaim = (input: unknown): { readonly present: boolean; readonly value: unknown } => { |
| 39 | const metadata = asRecord(input) |
| 40 | return metadata !== undefined && PROTOCOL_VERSION_METADATA_KEY in metadata |
| 41 | ? { present: true, value: metadata[PROTOCOL_VERSION_METADATA_KEY] } |
| 42 | : { present: false, value: undefined } |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * @internal |
| 47 | */ |
| 48 | export const hasRequestProtocolVersion = (input: unknown): boolean => |
| 49 | protocolVersionClaim(asRecord(asRecord(input)?.params)?._meta).present |
| 50 | |
| 51 | const routingNameKey = (method: string): "name" | "uri" | undefined => { |
| 52 | switch (method) { |
| 53 | case "tools/call": |
| 54 | case "prompts/get": |
| 55 | return "name" |
| 56 | case "resources/read": |
| 57 | return "uri" |
| 58 | default: |
| 59 | return undefined |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | const headerMismatch = (message: string): HttpProtocolSelection => ({ |
| 64 | _tag: "Rejected", |
| 65 | status: 400, |
| 66 | error: { code: PublicMcpSchema.HEADER_MISMATCH_ERROR_CODE, message } |
| 67 | }) |
| 68 | |
| 69 | const PingRpcs = RpcGroup.make(PublicMcpSchema.Ping) |
| 70 | |
| 71 | /** |
| 72 | * @internal |
| 73 | */ |
| 74 | export interface RequestBinding { |
| 75 | readonly initializePayload: typeof PublicMcpSchema.Initialize.payloadSchema.Type |
| 76 | readonly negotiatedProfile: McpCore.NegotiatedProtocolProfile |
| 77 | readonly protocol: PublicMcpProtocol.AnyProtocolAdapter |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * @internal |
| 82 | */ |
| 83 | export interface PreparedRequest { |
| 84 | readonly protocol: PublicMcpProtocol.AnyProtocolAdapter |
| 85 | readonly binding?: RequestBinding | undefined |
| 86 | readonly profile?: McpCore.NegotiatedProtocolProfile<string> | undefined |
| 87 | readonly requestContext?: PublicMcpSchema.McpRequestContext["Service"] | undefined |
| 88 | } |
| 89 | |
| 90 | type HttpProtocolSelection = |
| 91 | | { |
| 92 | readonly _tag: "Accepted" |
| 93 | readonly binding: RequestBinding | undefined |
| 94 | readonly protocol?: PublicMcpProtocol.AnyProtocolAdapter | undefined |
| 95 | } |
| 96 | | { |
| 97 | readonly _tag: "Rejected" |
| 98 | readonly status: 400 | 404 |
| 99 | readonly error?: { |
| 100 | readonly code: number |
| 101 | readonly message: string |
| 102 | readonly data?: unknown |
| 103 | } | undefined |
| 104 | } |
| 105 | |
| 106 | /** |
| 107 | * @internal |
| 108 | */ |
| 109 | export type HttpAdmission = |
| 110 | | { |
| 111 | readonly _tag: "Accepted" |
| 112 | readonly acknowledge: boolean |
| 113 | readonly isSubscription: boolean |
| 114 | } |
| 115 | | { |
| 116 | readonly _tag: "Rejected" |
| 117 | readonly response: HttpServerResponse.HttpServerResponse |
| 118 | } |
| 119 | |
| 120 | /** |
| 121 | * @internal |
| 122 | */ |
| 123 | export interface HandlerInstallationOptions { |
| 124 | readonly core: McpCore.McpCore |
| 125 | readonly subscribeServerNotifications: McpProtocol.HandlerInstallationContext["subscribeServerNotifications"] |
| 126 | readonly sendNotification?: NonNullable<McpProtocol.HandlerInstallationContext["sendNotification"]> |
| 127 | readonly markSubscriptionCancelled?: NonNullable< |
| 128 | McpProtocol.HandlerInstallationContext["markSubscriptionCancelled"] |
| 129 | > |
| 130 | readonly terminateSubscription?: NonNullable<McpProtocol.HandlerInstallationContext["terminateSubscription"]> |
| 131 | readonly defaultLogLevel: LogLevel.LogLevel |
| 132 | readonly serverInfo: { |
| 133 | readonly name: string |
| 134 | readonly version: string |
| 135 | readonly instructions?: string | undefined |
| 136 | readonly description?: string | undefined |
| 137 | readonly websiteUrl?: string | undefined |
| 138 | readonly icons?: ReadonlyArray<PublicMcpSchema.Icon> | undefined |
| 139 | readonly extensions?: NonNullable<PublicMcpSchema.ServerCapabilities["extensions"]> | undefined |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * @internal |
| 145 | */ |
| 146 | export const stateful = ( |
| 147 | transport: PublicMcpProtocol.TransportPolicy |
| 148 | ): PublicMcpProtocol.StatefulRuntimeDescriptor => ({ |
| 149 | _tag: "Stateful", |
| 150 | transport |
| 151 | }) |
| 152 | |
| 153 | /** |
| 154 | * @internal |
| 155 | */ |
| 156 | export interface ServerRuntimeShape { |
| 157 | readonly protocols: NonEmptyReadonlyArray<PublicMcpProtocol.AnyProtocolAdapter> |
| 158 | readonly clientRpcs: McpProtocolRegistry.ProtocolRegistry<PublicMcpProtocol.AnyProtocolAdapter>["clientRpcs"] |
| 159 | readonly selectProtocol: (offeredVersion: string) => PublicMcpProtocol.AnyProtocolAdapter |
| 160 | readonly protocolForInternalTag: (tag: string) => PublicMcpProtocol.AnyProtocolAdapter |
| 161 | readonly routeClientRequest: ( |
| 162 | protocol: PublicMcpProtocol.AnyProtocolAdapter, |
| 163 | request: RpcMessage.RequestEncoded |
| 164 | ) => RpcMessage.RequestEncoded |
| 165 | readonly prepareRequest: ( |
| 166 | clientId: number, |
| 167 | headers: Headers.Headers, |
| 168 | request: RpcMessage.RequestEncoded |
| 169 | ) => Effect.Effect<PreparedRequest, unknown> |
| 170 | readonly resolveRequest: (clientId: number, headers: Headers.Headers) => RequestBinding | undefined |
| 171 | readonly admitHttp: (headers: Headers.Headers, input: Result.Result<unknown, unknown>) => HttpAdmission |
| 172 | readonly effectLogLevel: ( |
| 173 | clientId: number, |
| 174 | headers: Headers.Headers, |
| 175 | fallback: LogLevel.LogLevel |
| 176 | ) => LogLevel.LogLevel |
| 177 | readonly disconnect: (clientId: number) => void |
| 178 | readonly deliveryClientIds: () => Iterable<number> |
| 179 | readonly canDeliver: ( |
| 180 | clientId: number, |
| 181 | headers: Headers.Headers, |
| 182 | notification: McpCore.ServerNotification, |
| 183 | fallbackLogLevel: LogLevel.LogLevel |
| 184 | ) => boolean |
| 185 | readonly installHandlers: ( |
| 186 | options: HandlerInstallationOptions |
| 187 | ) => Effect.Effect<Context.Context<never>, never, unknown> |
| 188 | } |
| 189 | |
| 190 | /** |
| 191 | * @internal |
| 192 | */ |
| 193 | export class ServerRuntime extends Context.Service<ServerRuntime, ServerRuntimeShape>()( |
| 194 | "effect/ai/McpRuntime/ServerRuntime" |
| 195 | ) {} |
| 196 | |
| 197 | /** |
| 198 | * @internal |
| 199 | */ |
| 200 | export const selectStatefulProtocol = <Protocol extends PublicMcpProtocol.AnyProtocolAdapter>( |
| 201 | protocols: ReadonlyArray<Protocol>, |
| 202 | offeredVersion: unknown |
| 203 | ): Protocol | undefined => |
| 204 | protocols.find((protocol) => protocol.runtime._tag === "Stateful" && protocol.protocolVersion === offeredVersion) ?? |
| 205 | protocols.find((protocol) => protocol.runtime._tag === "Stateful") |
| 206 | |
| 207 | /** |
| 208 | * @internal |
| 209 | */ |
| 210 | export const make = Effect.fnUntraced(function*( |
| 211 | protocols: NonEmptyReadonlyArray<PublicMcpProtocol.AnyProtocolAdapter> |
| 212 | ) { |
| 213 | const statefulProtocol = protocols.find((protocol) => protocol.runtime._tag === "Stateful") |
| 214 | const stateful = statefulProtocol === undefined ? undefined : McpStatefulRuntime.make() |
| 215 | const protocolVersions = protocols.map((protocol) => protocol.protocolVersion) |
| 216 | let statelessDescriptor: PublicMcpProtocol.StatelessRuntimeDescriptor | undefined |
| 217 | let statelessProtocol: PublicMcpProtocol.AnyProtocolAdapter | undefined |
| 218 | for (const protocol of protocols) { |
| 219 | if (protocol.runtime._tag !== "Stateless") { |
| 220 | continue |
| 221 | } |
| 222 | if (statelessDescriptor !== undefined) { |
| 223 | return yield* new Cause.IllegalArgumentError( |
| 224 | "MCP runtime supports at most one stateless protocol" |
| 225 | ) |
| 226 | } |
| 227 | statelessDescriptor = protocol.runtime |
| 228 | statelessProtocol = protocol |
| 229 | } |
| 230 | const registry = yield* McpProtocolRegistry.make(protocols) |
| 231 | const selectHttpProtocol = (headers: Headers.Headers, input: unknown): HttpProtocolSelection => { |
| 232 | const protocolVersion = headers[MCP_PROTOCOL_VERSION_HEADER] |
| 233 | const sessionId = headers[MCP_SESSION_ID_HEADER] |
| 234 | const inputRecord = asRecord(input) |
| 235 | const metadata = asRecord(asRecord(inputRecord?.params)?._meta) |
| 236 | const claim = protocolVersionClaim(metadata) |
| 237 | const id = inputRecord?.id |
| 238 | const isInitialize = inputRecord?.jsonrpc === "2.0" && inputRecord.method === "initialize" && |
| 239 | (typeof id === "string" || typeof id === "number") |
| 240 | const isCancellationNotification = inputRecord?.jsonrpc === "2.0" && |
| 241 | inputRecord.method === "notifications/cancelled" && id === undefined |
| 242 | const isStatelessRequest = claim.present || ((!isInitialize || stateful === undefined) && |
| 243 | statelessProtocol !== undefined && |
| 244 | (protocolVersion === statelessProtocol.protocolVersion || |
| 245 | (stateful === undefined && inputRecord?.jsonrpc === "2.0" && |
| 246 | typeof inputRecord.method === "string" && (typeof id === "string" || typeof id === "number")))) |
| 247 | if (isStatelessRequest) { |
| 248 | if (protocolVersion === undefined) { |
| 249 | return headerMismatch("MCP-Protocol-Version header is required") |
| 250 | } |
| 251 | if (!isCancellationNotification && (metadata === undefined || typeof claim.value !== "string")) { |
| 252 | return { |
| 253 | _tag: "Rejected", |
| 254 | status: 400, |
| 255 | error: { |
| 256 | code: PublicMcpSchema.INVALID_PARAMS_ERROR_CODE, |
| 257 | message: "Required request metadata is missing" |
| 258 | } |
| 259 | } |
| 260 | } |
| 261 | if ((!isCancellationNotification || claim.present) && claim.value !== protocolVersion) { |
| 262 | return headerMismatch("MCP-Protocol-Version header does not match request metadata") |
| 263 | } |
| 264 | if (!isCancellationNotification && asRecord(metadata?.[CLIENT_CAPABILITIES_METADATA_KEY]) === undefined) { |
| 265 | return { |
| 266 | _tag: "Rejected", |
| 267 | status: 400, |
| 268 | error: { |
| 269 | code: PublicMcpSchema.INVALID_PARAMS_ERROR_CODE, |
| 270 | message: `${CLIENT_CAPABILITIES_METADATA_KEY} request metadata is required` |
| 271 | } |
| 272 | } |
| 273 | } |
| 274 | if (statelessProtocol === undefined || protocolVersion !== statelessProtocol.protocolVersion) { |
| 275 | return { |
| 276 | _tag: "Rejected", |
| 277 | status: 400, |
| 278 | error: { |
| 279 | code: UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE, |
| 280 | message: `Unsupported protocol version '${protocolVersion}'`, |
| 281 | data: { |
| 282 | supported: protocolVersions, |
| 283 | requested: protocolVersion |
| 284 | } |
| 285 | } |
| 286 | } |
| 287 | } |
| 288 | const method = inputRecord?.method |
| 289 | if (typeof method !== "string" || headers[MCP_METHOD_HEADER] !== method) { |
| 290 | return headerMismatch("Mcp-Method header does not match request method") |
| 291 | } |
| 292 | const nameKey = routingNameKey(method) |
| 293 | if (nameKey !== undefined) { |
| 294 | const name = asRecord(inputRecord?.params)?.[nameKey] |
| 295 | const header = headers[MCP_NAME_HEADER] |
| 296 | if (typeof name !== "string" || header === undefined || McpProtocol.decodeRoutingHeader(header) !== name) { |
| 297 | return headerMismatch("Mcp-Name header does not match request parameters") |
| 298 | } |
| 299 | } |
| 300 | return { _tag: "Accepted", binding: undefined, protocol: statelessProtocol } |
| 301 | } |
| 302 | const binding = sessionId === undefined ? undefined : stateful?.resolveSessionId(sessionId) |
| 303 | if (sessionId !== undefined && binding === undefined) { |
| 304 | return { _tag: "Rejected", status: 404 } |
| 305 | } |
| 306 | if ( |
| 307 | !isInitialize && |
| 308 | protocolVersion !== undefined && |
| 309 | !registry.protocols.some((protocol) => protocol.protocolVersion === protocolVersion) |
| 310 | ) { |
| 311 | return { _tag: "Rejected", status: 400 } |
| 312 | } |
| 313 | if ( |
| 314 | !isInitialize && |
| 315 | binding?.protocol.runtime.transport.http.requiresVersionHeader === true && |
| 316 | protocolVersion !== binding.protocol.protocolVersion |
| 317 | ) { |
| 318 | return { _tag: "Rejected", status: 400 } |
| 319 | } |
| 320 | return { _tag: "Accepted", binding, protocol: binding?.protocol } |
| 321 | } |
| 322 | return ServerRuntime.of({ |
| 323 | protocols: registry.protocols, |
| 324 | clientRpcs: registry.clientRpcs, |
| 325 | selectProtocol: registry.select, |
| 326 | protocolForInternalTag: registry.protocolForInternalTag, |
| 327 | routeClientRequest: registry.routeClientRequest, |
| 328 | prepareRequest: Effect.fnUntraced(function*(clientId, headers, request) { |
| 329 | const metadata = asRecord(request.payload)?._meta |
| 330 | const claim = protocolVersionClaim(metadata) |
| 331 | const requestedVersion = typeof claim.value === "string" ? claim.value : undefined |
| 332 | const binding = claim.present ? undefined : stateful?.resolve(clientId, headers) |
| 333 | let protocol: PublicMcpProtocol.AnyProtocolAdapter |
| 334 | if (claim.present) { |
| 335 | protocol = registry.protocols.find((protocol) => protocol.protocolVersion === requestedVersion) ?? |
| 336 | statelessProtocol ?? registry.protocols[0] |
| 337 | } else if (binding !== undefined) { |
| 338 | protocol = binding.protocol |
| 339 | } else if (request.tag === "initialize") { |
| 340 | const offeredVersion = (request.payload as any)?.protocolVersion |
| 341 | const selected = selectStatefulProtocol(registry.protocols, offeredVersion) |
| 342 | if (selected === undefined) { |
| 343 | return yield* new McpProtocol.ProtocolError({ |
| 344 | code: UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE, |
| 345 | message: `initialize is not supported by the configured MCP protocols (requested '${offeredVersion}')` |
| 346 | }) |
| 347 | } |
| 348 | protocol = selected |
| 349 | } else if (request.tag === "ping") { |
| 350 | // An unversioned ping may precede initialize, even with a stateless adapter first. |
| 351 | protocol = selectStatefulProtocol(registry.protocols, undefined) ?? registry.protocols[0] |
| 352 | } else { |
| 353 | protocol = registry.protocols[0] |
| 354 | } |
| 355 | if (protocol.runtime._tag === "Stateful") { |
| 356 | return { protocol, binding } |
| 357 | } |
| 358 | if (request.isNotification && request.tag === "notifications/cancelled") { |
| 359 | return { protocol } |
| 360 | } |
| 361 | if (statelessDescriptor === undefined) { |
| 362 | return yield* Effect.die("MCP stateless runtime invariant failed") |
| 363 | } |
| 364 | if (requestedVersion !== undefined && requestedVersion !== protocol.protocolVersion) { |
| 365 | return yield* new McpProtocol.ProtocolError({ |
| 366 | code: UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE, |
| 367 | message: `Unsupported protocol version '${requestedVersion}'`, |
| 368 | data: { supported: protocolVersions, requested: requestedVersion } |
| 369 | }) |
| 370 | } |
| 371 | const decodedProfile = yield* statelessDescriptor.profileFromRequestMetadata(metadata) |
| 372 | const profile: McpCore.NegotiatedProtocolProfile<string> = { |
| 373 | protocolVersion: decodedProfile.protocolVersion, |
| 374 | clientCapabilities: decodedProfile.clientCapabilities, |
| 375 | clientInfo: decodedProfile.clientInfo, |
| 376 | requestMetadata: decodedProfile.requestMetadata |
| 377 | } |
| 378 | const requestContext = PublicMcpSchema.McpRequestContext.of({ |
| 379 | clientId, |
| 380 | protocolVersion: profile.protocolVersion, |
| 381 | clientCapabilities: profile.clientCapabilities, |
| 382 | clientInfo: profile.clientInfo, |
| 383 | requestMetadata: profile.requestMetadata |
| 384 | }) |
| 385 | return { protocol, profile, requestContext } |
| 386 | }), |
| 387 | resolveRequest: (clientId, headers) => stateful?.resolve(clientId, headers), |
| 388 | admitHttp: (headers, parsed) => { |
| 389 | const reject = (status: number, error?: unknown, id: string | number | null = null): HttpAdmission => ({ |
| 390 | _tag: "Rejected", |
| 391 | response: error === undefined |
| 392 | ? HttpServerResponse.empty({ status }) |
| 393 | : HttpServerResponse.jsonUnsafe({ jsonrpc: "2.0", id, error }, { status }) |
| 394 | }) |
| 395 | if (Result.isFailure(parsed)) { |
| 396 | const version = headers[MCP_PROTOCOL_VERSION_HEADER] |
| 397 | if (version !== undefined && !registry.protocols.some((protocol) => protocol.protocolVersion === version)) { |
| 398 | return reject(400) |
| 399 | } |
| 400 | const admission = selectHttpProtocol(headers, undefined) |
| 401 | return admission._tag === "Rejected" && admission.error === undefined |
| 402 | ? reject(admission.status) |
| 403 | : reject(200, new PublicMcpSchema.ParseError({ message: "Parse error" })) |
| 404 | } |
| 405 | const input = parsed.success |
| 406 | if (Array.isArray(input)) { |
| 407 | if (input.length === 0) { |
| 408 | return reject(400, new PublicMcpSchema.InvalidRequest({ message: "Invalid Request" })) |
| 409 | } |
| 410 | const admission = selectHttpProtocol(headers, input) |
| 411 | if ( |
| 412 | admission._tag === "Rejected" || |
| 413 | input.some(hasRequestProtocolVersion) || |
| 414 | input.some((message) => Predicate.hasProperty(message, "method") && message.method === "initialize") || |
| 415 | admission.binding?.protocol.runtime.transport.jsonRpc.acceptsBatches !== true |
| 416 | ) { |
| 417 | return reject(400) |
| 418 | } |
| 419 | return { |
| 420 | _tag: "Accepted", |
| 421 | acknowledge: !input.some((message) => |
| 422 | Predicate.hasProperty(message, "method") && Predicate.hasProperty(message, "id") |
| 423 | ), |
| 424 | isSubscription: false |
| 425 | } |
| 426 | } |
| 427 | const hasId = Predicate.hasProperty(input, "id") |
| 428 | const id = hasId && (typeof input.id === "string" || typeof input.id === "number") ? input.id : null |
| 429 | const isJsonRpc = Predicate.hasProperty(input, "jsonrpc") && input.jsonrpc === "2.0" |
| 430 | const hasValidRequestId = !hasId || typeof input.id === "string" || typeof input.id === "number" |
| 431 | const isRequest = isJsonRpc && hasValidRequestId && |
| 432 | Predicate.hasProperty(input, "method") && typeof input.method === "string" |
| 433 | const hasValidResponseId = hasId && |
| 434 | (typeof input.id === "string" || typeof input.id === "number" || input.id === null) |
| 435 | const hasResult = Predicate.hasProperty(input, "result") |
| 436 | const hasError = Predicate.hasProperty(input, "error") |
| 437 | const isResponse = isJsonRpc && hasValidResponseId && hasResult !== hasError |
| 438 | const admission = selectHttpProtocol(headers, input) |
| 439 | if (admission._tag === "Rejected") { |
| 440 | return reject(admission.status, admission.error, id) |
| 441 | } |
| 442 | if (!isRequest && !isResponse) { |
| 443 | return reject(200, new PublicMcpSchema.InvalidRequest({ message: "Invalid Request" }), id) |
| 444 | } |
| 445 | const isInitialize = Predicate.hasProperty(input, "method") && input.method === "initialize" |
| 446 | const hasSession = headers[MCP_SESSION_ID_HEADER] !== undefined |
| 447 | if (isInitialize ? hasSession : !hasSession && admission.protocol?.runtime._tag !== "Stateless") { |
| 448 | return reject(400) |
| 449 | } |
| 450 | if ( |
| 451 | isRequest && admission.protocol?.runtime._tag === "Stateless" && |
| 452 | !((admission.protocol as unknown as McpProtocol.ProtocolAdapter).handlerRpcs?.requests.has( |
| 453 | input.method as string |
| 454 | ) ?? |
| 455 | admission.protocol.clientRpcs.requests.has(input.method as string)) |
| 456 | ) { |
| 457 | return reject(404, new PublicMcpSchema.MethodNotFound({ message: `Method not found: ${input.method}` }), id) |
| 458 | } |
| 459 | return { |
| 460 | _tag: "Accepted", |
| 461 | acknowledge: !isRequest || !hasId, |
| 462 | isSubscription: Predicate.hasProperty(input, "method") && input.method === "subscriptions/listen" |
| 463 | } |
| 464 | }, |
| 465 | effectLogLevel: (clientId, headers, fallback) => stateful?.effectLogLevel(clientId, headers, fallback) ?? fallback, |
| 466 | disconnect: (clientId) => stateful?.disconnect(clientId), |
| 467 | deliveryClientIds: () => stateful?.initializedClientIds() ?? [], |
| 468 | canDeliver: (clientId, headers, notification, fallback) => |
| 469 | stateful?.canDeliver(clientId, headers, notification, fallback) ?? true, |
| 470 | installHandlers: Effect.fnUntraced(function*(options) { |
| 471 | const contextMap = new Map<string, unknown>() |
| 472 | const installationContext: McpProtocol.HandlerInstallationContext = { |
| 473 | subscribeServerNotifications: options.subscribeServerNotifications, |
| 474 | ...(options.sendNotification === undefined ? {} : { sendNotification: options.sendNotification }), |
| 475 | ...(options.markSubscriptionCancelled === undefined |
| 476 | ? {} |
| 477 | : { markSubscriptionCancelled: options.markSubscriptionCancelled }), |
| 478 | ...(options.terminateSubscription === undefined |
| 479 | ? {} |
| 480 | : { terminateSubscription: options.terminateSubscription }), |
| 481 | supportedVersions: protocolVersions, |
| 482 | serverInfo: options.serverInfo, |
| 483 | registrationPresence: options.core.registrationPresence |
| 484 | } |
| 485 | const handlerTarget = registry.handlerTarget(contextMap, installationContext) |
| 486 | for (const protocol of registry.protocols) { |
| 487 | if (protocol.runtime._tag === "Stateless") { |
| 488 | yield* protocol.installHandlers(options.core, undefined, handlerTarget) |
| 489 | continue |
| 490 | } |
| 491 | if (stateful === undefined) { |
| 492 | return yield* Effect.die("MCP sessionful runtime invariant failed") |
| 493 | } |
| 494 | yield* handlerTarget.install(protocol, PingRpcs, PingRpcs.of({ ping: () => Effect.succeed({}) })) |
| 495 | const lifecycle: McpProtocol.LifecycleRuntime = { |
| 496 | initialize: Effect.fnUntraced(function*(protocolVersion, profile, clientId) { |
| 497 | const presence = yield* options.core.registrationPresence |
| 498 | return yield* Effect.withFiber((fiber) => { |
| 499 | const httpRequest = Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest) |
| 500 | const capabilities: McpCore.CanonicalServerCapabilities = { |
| 501 | completions: true, |
| 502 | logging: true, |
| 503 | ...(presence.tools ? { tools: { listChanged: true } } : {}), |
| 504 | ...(presence.resources |
| 505 | ? { resources: { listChanged: true, subscribe: httpRequest === undefined } } |
| 506 | : {}), |
| 507 | ...(presence.prompts ? { prompts: { listChanged: true } } : {}), |
| 508 | ...(options.serverInfo.extensions ? { extensions: options.serverInfo.extensions } : {}) |
| 509 | } |
| 510 | const initializePayload = PublicMcpSchema.Initialize.payloadSchema.make({ |
| 511 | protocolVersion, |
| 512 | capabilities: profile.clientCapabilities, |
| 513 | clientInfo: profile.clientInfo!, |
| 514 | _meta: profile.requestMetadata |
| 515 | }) |
| 516 | const registration: McpStatefulRuntime.Registration = { |
| 517 | initializePayload, |
| 518 | negotiatedProfile: profile, |
| 519 | protocol: protocol as PublicMcpProtocol.ProtocolAdapter, |
| 520 | supportsResourceSubscriptions: httpRequest === undefined && |
| 521 | capabilities.resources?.subscribe === true, |
| 522 | logLevel: options.defaultLogLevel |
| 523 | } |
| 524 | if (httpRequest !== undefined) { |
| 525 | const sessionId = crypto.randomUUID() |
| 526 | stateful.registerHttp(sessionId, registration) |
| 527 | appendPreResponseHandlerUnsafe( |
| 528 | httpRequest, |
| 529 | (_request, response) => |
| 530 | Effect.succeed(HttpServerResponse.setHeaders(response, { |
| 531 | [MCP_SESSION_ID_HEADER]: sessionId, |
| 532 | [MCP_PROTOCOL_VERSION_HEADER]: protocol.protocolVersion |
| 533 | })) |
| 534 | ) |
| 535 | } else { |
| 536 | stateful.registerConnection(clientId, registration) |
| 537 | } |
| 538 | return Effect.succeed({ |
| 539 | capabilities, |
| 540 | instructions: options.serverInfo.instructions, |
| 541 | serverInfo: PublicMcpSchema.Implementation.make({ |
| 542 | name: options.serverInfo.name, |
| 543 | version: options.serverInfo.version, |
| 544 | description: options.serverInfo.description, |
| 545 | websiteUrl: options.serverInfo.websiteUrl, |
| 546 | icons: options.serverInfo.icons |
| 547 | }) |
| 548 | }) |
| 549 | }) |
| 550 | }), |
| 551 | setLogLevel: stateful.setLogLevel, |
| 552 | subscribe: stateful.subscribe, |
| 553 | unsubscribe: stateful.unsubscribe, |
| 554 | clientNotification: Effect.fnUntraced(function*(notification, clientId) { |
| 555 | if (notification._tag === "Initialized") { |
| 556 | stateful.markInitialized(clientId) |
| 557 | } |
| 558 | }) |
| 559 | } |
| 560 | yield* protocol.installHandlers(options.core, lifecycle, handlerTarget) |
| 561 | } |
| 562 | return Context.makeUnsafe(contextMap) |
| 563 | }) |
| 564 | }) |
| 565 | }) |
| 566 | |
| 567 | /** |
| 568 | * @internal |
| 569 | */ |
| 570 | export const layer = ( |
| 571 | protocols: NonEmptyReadonlyArray<PublicMcpProtocol.AnyProtocolAdapter> |
| 572 | ): Layer.Layer<ServerRuntime, Cause.IllegalArgumentError> => Layer.effect(ServerRuntime)(make(protocols)) |