| /** | |
| * Builds Model Context Protocol (MCP) servers with Effect. | |
| * | |
| * The `McpServer` service stores the tools, resources, resource templates, | |
| * prompts, completions, initialized clients, and outgoing notifications exposed | |
| * by a server. This module also includes the server runner, custom protocol, | |
| * stdio, and HTTP layers, registration helpers, and APIs that let handlers ask | |
| * the connected client for structured input or read its advertised | |
| * capabilities. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Arr from "../../Array.js"; | |
| import * as Cause from "../../Cause.js"; | |
| import * as Context from "../../Context.js"; | |
| import * as Effect from "../../Effect.js"; | |
| import * as Exit from "../../Exit.js"; | |
| import * as Fiber from "../../Fiber.js"; | |
| import * as Layer from "../../Layer.js"; | |
| import * as Option from "../../Option.js"; | |
| import * as Queue from "../../Queue.js"; | |
| import * as RcMap from "../../RcMap.js"; | |
| import { CurrentLogLevel } from "../../References.js"; | |
| import * as Schema from "../../Schema.js"; | |
| import * as SchemaAST from "../../SchemaAST.js"; | |
| import * as Sink from "../../Sink.js"; | |
| import * as Stream from "../../Stream.js"; | |
| import * as FindMyWay from "../http/FindMyWay.js"; | |
| import * as Headers from "../http/Headers.js"; | |
| import { appendPreResponseHandlerUnsafe } from "../http/HttpEffect.js"; | |
| import * as HttpRouter from "../http/HttpRouter.js"; | |
| import * as HttpServerRequest from "../http/HttpServerRequest.js"; | |
| import * as HttpServerResponse from "../http/HttpServerResponse.js"; | |
| import * as Rpc from "../rpc/Rpc.js"; | |
| import * as RpcClient from "../rpc/RpcClient.js"; | |
| import * as RpcMessage from "../rpc/RpcMessage.js"; | |
| import * as RpcSerialization from "../rpc/RpcSerialization.js"; | |
| import * as RpcServer from "../rpc/RpcServer.js"; | |
| import { CallToolResult, ClientNotificationRpcs, ClientRpcs, CompleteResult, Elicit, ElicitationDeclined, EnabledWhen, GetPromptResult, InternalError, InvalidParams, isParam, ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult, ListToolsResult, McpServerClient, McpServerClientMiddleware, Prompt, Resource, ResourceTemplate, ServerNotificationRpcs, ServerRequestRpcs, TextContent, Tool as McpTool } from "./McpSchema.js"; | |
| import * as Tool from "./Tool.js"; | |
| /** | |
| * Service that stores and serves an MCP server's registered tools, resources, | |
| * prompts, completions, and outgoing notifications. | |
| * | |
| * **Details** | |
| * | |
| * Handlers use this service to register capabilities and resolve incoming MCP | |
| * requests. | |
| * | |
| * @category server | |
| * @since 4.0.0 | |
| */ | |
| export class McpServer extends /*#__PURE__*/Context.Service()("effect/ai/McpServer") { | |
| /** | |
| * Builds an MCP server service from registered tools, prompts, resources, and completions. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| static make = /*#__PURE__*/Effect.gen(function* () { | |
| const matcher = makeUriMatcher(); | |
| const tools = Arr.empty(); | |
| const toolMap = new Map(); | |
| const resources = []; | |
| const resourceTemplates = []; | |
| const prompts = []; | |
| const promptMap = new Map(); | |
| const completionsMap = new Map(); | |
| const notificationsQueue = yield* Queue.make(); | |
| const listChangedHandles = new Map(); | |
| const notifications = yield* RpcClient.makeNoSerialization(ServerNotificationRpcs, { | |
| spanPrefix: "McpServer/Notifications", | |
| onFromClient: options => Effect.suspend(() => { | |
| const message = options.message; | |
| if (message._tag !== "Request") { | |
| return Effect.void; | |
| } | |
| if (message.tag.includes("list_changed")) { | |
| if (!listChangedHandles.has(message.tag)) { | |
| listChangedHandles.set(message.tag, setTimeout(() => { | |
| Queue.offerUnsafe(notificationsQueue, message); | |
| listChangedHandles.delete(message.tag); | |
| }, 0)); | |
| } | |
| } else { | |
| Queue.offerUnsafe(notificationsQueue, message); | |
| } | |
| return notifications.write({ | |
| clientId: 0, | |
| requestId: message.id, | |
| _tag: "Exit", | |
| exit: Exit.void | |
| }); | |
| }) | |
| }); | |
| return McpServer.of({ | |
| notifications: notifications.client, | |
| notificationsQueue, | |
| initializedClients: new Set(), | |
| get tools() { | |
| return tools; | |
| }, | |
| addTool: options => Effect.suspend(() => { | |
| tools.push(options); | |
| toolMap.set(options.tool.name, options.handle); | |
| return notifications.client["notifications/tools/list_changed"]({}); | |
| }), | |
| callTool: request => Effect.suspend(() => { | |
| const handle = toolMap.get(request.name); | |
| if (!handle) { | |
| return Effect.fail(new InvalidParams({ | |
| message: `Tool '${request.name}' not found` | |
| })); | |
| } | |
| return handle(request.arguments); | |
| }), | |
| get resources() { | |
| return resources; | |
| }, | |
| get resourceTemplates() { | |
| return resourceTemplates; | |
| }, | |
| addResource: options => Effect.suspend(() => { | |
| resources.push(options); | |
| matcher.add(options.resource.uri, { | |
| _tag: "Resource", | |
| effect: options.handle | |
| }); | |
| return notifications.client["notifications/resources/list_changed"]({}); | |
| }), | |
| addResourceTemplate: ({ | |
| annotations, | |
| completions, | |
| handle, | |
| routerPath, | |
| template | |
| }) => Effect.suspend(() => { | |
| resourceTemplates.push({ | |
| template, | |
| annotations | |
| }); | |
| matcher.add(routerPath, { | |
| _tag: "ResourceTemplate", | |
| handle | |
| }); | |
| for (const [param, handle] of Object.entries(completions)) { | |
| completionsMap.set(`ref/resource/${template.uriTemplate}/${param}`, handle); | |
| } | |
| return notifications.client["notifications/resources/list_changed"]({}); | |
| }), | |
| findResource: uri => Effect.suspend(() => { | |
| const match = matcher.find(uri); | |
| if (!match) { | |
| return Effect.succeed({ | |
| contents: [] | |
| }); | |
| } else if (match.handler._tag === "Resource") { | |
| return match.handler.effect; | |
| } | |
| const params = []; | |
| for (const key of Object.keys(match.params)) { | |
| params[Number(key)] = match.params[key]; | |
| } | |
| return match.handler.handle(uri, params); | |
| }), | |
| get prompts() { | |
| return prompts; | |
| }, | |
| addPrompt: options => Effect.suspend(() => { | |
| prompts.push(options); | |
| promptMap.set(options.prompt.name, options.handle); | |
| for (const [param, handle] of Object.entries(options.completions)) { | |
| completionsMap.set(`ref/prompt/${options.prompt.name}/${param}`, handle); | |
| } | |
| return notifications.client["notifications/prompts/list_changed"]({}); | |
| }), | |
| getPromptResult: Effect.fnUntraced(function* ({ | |
| arguments: params, | |
| name | |
| }) { | |
| const handler = promptMap.get(name); | |
| if (!handler) { | |
| return yield* new InvalidParams({ | |
| message: `Prompt '${name}' not found` | |
| }); | |
| } | |
| return yield* handler(params ?? {}); | |
| }), | |
| completion: Effect.fnUntraced(function* (complete) { | |
| const ref = complete.ref; | |
| const key = ref.type === "ref/resource" ? `ref/resource/${ref.uri}/${complete.argument.name}` : `ref/prompt/${ref.name}/${complete.argument.name}`; | |
| const handler = completionsMap.get(key); | |
| return handler ? yield* handler(complete.argument.value) : CompleteResult.empty; | |
| }) | |
| }); | |
| }); | |
| /** | |
| * Layer that provides the MCP server and client services. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| static layer = /*#__PURE__*/Layer.effect(McpServer)(McpServer.make); | |
| } | |
| const LATEST_PROTOCOL_VERSION = "2025-06-18"; | |
| const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, "2025-03-26", "2024-11-05", "2024-10-07"]; | |
| const mcpSessionIdHeader = "mcp-session-id"; | |
| const mcpProtocolVersionHeader = "mcp-protocol-version"; | |
| /** | |
| * Runs an MCP server over the current `RpcServer.Protocol`. | |
| * | |
| * **Details** | |
| * | |
| * The server performs initialization and session handling, serves registered | |
| * tools, resources, and prompts, and forwards queued server notifications to | |
| * initialized clients. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const run = /*#__PURE__*/Effect.fnUntraced(function* (options) { | |
| const protocol = yield* RpcServer.Protocol; | |
| const server = yield* McpServer; | |
| const isHttp = Option.isSome(yield* Effect.serviceOption(HttpRouter.HttpRouter)); | |
| const clientSessions = new Map(); | |
| const handlers = yield* Layer.build(layerHandlers(options, { | |
| clientSessions | |
| })); | |
| const clients = yield* RcMap.make({ | |
| lookup: Effect.fnUntraced(function* (clientId) { | |
| let write; | |
| const client = yield* RpcClient.make(ServerRequestRpcs, { | |
| spanPrefix: "McpServer/Client" | |
| }).pipe(Effect.provideServiceEffect(RpcClient.Protocol, RpcClient.Protocol.make(Effect.fnUntraced(function* (writeResponse) { | |
| let cid = 0; | |
| write = message => writeResponse(cid, message); | |
| return { | |
| send(id, request, _transferables) { | |
| cid = id; | |
| return protocol.send(clientId, { | |
| ...request, | |
| headers: undefined, | |
| traceId: undefined, | |
| spanId: undefined, | |
| sampled: undefined | |
| }); | |
| }, | |
| supportsAck: true, | |
| supportsTransferables: false, | |
| supportsStructuredClone: false | |
| }; | |
| })))); | |
| return { | |
| client, | |
| write | |
| }; | |
| }), | |
| idleTimeToLive: 10000 | |
| }); | |
| const clientMiddleware = McpServerClientMiddleware.of((effect, { | |
| client, | |
| headers, | |
| rpc | |
| }) => { | |
| const initializePayload = getInitializedClient(clientSessions, client.id, headers); | |
| const isInitialize = rpc._tag === "initialize"; | |
| if (!isInitialize && !initializePayload) { | |
| const fiber = Fiber.getCurrent(); | |
| const httpRequest = Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest); | |
| if (httpRequest) { | |
| appendPreResponseHandlerUnsafe(httpRequest, () => Effect.succeed(HttpServerResponse.empty({ | |
| status: 404 | |
| }))); | |
| } | |
| return Effect.die(new Error(`Mcp-Session-Id does not exist`)); | |
| } | |
| return Effect.provideService(effect, McpServerClient, McpServerClient.of({ | |
| clientId: client.id, | |
| initializePayload: initializePayload, | |
| getClient: RcMap.get(clients, client.id).pipe(Effect.map(({ | |
| client | |
| }) => client)) | |
| })); | |
| }); | |
| const patchedProtocol = RpcServer.Protocol.of({ | |
| ...protocol, | |
| run: f => protocol.run((clientId, request_) => { | |
| const request = request_; | |
| switch (request._tag) { | |
| case "Request": | |
| { | |
| if (isHttp) { | |
| const fiber = Fiber.getCurrent(); | |
| const httpRequest = Context.getUnsafe(fiber.context, HttpServerRequest.HttpServerRequest); | |
| const client = getInitializedClient(clientSessions, clientId, httpRequest.headers); | |
| if (client) { | |
| appendPreResponseHandlerUnsafe(httpRequest, (_, res) => Effect.succeed(HttpServerResponse.setHeader(res, mcpProtocolVersionHeader, client.protocolVersion))); | |
| } | |
| } | |
| const rpc = ClientNotificationRpcs.requests.get(request.tag); | |
| if (rpc) { | |
| if (request.tag === "notifications/cancelled") { | |
| return f(clientId, { | |
| _tag: "Interrupt", | |
| requestId: String(request.payload.requestId) | |
| }); | |
| } | |
| const handler = handlers.mapUnsafe.get(request.tag); | |
| return handler ? handler.handler(request.payload, { | |
| rpc, | |
| requestId: RpcMessage.RequestId(request.id), | |
| client: new Rpc.ServerClient(clientId), | |
| headers: Headers.fromInput(request.headers) | |
| }) : Effect.void; | |
| } | |
| return f(clientId, request); | |
| } | |
| case "Ping": | |
| case "Ack": | |
| case "Interrupt": | |
| case "Eof": | |
| return f(clientId, request); | |
| case "Pong": | |
| case "Exit": | |
| case "Chunk": | |
| case "ClientProtocolError": | |
| case "Defect": | |
| return RcMap.get(clients, clientId).pipe(Effect.flatMap(({ | |
| write | |
| }) => write(request)), Effect.scoped); | |
| } | |
| }) | |
| }); | |
| const encodeNotification = Schema.encodeUnknownEffect(Schema.Union(Array.from(ServerNotificationRpcs.requests.values(), rpc => rpc.payloadSchema))); | |
| yield* Queue.take(server.notificationsQueue).pipe(Effect.flatMap(Effect.fnUntraced(function* (request) { | |
| const encoded = yield* encodeNotification(request.payload); | |
| const message = { | |
| _tag: "Request", | |
| tag: request.tag, | |
| payload: encoded | |
| }; | |
| const clientIds = yield* patchedProtocol.clientIds; | |
| for (const clientId of server.initializedClients.keys()) { | |
| if (!clientIds.has(clientId)) { | |
| server.initializedClients.delete(clientId); | |
| continue; | |
| } | |
| yield* patchedProtocol.send(clientId, message); | |
| } | |
| })), Effect.catchCause(() => Effect.void), Effect.forever, Effect.forkScoped); | |
| return yield* RpcServer.make(ClientRpcs, { | |
| spanPrefix: "McpServer", | |
| disableFatalDefects: true | |
| }).pipe(Effect.provideService(RpcServer.Protocol, patchedProtocol), Effect.provideService(McpServerClientMiddleware, clientMiddleware), Effect.provide(handlers)); | |
| }, Effect.scoped); | |
| /** | |
| * Creates a layer that starts an MCP server over an existing | |
| * `RpcServer.Protocol` and provides the `McpServer` and `McpServerClient` | |
| * services. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you already have a custom or externally provided | |
| * `RpcServer.Protocol` and want to start an MCP server as part of a layer | |
| * graph. | |
| * | |
| * **Details** | |
| * | |
| * The returned layer forks `run(options)` in the layer scope and merges | |
| * `McpServer.layer`, so registration layers can use the `McpServer` service | |
| * while the server is running. | |
| * | |
| * **Gotchas** | |
| * | |
| * Unlike `layerStdio` and `layerHttp`, this layer does not install a concrete | |
| * transport. The surrounding layer graph must provide `RpcServer.Protocol`. | |
| * | |
| * @see {@link run} for the effect form used by this layer | |
| * @see {@link layerStdio} for a stdio-backed layer that installs the MCP protocol and NDJSON-RPC serialization | |
| * @see {@link layerHttp} for an HTTP-backed layer that registers with `HttpRouter` and installs JSON-RPC serialization | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layer = options => Layer.effectDiscard(Effect.forkScoped(run(options))).pipe(Layer.provideMerge(McpServer.layer)); | |
| /** | |
| * Runs the McpServer, using stdio for input and output. | |
| * | |
| * **Example** (Running an MCP server over stdio) | |
| * | |
| * ```ts | |
| * import { Effect, Layer, Logger, Schema } from "effect" | |
| * import { NodeRuntime, NodeStdio } from "@effect/platform-node" | |
| * import { McpSchema, McpServer } from "effect/unstable/ai" | |
| * | |
| * const idParam = McpSchema.param("id", Schema.Number) | |
| * | |
| * // Define a resource template for a README file | |
| * const ReadmeTemplate = McpServer.resource`file://readme/${idParam}`({ | |
| * name: "README Template", | |
| * // You can add auto-completion for the ID parameter | |
| * completion: { | |
| * id: (_) => Effect.succeed([1, 2, 3, 4, 5]) | |
| * }, | |
| * content: Effect.fn(function*(_uri, id) { | |
| * return `# MCP Server Demo - ID: ${id}` | |
| * }) | |
| * }) | |
| * | |
| * // Define a test prompt with parameters | |
| * const TestPrompt = McpServer.prompt({ | |
| * name: "Test Prompt", | |
| * description: "A test prompt to demonstrate MCP server capabilities", | |
| * parameters: { | |
| * flightNumber: Schema.String | |
| * }, | |
| * completion: { | |
| * flightNumber: () => Effect.succeed(["FL123", "FL456", "FL789"]) | |
| * }, | |
| * content: ({ flightNumber }) => | |
| * Effect.succeed(`Get the booking details for flight number: ${flightNumber}`) | |
| * }) | |
| * | |
| * // Merge all the resources and prompts into a single server layer | |
| * const ServerLayer = Layer.mergeAll( | |
| * ReadmeTemplate, | |
| * TestPrompt | |
| * ).pipe( | |
| * // Provide the MCP server implementation | |
| * Layer.provide(McpServer.layerStdio({ | |
| * name: "Demo Server", | |
| * version: "1.0.0", | |
| * })), | |
| * Layer.provide(NodeStdio.layer), | |
| * Layer.provide(Layer.succeed(Logger.LogToStderr)(true)) | |
| * ) | |
| * | |
| * Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) | |
| * ``` | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerStdio = options => layer(options).pipe(Layer.provide(RpcServer.layerProtocolStdio), Layer.provide(RpcSerialization.layerNdJsonRpc())); | |
| /** | |
| * Registers an HTTP POST JSON-RPC route at `options.path` on the current | |
| * `HttpRouter`. | |
| * | |
| * **When to use** | |
| * | |
| * Use to expose an MCP server through an existing `HttpRouter`. | |
| * | |
| * **Details** | |
| * | |
| * This layer composes `layer(options)`, `RpcServer.layerProtocolHttp(options)`, | |
| * and `RpcSerialization.layerJsonRpc()`. | |
| * | |
| * @see {@link layerStdio} for exposing the server over stdio | |
| * @see {@link layer} for the base MCP server layer without a transport protocol | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layerHttp = options => layer(options).pipe(Layer.provide(RpcServer.layerProtocolHttp(options)), Layer.provide(RpcSerialization.layerJsonRpc())); | |
| /** | |
| * Registers a `Toolkit` with the `McpServer`. | |
| * | |
| * @category tools | |
| * @since 4.0.0 | |
| */ | |
| export const registerToolkit = /*#__PURE__*/Effect.fnUntraced(function* (toolkit) { | |
| const registry = yield* McpServer; | |
| const built = yield* toolkit; | |
| const services = yield* Effect.context(); | |
| for (const tool of Object.values(built.tools)) { | |
| const annotations = tool.annotations; | |
| const toolMeta = Context.getOrUndefined(annotations, Tool.Meta); | |
| const mcpTool = new McpTool({ | |
| name: tool.name, | |
| description: Tool.getDescription(tool), | |
| inputSchema: Tool.getJsonSchema(tool), | |
| annotations: { | |
| ...Context.getOption(tool.annotations, Tool.Title).pipe(Option.map(title => ({ | |
| title | |
| })), Option.getOrUndefined), | |
| readOnlyHint: Context.get(tool.annotations, Tool.Readonly), | |
| destructiveHint: Context.get(tool.annotations, Tool.Destructive), | |
| idempotentHint: Context.get(tool.annotations, Tool.Idempotent), | |
| openWorldHint: Context.get(tool.annotations, Tool.OpenWorld) | |
| }, | |
| _meta: toolMeta | |
| }); | |
| yield* registry.addTool({ | |
| tool: mcpTool, | |
| annotations, | |
| handle(payload) { | |
| return built.handle(tool.name, payload).pipe(Stream.unwrap, Stream.run(Sink.last()), Effect.flatMap(Effect.fromOption), Effect.provideContext(services), Effect.matchCause({ | |
| onFailure: cause => new CallToolResult({ | |
| isError: true, | |
| content: [{ | |
| type: "text", | |
| text: Cause.pretty(cause) | |
| }] | |
| }), | |
| onSuccess: result => new CallToolResult({ | |
| isError: false, | |
| structuredContent: typeof result.encodedResult === "object" ? result.encodedResult : undefined, | |
| content: [{ | |
| type: "text", | |
| text: JSON.stringify(result.encodedResult) | |
| }] | |
| }) | |
| }), Effect.tapCause(Effect.log)); | |
| } | |
| }); | |
| } | |
| }); | |
| /** | |
| * Registers an `AiToolkit` with the `McpServer`. | |
| * | |
| * @category tools | |
| * @since 4.0.0 | |
| */ | |
| export const toolkit = toolkit => Layer.effectDiscard(registerToolkit(toolkit)).pipe(Layer.provide(McpServer.layer)); | |
| /** | |
| * Registers an MCP resource or resource template from an Effect program. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you are already inside an Effect program with an `McpServer` | |
| * service and need to add a concrete resource or URI-template resource | |
| * directly. | |
| * | |
| * @see {@link resource} for the layer-based resource registration wrapper | |
| * | |
| * @category resources | |
| * @since 4.0.0 | |
| */ | |
| export const registerResource = function () { | |
| if (arguments.length === 1) { | |
| const options = arguments[0]; | |
| return Effect.gen(function* () { | |
| const services = yield* Effect.context(); | |
| const registry = yield* McpServer; | |
| yield* registry.addResource({ | |
| resource: new Resource({ | |
| ...options, | |
| annotations: options | |
| }), | |
| handle: options.content.pipe(Effect.provideContext(services), Effect.map(content => resolveResourceContent(options.uri, content)), Effect.catchCause(cause => { | |
| const prettyError = Cause.prettyErrors(cause)[0]; | |
| return Effect.fail(new InternalError({ | |
| message: prettyError.message | |
| })); | |
| })), | |
| annotations: options.annotations ?? Context.empty() | |
| }); | |
| }); | |
| } | |
| const { | |
| params, | |
| routerPath, | |
| schema, | |
| uriPath | |
| } = compileUriTemplate(...arguments); | |
| return Effect.fnUntraced(function* (options) { | |
| const services = yield* Effect.context(); | |
| const registry = yield* McpServer; | |
| const decode = Schema.decodeUnknownEffect(schema); | |
| const template = new ResourceTemplate({ | |
| ...options, | |
| uriTemplate: uriPath, | |
| annotations: options | |
| }); | |
| const completions = {}; | |
| for (const [param, handle] of Object.entries(options.completion ?? {})) { | |
| const encodeArray = Schema.encodeUnknownEffect(Schema.Array(params[param])); | |
| const handler = input => handle(input).pipe(Effect.flatMap(encodeArray), Effect.map(values => ({ | |
| completion: { | |
| values: values, | |
| total: values.length, | |
| hasMore: false | |
| } | |
| })), Effect.catchCause(cause => { | |
| const prettyError = Cause.prettyErrors(cause)[0]; | |
| return Effect.fail(new InternalError({ | |
| message: prettyError.message | |
| })); | |
| }), Effect.provideContext(services)); | |
| completions[param] = handler; | |
| } | |
| yield* registry.addResourceTemplate({ | |
| template, | |
| routerPath, | |
| completions, | |
| annotations: options.annotations ?? Context.empty(), | |
| handle: (uri, params) => decode(params).pipe(Effect.mapError(error => new InvalidParams({ | |
| message: error.message | |
| })), Effect.flatMap(params => options.content(uri, ...params).pipe(Effect.map(content => resolveResourceContent(uri, content)), Effect.catchCause(cause => { | |
| const prettyError = Cause.prettyErrors(cause)[0]; | |
| return Effect.fail(new InternalError({ | |
| message: prettyError.message | |
| })); | |
| }))), Effect.provideContext(services)) | |
| }); | |
| }); | |
| }; | |
| /** | |
| * Creates a layer that registers an MCP resource or resource template. | |
| * | |
| * **When to use** | |
| * | |
| * Use to compose resource registration into an MCP server layer. | |
| * | |
| * @see {@link registerResource} for the Effect-level resource registration API | |
| * | |
| * @category resources | |
| * @since 4.0.0 | |
| */ | |
| export const resource = function () { | |
| if (arguments.length === 1) { | |
| return Layer.effectDiscard(registerResource(arguments[0])).pipe(Layer.provide(McpServer.layer)); | |
| } | |
| const register = registerResource(...arguments); | |
| return options => Layer.effectDiscard(register(options)).pipe(Layer.provide(McpServer.layer)); | |
| }; | |
| /** | |
| * Registers an MCP prompt from an Effect program. | |
| * | |
| * **When to use** | |
| * | |
| * Use when you are already inside an Effect program with an `McpServer` | |
| * service and need to add a prompt handler directly. | |
| * | |
| * **Details** | |
| * | |
| * Parameters are decoded with the supplied schema, completion handlers encode | |
| * per-parameter suggestions, and string prompt content is converted into a user | |
| * text message. | |
| * | |
| * @see {@link prompt} for the layer-based prompt registration wrapper | |
| * | |
| * @category prompts | |
| * @since 4.0.0 | |
| */ | |
| export const registerPrompt = options => { | |
| const args = Arr.empty(); | |
| const props = options.parameters ?? {}; | |
| for (const [name, prop] of Object.entries(props)) { | |
| args.push({ | |
| name, | |
| description: SchemaAST.resolveDescription(prop.ast), | |
| required: !SchemaAST.isOptional(prop.ast) | |
| }); | |
| } | |
| const prompt = new Prompt({ | |
| name: options.name, | |
| description: options.description, | |
| arguments: args | |
| }); | |
| const decode = options.parameters ? Schema.decodeEffect(Schema.Struct(props)) : () => Effect.succeed({}); | |
| const completion = options.completion ?? {}; | |
| return Effect.gen(function* () { | |
| const registry = yield* McpServer; | |
| const services = yield* Effect.context(); | |
| const completions = {}; | |
| for (const [param, handle] of Object.entries(completion)) { | |
| const encodeArray = Schema.encodeEffect(Schema.Array(props[param])); | |
| const handler = input => handle(input).pipe(Effect.flatMap(encodeArray), Effect.map(values => ({ | |
| completion: { | |
| values: values, | |
| total: values.length, | |
| hasMore: false | |
| } | |
| })), Effect.catchCause(cause => { | |
| const prettyError = Cause.prettyErrors(cause)[0]; | |
| return Effect.fail(new InternalError({ | |
| message: prettyError.message | |
| })); | |
| }), Effect.provide(services)); | |
| completions[param] = handler; | |
| } | |
| yield* registry.addPrompt({ | |
| prompt, | |
| completions, | |
| annotations: options.annotations ?? Context.empty(), | |
| handle: params => decode(params).pipe(Effect.mapError(error => new InvalidParams({ | |
| message: error.message | |
| })), Effect.flatMap(params => options.content(params)), Effect.map(messages => { | |
| messages = typeof messages === "string" ? [{ | |
| role: "user", | |
| content: TextContent.make({ | |
| text: messages | |
| }) | |
| }] : messages; | |
| return new GetPromptResult({ | |
| messages, | |
| description: prompt.description | |
| }); | |
| }), Effect.catchCause(cause => { | |
| const prettyError = Cause.prettyErrors(cause)[0]; | |
| return Effect.fail(new InternalError({ | |
| message: prettyError.message | |
| })); | |
| }), Effect.provideContext(services)) | |
| }); | |
| }); | |
| }; | |
| /** | |
| * Creates a layer that registers an MCP prompt. | |
| * | |
| * **When to use** | |
| * | |
| * Use to compose prompt registration into an MCP server layer. | |
| * | |
| * **Details** | |
| * | |
| * Parameters are decoded with the supplied schema, completion handlers encode | |
| * per-parameter suggestions, and string prompt content is converted into a user | |
| * text message. | |
| * | |
| * @see {@link registerPrompt} for the Effect-level prompt registration API | |
| * | |
| * @category prompts | |
| * @since 4.0.0 | |
| */ | |
| export const prompt = options => Layer.effectDiscard(registerPrompt(options)).pipe(Layer.provide(McpServer.layer)); | |
| /** | |
| * Collects structured input from the current MCP client and decodes the | |
| * accepted response with `schema`. | |
| * | |
| * **Details** | |
| * | |
| * Accepted content is decoded with the supplied schema, declined requests fail | |
| * with `ElicitationDeclined`, and canceled requests interrupt the effect. | |
| * | |
| * @category elicitation | |
| * @since 4.0.0 | |
| */ | |
| export const elicit = /*#__PURE__*/Effect.fnUntraced(function* (options) { | |
| const { | |
| getClient | |
| } = yield* McpServerClient; | |
| const client = yield* getClient; | |
| const schema = options.schema; | |
| const request = Elicit.payloadSchema.make({ | |
| message: options.message, | |
| requestedSchema: Tool.getJsonSchemaFromSchema(schema) | |
| }); | |
| const res = yield* client["elicitation/create"](request).pipe(Effect.catchCause(cause => Effect.fail(new ElicitationDeclined({ | |
| cause: Cause.squash(cause), | |
| request | |
| })))); | |
| switch (res.action) { | |
| case "accept": | |
| return yield* Effect.orDie(Schema.decodeUnknownEffect(schema)(res.content)); | |
| case "cancel": | |
| return yield* Effect.interrupt; | |
| case "decline": | |
| return yield* new ElicitationDeclined({ | |
| request | |
| }); | |
| } | |
| }, Effect.scoped); | |
| /** | |
| * Accesses the current client's capabilities. | |
| * | |
| * @category capabilities | |
| * @since 4.0.0 | |
| */ | |
| export const clientCapabilities = /*#__PURE__*/McpServerClient.useSync(_ => _.initializePayload.capabilities); | |
| // ----------------------------------------------------------------------------- | |
| // Internal | |
| // ----------------------------------------------------------------------------- | |
| const makeUriMatcher = () => { | |
| const router = FindMyWay.make({ | |
| ignoreTrailingSlash: true, | |
| ignoreDuplicateSlashes: true, | |
| caseSensitive: true | |
| }); | |
| const add = (uri, value) => { | |
| router.on("GET", uri, value); | |
| }; | |
| const find = uri => router.find("GET", uri); | |
| return { | |
| add, | |
| find | |
| }; | |
| }; | |
| const compileUriTemplate = (segments, ...schemas) => { | |
| let routerPath = segments[0].replace(":", "::"); | |
| let uriPath = segments[0]; | |
| const params = {}; | |
| let pathSchema = Schema.Tuple([]); | |
| if (schemas.length > 0) { | |
| const arr = []; | |
| for (let i = 0; i < schemas.length; i++) { | |
| const toCodecStringTree = Schema.toCodecStringTree(schemas[i]); | |
| const segment = segments[i + 1]; | |
| const key = String(i); | |
| arr.push(toCodecStringTree); | |
| routerPath += `:${key}${segment.replace(":", "::")}`; | |
| const schema = schemas[i]; | |
| const paramName = isParam(schema) ? schema.name : `param${key}`; | |
| params[paramName] = toCodecStringTree; | |
| uriPath += `{${paramName}}${segment}`; | |
| } | |
| pathSchema = Schema.Tuple(arr); | |
| } | |
| return { | |
| routerPath, | |
| uriPath, | |
| schema: pathSchema, | |
| params | |
| }; | |
| }; | |
| const layerHandlers = (serverInfo, options) => ClientRpcs.toLayer(Effect.gen(function* () { | |
| const server = yield* McpServer; | |
| let currentLogLevel = yield* CurrentLogLevel; | |
| return ClientRpcs.of({ | |
| // Requests | |
| ping: () => Effect.succeed({}), | |
| initialize(params, { | |
| client | |
| }) { | |
| const requestedVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(params.protocolVersion) ? params.protocolVersion : LATEST_PROTOCOL_VERSION; | |
| if (requestedVersion !== params.protocolVersion) { | |
| params = { | |
| ...params, | |
| protocolVersion: requestedVersion | |
| }; | |
| } | |
| const capabilities = { | |
| completions: {} | |
| }; | |
| if (server.tools.length > 0) { | |
| capabilities.tools = { | |
| listChanged: true | |
| }; | |
| } | |
| if (server.resources.length > 0 || server.resourceTemplates.length > 0) { | |
| capabilities.resources = { | |
| listChanged: true, | |
| subscribe: false | |
| }; | |
| } | |
| if (server.prompts.length > 0) { | |
| capabilities.prompts = { | |
| listChanged: true | |
| }; | |
| } | |
| if (serverInfo.extensions) { | |
| capabilities.extensions = serverInfo.extensions; | |
| } | |
| return Effect.withFiber(fiber => { | |
| const httpRequest = Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest); | |
| if (httpRequest) { | |
| const sessionId = crypto.randomUUID(); | |
| options.clientSessions.set(sessionId, params); | |
| appendPreResponseHandlerUnsafe(httpRequest, (_req, res) => Effect.succeed(HttpServerResponse.setHeaders(res, { | |
| [mcpSessionIdHeader]: sessionId, | |
| [mcpProtocolVersionHeader]: requestedVersion | |
| }))); | |
| } else { | |
| options.clientSessions.set(String(client.id), params); | |
| } | |
| return Effect.succeed({ | |
| capabilities, | |
| serverInfo, | |
| protocolVersion: requestedVersion | |
| }); | |
| }); | |
| }, | |
| "completion/complete": r => server.completion(r).pipe(Effect.provideService(CurrentLogLevel, currentLogLevel)), | |
| "logging/setLevel": ({ | |
| level | |
| }) => Effect.sync(() => { | |
| switch (level) { | |
| case "notice": | |
| case "info": | |
| currentLogLevel = "Info"; | |
| break; | |
| case "error": | |
| currentLogLevel = "Error"; | |
| break; | |
| case "debug": | |
| currentLogLevel = "Debug"; | |
| break; | |
| case "warning": | |
| currentLogLevel = "Warn"; | |
| break; | |
| case "critical": | |
| case "alert": | |
| case "emergency": | |
| currentLogLevel = "Fatal"; | |
| break; | |
| } | |
| }), | |
| "prompts/get": r => server.getPromptResult(r).pipe(Effect.provideService(CurrentLogLevel, currentLogLevel)), | |
| "prompts/list": (_, { | |
| client, | |
| headers | |
| }) => Effect.sync(() => { | |
| const initialized = getInitializedClient(options.clientSessions, client.id, headers); | |
| return new ListPromptsResult({ | |
| prompts: filterByClient(initialized, server.prompts, "prompt") | |
| }); | |
| }), | |
| "resources/list": (_, { | |
| client, | |
| headers | |
| }) => Effect.sync(() => { | |
| const initialized = getInitializedClient(options.clientSessions, client.id, headers); | |
| return new ListResourcesResult({ | |
| resources: filterByClient(initialized, server.resources, "resource") | |
| }); | |
| }), | |
| "resources/read": ({ | |
| uri | |
| }) => server.findResource(uri).pipe(Effect.provideService(CurrentLogLevel, currentLogLevel)), | |
| "resources/subscribe": () => InternalError.notImplemented, | |
| "resources/unsubscribe": () => InternalError.notImplemented, | |
| "resources/templates/list": (_, { | |
| client, | |
| headers | |
| }) => Effect.sync(() => { | |
| const initialized = getInitializedClient(options.clientSessions, client.id, headers); | |
| return new ListResourceTemplatesResult({ | |
| resourceTemplates: filterByClient(initialized, server.resourceTemplates, "template") | |
| }); | |
| }), | |
| "tools/call": r => server.callTool(r).pipe(Effect.provideService(CurrentLogLevel, currentLogLevel)), | |
| "tools/list": (_, { | |
| client, | |
| headers | |
| }) => Effect.sync(() => { | |
| const initialized = getInitializedClient(options.clientSessions, client.id, headers); | |
| return new ListToolsResult({ | |
| tools: filterByClient(initialized, server.tools, "tool") | |
| }); | |
| }), | |
| // Notifications | |
| "notifications/cancelled": _ => Effect.void, | |
| "notifications/initialized": _ => Effect.void, | |
| "notifications/progress": _ => Effect.void, | |
| "notifications/roots/list_changed": _ => Effect.void | |
| }); | |
| })); | |
| const resolveResourceContent = (uri, content) => { | |
| if (typeof content === "string") { | |
| return { | |
| contents: [{ | |
| uri, | |
| text: content | |
| }] | |
| }; | |
| } else if (content instanceof Uint8Array) { | |
| return { | |
| contents: [{ | |
| uri, | |
| blob: content | |
| }] | |
| }; | |
| } | |
| return content; | |
| }; | |
| const filterByClient = (client, items, prop) => { | |
| if (!client) { | |
| return items.map(item => item[prop]); | |
| } | |
| const out = Arr.empty(); | |
| for (let i = 0; i < items.length; i++) { | |
| const item = items[i]; | |
| const enabledWhen = Context.getOrUndefined(item.annotations, EnabledWhen); | |
| if (!enabledWhen || enabledWhen(client)) { | |
| out.push(item[prop]); | |
| } | |
| } | |
| return out; | |
| }; | |
| const getInitializedClient = (sessions, clientId, headers) => { | |
| const sessionId = headers[mcpSessionIdHeader]; | |
| if (sessionId === undefined) { | |
| return sessions.get(String(clientId)); | |
| } | |
| return sessions.get(sessionId); | |
| }; | |
| //# sourceMappingURL=McpServer.js.map |
Xet Storage Details
- Size:
- 34.6 kB
- Xet hash:
- af39d8d1849fc51701c5f7c5d06f2bce92e70462460c0a83c8c3757271f63d12
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.