import * as Data from "../../Data.ts" import * as Effect from "../../Effect.ts" import * as Base64 from "../../encoding/Base64.ts" import type * as Headers from "../../http/Headers.ts" import type * as LogLevel from "../../LogLevel.ts" import * as Match from "../../Match.ts" import * as Predicate from "../../Predicate.ts" import type * as PubSub from "../../PubSub.ts" import * as Result from "../../Result.ts" import type * as Rpc from "../../rpc/Rpc.ts" import * as RpcClient from "../../rpc/RpcClient.ts" import type { RpcClientError } from "../../rpc/RpcClientError.ts" import type * as RpcGroup from "../../rpc/RpcGroup.ts" import type * as RpcMessage from "../../rpc/RpcMessage.ts" import * as Schema from "../../Schema.ts" import type * as Scope from "../../Scope.ts" import type * as PublicMcpProtocol from "../McpProtocol.ts" import * as PublicMcpSchema from "../McpSchema.ts" import * as McpCore from "./mcpCore.ts" const LEGACY_RESOURCE_NOT_FOUND_ERROR_CODE = -32002 /** @internal */ export const BASE64_SENTINEL_PREFIX = "=?base64?" /** @internal */ export const BASE64_SENTINEL_SUFFIX = "?=" // A leading U+FEFF belongs to the header value and must survive comparison with the JSON body. const routingHeaderDecoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }) /** * @internal */ export const mcpLogLevels: Readonly< Record< PublicMcpSchema.LoggingLevel, { readonly effect: LogLevel.LogLevel; readonly order: number } > > = { debug: { effect: "Debug", order: 0 }, info: { effect: "Info", order: 1 }, notice: { effect: "Info", order: 2 }, warning: { effect: "Warn", order: 3 }, error: { effect: "Error", order: 4 }, critical: { effect: "Fatal", order: 5 }, alert: { effect: "Fatal", order: 6 }, emergency: { effect: "Fatal", order: 7 } } /** * @internal */ export const decodeRoutingHeader = (value: string): string | undefined => { const startsWithSentinel = value.startsWith(BASE64_SENTINEL_PREFIX) const endsWithSentinel = value.endsWith(BASE64_SENTINEL_SUFFIX) if (!startsWithSentinel || !endsWithSentinel) { return /^[\t\x20-\x7e]*$/.test(value) ? value : undefined } const decoded = Base64.decode( value.slice(BASE64_SENTINEL_PREFIX.length, -BASE64_SENTINEL_SUFFIX.length) ) if (Result.isFailure(decoded)) return undefined try { return routingHeaderDecoder.decode(decoded.success) } catch { return undefined } } /** * @internal */ export const profileFromClient = ( request: PublicMcpSchema.McpServerClient["Service"] ): McpCore.NegotiatedProtocolProfile => ({ protocolVersion: request.protocolVersion, clientCapabilities: request.clientCapabilities, clientInfo: request.clientInfo, requestMetadata: request.initializePayload._meta }) /** * @internal */ export const invocationFromClient = ( request: PublicMcpSchema.McpServerClient["Service"] ): McpCore.McpInvocation => ({ clientId: request.clientId, protocol: profileFromClient(request), requestContext: PublicMcpSchema.McpRequestContext.of({ clientId: request.clientId, protocolVersion: request.protocolVersion, clientCapabilities: request.clientCapabilities, clientInfo: request.clientInfo, requestMetadata: request.requestMetadata }), serverClient: request }) /** * @internal */ export const invocationFromRequestContext = ( request: PublicMcpSchema.McpRequestContext["Service"] ): McpCore.McpInvocation => ({ clientId: request.clientId, protocol: { protocolVersion: request.protocolVersion, clientCapabilities: request.clientCapabilities, clientInfo: request.clientInfo, requestMetadata: request.requestMetadata }, requestContext: request }) /** * @internal */ export const requireCompleteOperation = ( protocolVersion: PublicMcpProtocol.ProtocolVersion, outcome: McpCore.OperationOutcome ): Effect.Effect => outcome._tag === "Complete" ? Effect.succeed(outcome.value) : Effect.fail(new McpCore.UnsupportedByProtocol({ protocolVersion, feature: "Client input" })) /** * Projects boolean property schemas to equivalent objects for pre-June adapters. * Their dated tool descriptors require object-valued direct property schemas. * * @internal */ export const projectLegacyToolInputSchema = (inputSchema: PublicMcpSchema.ToolJson) => ({ ...inputSchema, properties: inputSchema.properties === undefined ? undefined : Object.fromEntries>>( Object.entries(inputSchema.properties).map(([name, schema]) => [ name, typeof schema === "boolean" ? schema ? {} : { not: {} } : schema ]) ) }) /** * Unquotes an exact text mirror when the protocol drops string `structuredContent`. * * @internal */ export const unwrapStringStructuredContent = ( result: PublicMcpSchema.CallToolResult ): PublicMcpSchema.CallToolResult["content"] => { const { content, structuredContent } = result if (typeof structuredContent !== "string" || content.length !== 1) return content const [block] = content return block.type === "text" && block.text === JSON.stringify(structuredContent) ? [{ ...block, text: structuredContent }] : content } const isSamplingToolContent = (content: unknown): boolean => Predicate.isReadonlyObject(content) && (content.type === "tool_use" || content.type === "tool_result") /** * @internal */ export const samplingRequestRequiresTools = (request: unknown): boolean => { if (!Predicate.isReadonlyObject(request)) return false if (request.tools !== undefined || request.toolChoice !== undefined) return true if (!Array.isArray(request.messages)) return false return request.messages.some((message) => { if (!Predicate.isReadonlyObject(message)) return false const content = message.content return Array.isArray(content) ? content.some(isSamplingToolContent) : isSamplingToolContent(content) }) } // NOTE: Keep the two codec assertions below as the single documented // existential-schema boundary. Rpc.AnyWithProps intentionally erases each // request's payload type, while the runtime schema still performs decoding and // encoding; do not spread this erasure into MCP domain records. /** * @internal */ export interface LifecycleRuntime { readonly initialize: ( protocolVersion: PublicMcpProtocol.ProtocolVersion, profile: McpCore.NegotiatedProtocolProfile, clientId: number ) => Effect.Effect readonly setLogLevel: ( level: PublicMcpSchema.LoggingLevel, clientId: number, headers: Headers.Headers ) => Effect.Effect readonly subscribe: (uri: string, clientId: number, headers: Headers.Headers) => Effect.Effect readonly unsubscribe: (uri: string, clientId: number, headers: Headers.Headers) => Effect.Effect readonly clientNotification: ( notification: McpCore.ClientNotification, clientId: number, headers: Headers.Headers ) => Effect.Effect } /** * @internal */ export class ProtocolError extends Data.TaggedError("ProtocolError")<{ readonly code: number readonly message: string readonly data?: unknown }> { static fromTool( error: McpCore.ToolError | McpCore.UnsupportedByProtocol ): ProtocolError { const message = Match.value(error).pipe( Match.tag("ToolNotFound", (error) => `Tool '${error.name}' not found`), Match.tag( "UnsupportedByProtocol", (error) => `${error.feature} is not supported by MCP ${error.protocolVersion}` ), Match.tags({ InvalidToolInput: (error) => error.message, InvalidToolContinuation: (error) => error.message, ToolExecutionError: (error) => error.message }), Match.exhaustive ) return new ProtocolError({ code: error._tag === "ToolExecutionError" ? PublicMcpSchema.INTERNAL_ERROR_CODE : PublicMcpSchema.INVALID_PARAMS_ERROR_CODE, message }) } static fromFeature(error: unknown): ProtocolError { if (Predicate.hasProperty(error, "_tag")) { if (error._tag === "PromptNotFound" && Predicate.hasProperty(error, "name") && Predicate.isString(error.name)) { return new ProtocolError({ code: PublicMcpSchema.INVALID_PARAMS_ERROR_CODE, message: `Prompt '${error.name}' not found` }) } if (error._tag === "ResourceNotFound" && Predicate.hasProperty(error, "uri") && Predicate.isString(error.uri)) { return new ProtocolError({ code: LEGACY_RESOURCE_NOT_FOUND_ERROR_CODE, message: `Resource '${error.uri}' not found` }) } } const decoded = decodeProtocolErrorFields(error) return Result.isSuccess(decoded) ? new ProtocolError(decoded.success) : new ProtocolError({ code: PublicMcpSchema.INTERNAL_ERROR_CODE, message: "MCP feature handler failed" }) } } const ProtocolErrorFields = Schema.Struct({ code: Schema.Finite, message: Schema.String, data: Schema.optionalKey(Schema.Unknown) }) const decodeProtocolErrorFields = Schema.decodeUnknownResult(ProtocolErrorFields) const isReverseOperationUnsupported = (cause: unknown): cause is PublicMcpSchema.McpReverseOperationUnsupported => Predicate.isTagged(cause, "McpReverseOperationUnsupported") /** * @internal */ export const reverseError = ( operation: PublicMcpSchema.McpReverseOperationError["operation"] ) => (cause: unknown) => isReverseOperationUnsupported(cause) ? cause : new PublicMcpSchema.McpReverseOperationError({ operation, cause }) /** * @internal */ export const transcode = < From extends Schema.Constraint, To extends Schema.Constraint >( from: From, to: To, input: Schema.Schema.Type ) => Schema.encodeEffect(from)(input).pipe( Effect.flatMap(Schema.decodeUnknownEffect(to)) ) /** * @internal */ export const transcodeStrict = < From extends Schema.Constraint, To extends Schema.Constraint >( from: From, to: To, input: Schema.Schema.Type ) => Schema.encodeEffect(from)(input).pipe( Effect.flatMap(Schema.decodeUnknownEffect(to, { onExcessProperty: "error" })) ) /** * @internal */ export const makeNotificationProjector = Effect.fn(function*( options: { readonly supportsProgressMessage: boolean }, notification: McpCore.ServerNotification ) { return McpCore.ServerNotification.$match(notification, { Cancelled: (notification): PublicMcpProtocol.ProjectedNotification => ({ tag: PublicMcpSchema.CancelledNotification._tag, payload: PublicMcpSchema.CancelledNotification.payloadSchema.make({ _meta: notification.metadata, requestId: notification.requestId, reason: notification.reason }) }), Progress: (notification): PublicMcpProtocol.ProjectedNotification => ({ tag: PublicMcpSchema.ProgressNotification._tag, payload: PublicMcpSchema.ProgressNotification.payloadSchema.make({ _meta: notification.metadata, progressToken: notification.progressToken, progress: notification.progress, total: notification.total, message: options.supportsProgressMessage ? notification.message : undefined }) }), LoggingMessage: (notification): PublicMcpProtocol.ProjectedNotification => ({ tag: PublicMcpSchema.LoggingMessageNotification._tag, payload: PublicMcpSchema.LoggingMessageNotification.payloadSchema.make({ _meta: notification.metadata, level: notification.level, logger: notification.logger, data: notification.data }) }), ResourceUpdated: (notification): PublicMcpProtocol.ProjectedNotification => ({ tag: PublicMcpSchema.ResourceUpdatedNotification._tag, payload: PublicMcpSchema.ResourceUpdatedNotification.payloadSchema.make({ _meta: notification.metadata, uri: notification.uri }) }), ResourcesChanged: (notification): PublicMcpProtocol.ProjectedNotification => ({ tag: PublicMcpSchema.ResourceListChangedNotification._tag, payload: PublicMcpSchema.ResourceListChangedNotification.payloadSchema.make({ _meta: notification.metadata }) }), ToolsChanged: (notification): PublicMcpProtocol.ProjectedNotification => ({ tag: PublicMcpSchema.ToolListChangedNotification._tag, payload: PublicMcpSchema.ToolListChangedNotification.payloadSchema.make({ _meta: notification.metadata }) }), PromptsChanged: (notification): PublicMcpProtocol.ProjectedNotification => ({ tag: PublicMcpSchema.PromptListChangedNotification._tag, payload: PublicMcpSchema.PromptListChangedNotification.payloadSchema.make({ _meta: notification.metadata }) }), ElicitationComplete: () => undefined }) }) /** * @internal */ export interface HandlerInstallationTarget { readonly context: HandlerInstallationContext readonly install: < Rpcs extends Rpc.Any, Handlers extends RpcGroup.HandlersFrom >( protocol: { readonly protocolVersion: string }, rpcs: RpcGroup.RpcGroup, handlers: Handlers ) => Effect.Effect> } /** * @internal */ export interface HandlerInstallationContext { readonly subscribeServerNotifications: Effect.Effect< PubSub.Subscription, never, Scope.Scope > /** Current list-change revision; subscriptions ignore changes emitted at or before it. */ readonly getListChangeRevision?: (() => number) | undefined readonly sendNotification?: ( protocolVersion: string, clientId: number, notification: PublicMcpProtocol.ProjectedNotification ) => Effect.Effect readonly terminateSubscription?: ( protocolVersion: string, clientId: number, requestId: RpcMessage.RequestId, reason: string ) => Effect.Effect readonly markSubscriptionCancelled?: ( clientId: number, requestId: RpcMessage.RequestId ) => Effect.Effect readonly supportedVersions: ReadonlyArray readonly serverInfo: { readonly name: string readonly version: string readonly instructions?: string | undefined readonly description?: string | undefined readonly websiteUrl?: string | undefined readonly icons?: ReadonlyArray | undefined readonly extensions?: PublicMcpSchema.ServerCapabilities["extensions"] | undefined } readonly registrationPresence: Effect.Effect<{ readonly tools: boolean readonly resources: boolean readonly prompts: boolean }> } /** * @internal */ export interface CanonicalServerNotification { readonly notification: SubscriptionServerNotification readonly listChangeRevision?: number | undefined readonly targetClientId?: number | undefined } /** * @internal */ export type SubscriptionServerNotification = Extract< McpCore.ServerNotification, { readonly _tag: "ToolsChanged" | "PromptsChanged" | "ResourcesChanged" | "ResourceUpdated" } > /** * @internal */ export const isSubscriptionServerNotification = ( notification: McpCore.ServerNotification ): notification is SubscriptionServerNotification => { switch (notification._tag) { case "ToolsChanged": case "PromptsChanged": case "ResourcesChanged": case "ResourceUpdated": return true default: return false } } /** * @internal */ export interface ProtocolAdapter< out Version extends string = string, out Runtime extends PublicMcpProtocol.RuntimeDescriptor = PublicMcpProtocol.RuntimeDescriptor, ClientRpcs extends Rpc.Any = Rpc.Any, ClientNotificationRpcs extends ClientRpcs = ClientRpcs, ServerRequestRpcs extends Rpc.Any = Rpc.Any, ServerNotificationRpcs extends Rpc.Any = Rpc.Any, HandlerRpcs extends Rpc.Any = never, HandlerRequirements = never > { // Each adapter owns its dated RPC vocabulary, transport policy, handler // projection, and wire behavior. readonly protocolVersion: Version readonly runtime: Runtime readonly clientRpcs: RpcGroup.RpcGroup readonly clientNotificationRpcs: RpcGroup.RpcGroup readonly serverRequestRpcs: RpcGroup.RpcGroup readonly serverNotificationRpcs: RpcGroup.RpcGroup readonly payloadCodecs: (rpc: Rpc.AnyWithProps) => PublicMcpProtocol.PayloadCodecs readonly handlerRpcs?: RpcGroup.RpcGroup | undefined readonly installHandlers: ( core: McpCore.McpCore, lifecycle: LifecycleRuntime | undefined, target: HandlerInstallationTarget ) => Effect.Effect readonly makeReverseClient: ( profile: McpCore.NegotiatedProtocolProfile ) => Effect.Effect< PublicMcpSchema.McpReverseClient, never, RpcClient.Protocol | Scope.Scope > readonly projectNotification: ( notification: McpCore.ServerNotification ) => Effect.Effect readonly normalizeCancellation: ( payload: unknown ) => Effect.Effect } type HandlerLifecycle = Runtime extends PublicMcpProtocol.StatefulRuntimeDescriptor ? LifecycleRuntime : undefined /** * @internal */ export const make = < const Version extends string, const Runtime extends PublicMcpProtocol.RuntimeDescriptor, ClientRpcs extends Rpc.Any, ClientNotificationRpcs extends ClientRpcs, ServerRequestRpcs extends Rpc.Any, ServerNotificationRpcs extends Rpc.Any, HandlerRpcs extends Rpc.Any = never, Handlers extends RpcGroup.HandlersFrom = RpcGroup.HandlersFrom >(options: { readonly protocolVersion: Version readonly runtime: Runtime readonly clientRpcs: RpcGroup.RpcGroup readonly clientNotificationRpcs: RpcGroup.RpcGroup readonly serverRequestRpcs: RpcGroup.RpcGroup readonly serverNotificationRpcs: RpcGroup.RpcGroup readonly handlerRpcs?: RpcGroup.RpcGroup | undefined readonly makeHandlers?: | (( core: McpCore.McpCore, lifecycle: HandlerLifecycle, context: HandlerInstallationContext ) => Handlers) | undefined readonly toReverseClient: ( profile: McpCore.NegotiatedProtocolProfile, client: RpcClient.RpcClient ) => PublicMcpSchema.McpReverseClient readonly projectNotification: ( notification: McpCore.ServerNotification ) => Effect.Effect readonly normalizeCancellation: ( payload: unknown ) => Effect.Effect }): ProtocolAdapter< Version, Runtime, ClientRpcs, ClientNotificationRpcs, ServerRequestRpcs, ServerNotificationRpcs, HandlerRpcs, RpcGroup.HandlersServices > => { const payloadCodecsCache = new WeakMap() const payloadCodecs = (rpc: Rpc.AnyWithProps): PublicMcpProtocol.PayloadCodecs => { let codecs = payloadCodecsCache.get(rpc) if (codecs === undefined) { const schema = Schema.toCodecJson(rpc.payloadSchema) codecs = { decode: Schema.decodeUnknownEffect(schema) as PublicMcpProtocol.PayloadCodecs["decode"], encode: Schema.encodeUnknownEffect(schema) as PublicMcpProtocol.PayloadCodecs["encode"] } payloadCodecsCache.set(rpc, codecs) } return codecs } const installHandlers = ( core: McpCore.McpCore, lifecycle: LifecycleRuntime | undefined, target: HandlerInstallationTarget ): Effect.Effect> => options.handlerRpcs === undefined || options.makeHandlers === undefined ? Effect.void : lifecycle === undefined && options.runtime._tag === "Stateful" ? Effect.die("MCP sessionful handler installation requires a lifecycle runtime") : target.install( options, options.handlerRpcs, options.makeHandlers( core, // TypeScript cannot narrow HandlerLifecycle from the generic runtime tag; the preceding guard // enforces the stateful lifecycle invariant. (options.runtime._tag === "Stateful" ? lifecycle : undefined) as HandlerLifecycle, target.context ) ) const makeReverseClient = ( profile: McpCore.NegotiatedProtocolProfile ): Effect.Effect< PublicMcpSchema.McpReverseClient, never, RpcClient.Protocol | Scope.Scope > => RpcClient.make(options.serverRequestRpcs, { spanPrefix: "McpServer/Client" }).pipe( Effect.map((client) => options.toReverseClient(profile, client)) ) return { ...options, payloadCodecs, installHandlers, makeReverseClient } }