UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

1,027 lines (1,026 loc) 31.6 kB
import { $atom, $inject, $module, $state, Alepha, AlephaError, KIND, Primitive, TypeBoxError, createPrimitive, z } from "alepha"; import { $logger } from "alepha/logger"; import { $route } from "alepha/server"; //#region ../../src/mcp/helpers/jsonrpc.ts const JSONRPC_VERSION = "2.0"; /** * The latest MCP protocol revision Alepha targets. * See {@link SUPPORTED_PROTOCOL_VERSIONS} for the full negotiation list. */ const MCP_PROTOCOL_VERSION = "2025-11-25"; /** * Protocol versions Alepha will accept during `initialize` negotiation, * highest preference first. The server echoes back whichever version the * client requested if it appears here, otherwise picks the first entry. */ const SUPPORTED_PROTOCOL_VERSIONS = [ "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05" ]; const isSupportedProtocolVersion = (v) => typeof v === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(v); const JsonRpcErrorCodes = { PARSE_ERROR: -32700, INVALID_REQUEST: -32600, METHOD_NOT_FOUND: -32601, INVALID_PARAMS: -32602, INTERNAL_ERROR: -32603 }; function createResponse(id, result) { return { jsonrpc: "2.0", id, result }; } function createErrorResponse(id, error) { return { jsonrpc: "2.0", id, error }; } function createNotification(method, params) { return { jsonrpc: "2.0", method, params }; } function createParseError(message = "Parse error") { return { code: JsonRpcErrorCodes.PARSE_ERROR, message }; } function createInvalidRequestError(message = "Invalid request") { return { code: JsonRpcErrorCodes.INVALID_REQUEST, message }; } function createMethodNotFoundError(method) { return { code: JsonRpcErrorCodes.METHOD_NOT_FOUND, message: `Method not found: ${method}` }; } function createInvalidParamsError(message) { return { code: JsonRpcErrorCodes.INVALID_PARAMS, message }; } function createInternalError(message) { return { code: JsonRpcErrorCodes.INTERNAL_ERROR, message }; } function parseMessage(data) { let parsed; try { parsed = JSON.parse(data); } catch { throw new JsonRpcParseError("Invalid JSON"); } if (!isValidJsonRpcRequest(parsed)) throw new JsonRpcParseError("Invalid JSON-RPC request"); return parsed; } function isValidJsonRpcRequest(value) { if (typeof value !== "object" || value === null) return false; const obj = value; if (obj.jsonrpc !== "2.0") return false; if (typeof obj.method !== "string") return false; if (obj.id !== void 0 && typeof obj.id !== "string" && typeof obj.id !== "number") return false; if (obj.params !== void 0 && typeof obj.params !== "object") return false; return true; } function isNotification(request) { return request.id === void 0; } var JsonRpcParseError = class extends AlephaError { name = "JsonRpcParseError"; }; //#endregion //#region ../../src/mcp/errors/McpError.ts /** * MCP-specific error codes (application-specific codes in the -32000 to -32099 range). */ const McpErrorCodes = { UNAUTHORIZED: -32001, FORBIDDEN: -32003 }; var McpError = class extends Error { name = "McpError"; code; constructor(message, code = JsonRpcErrorCodes.INTERNAL_ERROR) { super(message); this.code = code; } }; var McpMethodNotFoundError = class extends McpError { name = "McpMethodNotFoundError"; constructor(method) { super(`Method not found: ${method}`, JsonRpcErrorCodes.METHOD_NOT_FOUND); } }; var McpToolNotFoundError = class extends McpError { name = "McpToolNotFoundError"; tool; constructor(tool) { super(`Tool not found: ${tool}`, JsonRpcErrorCodes.METHOD_NOT_FOUND); this.tool = tool; } }; var McpResourceNotFoundError = class extends McpError { name = "McpResourceNotFoundError"; uri; constructor(uri) { super(`Resource not found: ${uri}`, JsonRpcErrorCodes.METHOD_NOT_FOUND); this.uri = uri; } }; var McpPromptNotFoundError = class extends McpError { name = "McpPromptNotFoundError"; prompt; constructor(prompt) { super(`Prompt not found: ${prompt}`, JsonRpcErrorCodes.METHOD_NOT_FOUND); this.prompt = prompt; } }; var McpInvalidParamsError = class extends McpError { name = "McpInvalidParamsError"; constructor(message) { super(message, JsonRpcErrorCodes.INVALID_PARAMS); } }; var McpUnauthorizedError = class extends McpError { name = "McpUnauthorizedError"; constructor(message = "Unauthorized") { super(message, McpErrorCodes.UNAUTHORIZED); } }; var McpForbiddenError = class extends McpError { name = "McpForbiddenError"; constructor(message = "Forbidden") { super(message, McpErrorCodes.FORBIDDEN); } }; //#endregion //#region ../../src/mcp/providers/McpServerProvider.ts /** * Core MCP server provider that handles protocol messages. * * This provider maintains registries of tools, resources, and prompts, * and routes incoming JSON-RPC requests to the appropriate handlers. * * It is transport-agnostic - actual communication is handled by * transport providers like StdioMcpTransport or SseMcpTransport. */ var McpServerProvider = class { log = $logger(); alepha = $inject(Alepha); tools = /* @__PURE__ */ new Map(); resources = /* @__PURE__ */ new Map(); prompts = /* @__PURE__ */ new Map(); initialized = false; /** * Protocol version negotiated with the client during `initialize`. * Used by transports to validate the `MCP-Protocol-Version` header on * subsequent HTTP requests (per spec 2025-06-18+). */ negotiatedVersion = MCP_PROTOCOL_VERSION; /** * Server identity returned during `initialize`. Consumers may override * fields directly (e.g. `mcpServer.serverInfo = { name: "lore-mcp", * version: "0.20.3", description: "..." }`) — the `description` field * is supported per spec 2025-11-25 (minor change #2). */ serverInfo = { name: "alepha-mcp", version: "1.0.0" }; /** * Register a tool with the MCP server. */ registerTool(tool) { this.log.trace(`Registering MCP tool: ${tool.name}`); this.tools.set(tool.name, tool); } /** * Register a resource with the MCP server. */ registerResource(resource) { this.log.trace(`Registering MCP resource: ${resource.uri}`); this.resources.set(resource.uri, resource); } /** * Register a prompt with the MCP server. */ registerPrompt(prompt) { this.log.trace(`Registering MCP prompt: ${prompt.name}`); this.prompts.set(prompt.name, prompt); } /** * Get the server capabilities based on registered primitives. */ getCapabilities() { return { tools: this.tools.size > 0 ? {} : void 0, resources: this.resources.size > 0 ? {} : void 0, prompts: this.prompts.size > 0 ? {} : void 0 }; } /** * Get all registered tools. */ getTools() { return Array.from(this.tools.values()); } /** * Get all registered resources. */ getResources() { return Array.from(this.resources.values()); } /** * Get all registered prompts. */ getPrompts() { return Array.from(this.prompts.values()); } /** * Get a tool by name. */ getTool(name) { return this.tools.get(name); } /** * Get a resource by URI. */ getResource(uri) { return this.resources.get(uri); } /** * Get a prompt by name. */ getPrompt(name) { return this.prompts.get(name); } /** * Handle an incoming JSON-RPC request. * * @param request - The parsed JSON-RPC request * @param context - Optional context from the transport layer (headers, auth, etc.) * @returns The JSON-RPC response, or null for notifications */ async handleMessage(request, context) { const id = request.id; if (id === void 0) { await this.handleNotification(request); return null; } try { return createResponse(id, await this.handleRequest(request, context)); } catch (error) { this.log.error("MCP request failed", error); if (error instanceof McpError) return createErrorResponse(id, { code: error.code, message: error.message }); return createErrorResponse(id, createInternalError(error.message)); } } /** * Handle a JSON-RPC request that expects a response. */ async handleRequest(request, context) { const { method, params = {} } = request; switch (method) { case "initialize": return this.handleInitialize(params); case "ping": return this.handlePing(); case "tools/list": return this.handleToolsList(); case "tools/call": return this.handleToolsCall(params, context); case "resources/list": return this.handleResourcesList(); case "resources/read": return this.handleResourcesRead(params, context); case "prompts/list": return this.handlePromptsList(); case "prompts/get": return this.handlePromptsGet(params, context); default: throw new McpMethodNotFoundError(method); } } /** * Handle a notification (no response expected). */ async handleNotification(request) { const { method } = request; switch (method) { case "notifications/initialized": this.log.debug("MCP client initialized"); break; case "notifications/cancelled": this.log.debug("MCP request cancelled", request.params); break; default: this.log.debug(`Unknown MCP notification: ${method}`); } } handleInitialize(params) { const requested = params.protocolVersion; const negotiated = isSupportedProtocolVersion(requested) ? requested : SUPPORTED_PROTOCOL_VERSIONS[0]; this.log.info("MCP client initializing", { clientInfo: params.clientInfo, requestedProtocolVersion: requested, negotiatedProtocolVersion: negotiated }); this.initialized = true; this.negotiatedVersion = negotiated; return { protocolVersion: negotiated, capabilities: this.getCapabilities(), serverInfo: this.serverInfo }; } handlePing() { return {}; } handleToolsList() { return { tools: Array.from(this.tools.values()).map((t) => t.toDescriptor()) }; } async handleToolsCall(params, context) { const name = params.name; const args = params.arguments ?? {}; const tool = this.tools.get(name); if (!tool) throw new McpToolNotFoundError(name); try { const result = await tool.execute(args, context); if (!tool.hasOutputSchema()) { const raw = this.asRawToolContent(result); if (raw) return raw; } const callResult = { content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result ?? null) }] }; if (tool.hasOutputSchema() && result !== void 0) callResult.structuredContent = result; return callResult; } catch (error) { if (error instanceof TypeBoxError) { const path = error.value?.path || "/"; const message = error.value?.message || error.message; return { content: [{ type: "text", text: `Validation error at ${path}: ${message}` }], structuredContent: { errors: [{ path, message }] }, isError: true }; } return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true }; } } /** * Recognize a tool handler's return value as a pre-built MCP tool result — * i.e. it already carries a `content` array of content blocks (text, image, * audio, resource, resource_link). Returns the normalized * {@link McpToolCallResult} when matched, or `undefined` to fall back to the * default JSON/text encoding. Only ever consulted for tools that did NOT * declare an output schema (see {@link handleToolCall}). */ asRawToolContent(result) { if (!result || typeof result !== "object") return; const candidate = result; if (!Array.isArray(candidate.content) || candidate.content.length === 0) return; if (!candidate.content.every((block) => !!block && typeof block === "object" && typeof block.type === "string")) return; return { content: candidate.content, isError: typeof candidate.isError === "boolean" ? candidate.isError : void 0, _meta: candidate._meta && typeof candidate._meta === "object" ? candidate._meta : void 0 }; } handleResourcesList() { return { resources: Array.from(this.resources.values()).map((r) => r.toDescriptor()) }; } async handleResourcesRead(params, context) { const uri = params.uri; const resource = this.resources.get(uri); if (!resource) throw new McpResourceNotFoundError(uri); const content = await resource.read(context); const resourceContent = { uri, mimeType: resource.mimeType }; if (content.text !== void 0) resourceContent.text = content.text; if (content.blob !== void 0) resourceContent.blob = Buffer.from(content.blob).toString("base64"); return { contents: [resourceContent] }; } handlePromptsList() { return { prompts: Array.from(this.prompts.values()).map((p) => p.toDescriptor()) }; } async handlePromptsGet(params, context) { const name = params.name; const args = params.arguments ?? {}; const prompt = this.prompts.get(name); if (!prompt) throw new McpPromptNotFoundError(name); const mcpMessages = (await prompt.get(args, context)).map((msg) => ({ role: msg.role, content: { type: "text", text: msg.content } })); return { description: prompt.description, messages: mcpMessages }; } }; //#endregion //#region ../../src/mcp/primitives/$prompt.ts /** * Creates an MCP prompt primitive for defining reusable prompt templates. * * Prompts allow you to define templated messages that can be filled in * with arguments at runtime. They're useful for creating consistent * interaction patterns. * * @example * ```ts * class Prompts { * greeting = $prompt({ * description: "Generate a personalized greeting", * args: z.object({ * name: z.text({ description: "Name of the person to greet" }), * style: z.enum(["formal", "casual"]).optional(), * }), * handler: async ({ args }) => [ * { * role: "user", * content: args.style === "formal" * ? `Please greet ${args.name} in a formal manner.` * : `Say hi to ${args.name}!`, * }, * ], * }); * * codeReview = $prompt({ * description: "Request a code review", * args: z.object({ * code: z.text({ description: "The code to review" }), * language: z.text({ description: "Programming language" }), * }), * handler: async ({ args }) => [ * { * role: "user", * content: `Please review this ${args.language} code:\n\n${args.code}`, * }, * ], * }); * } * ``` */ const $prompt = (options) => { return createPrimitive(PromptPrimitive, options); }; var PromptPrimitive = class extends Primitive { mcpServer = $inject(McpServerProvider); /** * Returns the name of the prompt. */ get name() { return this.options.name ?? this.config.propertyKey; } /** * Returns the description of the prompt. */ get description() { return this.options.description; } onInit() { this.mcpServer.registerPrompt(this); } /** * Get the prompt messages with the given arguments. * * @param rawArgs - Raw arguments to validate and pass to the handler * @param context - Optional context from the transport layer * @returns Array of prompt messages */ async get(rawArgs, context) { let args = rawArgs ?? {}; if (this.options.args) args = this.alepha.codec.decode(this.options.args, rawArgs ?? {}); return this.options.handler({ args, context }); } /** * Convert the prompt to an MCP prompt descriptor for protocol messages. */ toDescriptor() { const descriptor = { name: this.name, description: this.description, arguments: this.options.args ? this.schemaToArguments(this.options.args) : [] }; if (this.options.title) descriptor.title = this.options.title; if (this.options.icons && this.options.icons.length > 0) descriptor.icons = this.options.icons; return descriptor; } /** * Convert a TypeBox schema to an array of prompt arguments. */ schemaToArguments(schema) { const args = []; for (const [name, propSchema] of Object.entries(schema.properties)) { const prop = z.schema.unwrap(propSchema); args.push({ name, description: prop.description, required: !z.schema.isOptional(propSchema) }); } return args; } }; $prompt[KIND] = PromptPrimitive; //#endregion //#region ../../src/mcp/primitives/$resource.ts /** * Creates an MCP resource primitive for exposing read-only data. * * Resources represent any kind of data that an LLM might want to read, * such as files, database records, API responses, or computed data. * * **Key Features** * - URI-based identification for resources * - Support for text and binary content * - MIME type specification * - Lazy loading via handler function * * @example * ```ts * class ProjectResources { * readme = $resource({ * uri: "file:///readme", * description: "Project README file", * mimeType: "text/markdown", * handler: async () => ({ * text: await fs.readFile("README.md", "utf-8"), * }), * }); * * config = $resource({ * uri: "config://app", * name: "Application Configuration", * mimeType: "application/json", * handler: async () => ({ * text: JSON.stringify(this.configService.getConfig()), * }), * }); * } * ``` */ const $resource = (options) => { return createPrimitive(ResourcePrimitive, options); }; var ResourcePrimitive = class extends Primitive { mcpServer = $inject(McpServerProvider); /** * Returns the name of the resource. */ get name() { return this.options.name ?? this.config.propertyKey; } /** * Returns the URI of the resource. */ get uri() { return this.options.uri; } /** * Returns the description of the resource. */ get description() { return this.options.description; } /** * Returns the MIME type of the resource. */ get mimeType() { return this.options.mimeType ?? "text/plain"; } onInit() { this.mcpServer.registerResource(this); } /** * Read the resource content. * * @param context - Optional context from the transport layer * @returns The resource content */ async read(context) { return this.options.handler({ context }); } /** * Convert the resource to an MCP resource descriptor for protocol messages. */ toDescriptor() { const descriptor = { uri: this.uri, name: this.name, description: this.description, mimeType: this.mimeType }; if (this.options.title) descriptor.title = this.options.title; if (this.options.icons && this.options.icons.length > 0) descriptor.icons = this.options.icons; return descriptor; } }; $resource[KIND] = ResourcePrimitive; //#endregion //#region ../../src/mcp/primitives/$tool.ts /** * Creates an MCP tool primitive for defining callable functions. * * Tools are the primary way for LLMs to interact with external systems through MCP. * Each tool has a name, description, typed parameters, and a handler function. * * **Key Features** * - Full TypeScript inference for parameters and results * - Automatic schema validation using TypeBox * - JSON Schema generation for MCP protocol * - Integration with MCP server provider * * @example * ```ts * class CalculatorTools { * add = $tool({ * description: "Add two numbers together", * schema: { * params: z.object({ * a: z.number(), * b: z.number(), * }), * result: z.number(), * }, * handler: async ({ params }) => { * return params.a + params.b; * }, * }); * * greet = $tool({ * description: "Generate a greeting message", * schema: { * params: z.object({ * name: z.text(), * }), * result: z.text(), * }, * handler: async ({ params }) => { * return `Hello, ${params.name}!`; * }, * }); * } * ``` */ const $tool = (options) => { return createPrimitive(ToolPrimitive, options); }; var ToolPrimitive = class extends Primitive { mcpServer = $inject(McpServerProvider); /** * Returns the name of the tool. */ get name() { return this.options.name ?? this.config.propertyKey; } /** * Returns the description of the tool. */ get description() { return this.options.description; } /** * Whether the tool declared a result schema. When true, `tools/call` * responses include `structuredContent` populated with the validated * result (spec 2025-06-18). */ hasOutputSchema() { return !!this.options.schema?.result; } onInit() { this.mcpServer.registerTool(this); } /** * Execute the tool with the given parameters. * * @param params - Raw parameters to validate and pass to the handler * @param context - Optional context from the transport layer * @returns The tool result */ async execute(params, context) { let validatedParams = params ?? {}; if (this.options.schema?.params) validatedParams = this.alepha.codec.decode(this.options.schema.params, validatedParams); const result = await this.options.handler({ params: validatedParams, context }); if (this.options.schema?.result && result !== void 0) return this.alepha.codec.encode(this.options.schema.result, result); return result; } /** * Convert the tool to an MCP tool descriptor for protocol messages. * * Emits the spec 2025-11-25 surface: `title`, `annotations`, `icons`, * and (when `schema.result` is defined) `outputSchema` so the server * can populate `structuredContent` on call results. */ toDescriptor() { const inputSchema = this.options.schema?.params ? this.schemaToJsonSchema(this.options.schema.params) : { type: "object", properties: {}, required: [] }; const descriptor = { name: this.name, description: this.description, inputSchema }; if (this.options.title) descriptor.title = this.options.title; if (this.options.annotations) descriptor.annotations = this.options.annotations; if (this.options.icons && this.options.icons.length > 0) descriptor.icons = this.options.icons; if (this.options.schema?.result) { const out = this.propertyToJsonSchema(this.options.schema.result); descriptor.outputSchema = typeof out === "object" && out !== null && "type" in out ? out : { type: "object", properties: {}, required: [] }; } return descriptor; } /** * Convert a TypeBox schema to JSON Schema format. * * Emits the 2020-12 dialect annotation at the root (spec 2025-11-25 / * SEP-1613 — JSON Schema 2020-12 is the default dialect for MCP). * The TypeBox shapes Alepha emits today are already 2020-12-compatible; * this is just the dialect declaration. */ schemaToJsonSchema(schema, options) { let json; try { json = z.toJSONSchema(schema); } catch { json = { type: "object", properties: {}, required: [] }; } if (options?.root === false) json.$schema = void 0; else json.$schema = "https://json-schema.org/draft/2020-12/schema"; return json; } /** * Convert a single property schema to JSON Schema format (zod-native). */ propertyToJsonSchema(schema) { try { const json = z.toJSONSchema(schema); json.$schema = void 0; return json; } catch { return { type: "string" }; } } }; $tool[KIND] = ToolPrimitive; //#endregion //#region ../../src/mcp/transports/StreamableHttpMcpTransport.ts const mcpStreamableHttpOptions = $atom({ name: "alepha.mcp.streamableHttp.options", description: "Configuration options for the MCP Streamable HTTP transport.", schema: z.object({ /** * Path for the MCP endpoint. Single endpoint for both requests and * (optional) server-streamed responses, per spec 2025-03-26+. */ path: z.text({ default: "/mcp" }), /** * Allow-list of `Origin` header values accepted on incoming requests. * Empty array (default) means "allow any". When set, browser-originated * requests with a non-matching `Origin` are rejected with 403 Forbidden, * blocking DNS-rebinding attacks against localhost MCP servers. * * Server-to-server callers (no `Origin` header) are always allowed. * * Spec 2025-11-25, PR #1439. */ allowedOrigins: z.array(z.text()).default([]), /** * When true, an unauthenticated POST to the MCP endpoint is rejected * with `401 Unauthorized` and an RFC 9728 `WWW-Authenticate` challenge * instead of being dispatched. MCP clients (Claude, etc.) rely on that * challenge to discover the OAuth authorization server. * * The transport stays OAuth-agnostic: it only knows "auth is required". * `$realm({ features: { oauth: true } })` flips this on automatically. * * @default false */ requireAuth: z.boolean().default(false), /** * Path of the RFC 9728 protected-resource metadata document, advertised * (as an absolute URL, resolved against the request origin) in the * `WWW-Authenticate` challenge emitted when {@link requireAuth} rejects * a request. */ resourceMetadataPath: z.text({ default: "/.well-known/oauth-protected-resource" }) }), default: { path: "/mcp", allowedOrigins: [], requireAuth: false, resourceMetadataPath: "/.well-known/oauth-protected-resource" } }); const mcpSseOptions = mcpStreamableHttpOptions; /** * Streamable HTTP transport for MCP communication. * * Implements the 2025-03-26+ Streamable HTTP transport: a single `/mcp` * endpoint that accepts JSON-RPC over POST and returns either * `application/json` (single response, the default) or * `text/event-stream` (when the server wants to stream multiple messages). * * Designed for serverless deployment (Cloudflare Workers, etc.) — there is * no long-lived GET stream. GET on the endpoint returns 405 Method Not * Allowed; clients that want server-initiated push must rely on the POST * response stream when the server upgrades to SSE for that particular call. * * Spec compliance: * - 2025-06-18: validates `MCP-Protocol-Version` header on every request * after `initialize` against the version negotiated and stored on * `McpServerProvider`. * - 2025-11-25: rejects requests with a non-allow-listed `Origin` header * (PR #1439). See {@link mcpStreamableHttpOptions.allowedOrigins}. * * @example * ```ts * import { Alepha, run } from "alepha"; * import { AlephaServer } from "alepha/server"; * import { AlephaMcp, StreamableHttpMcpTransport } from "alepha/mcp"; * * class MyTools { * // ... tool definitions * } * * run( * Alepha.create() * .with(AlephaServer) * .with(AlephaMcp) * .with(StreamableHttpMcpTransport) * .with(MyTools) * ); * ``` */ var StreamableHttpMcpTransport = class { log = $logger(); options = $state(mcpStreamableHttpOptions); mcpServer = $inject(McpServerProvider); /** * GET on the MCP endpoint is not supported in this transport. Returning * 405 (rather than serving the legacy two-endpoint SSE pattern) is the * spec-allowed response for servers that don't offer server-initiated * push outside of an active POST. */ notAllowed = $route({ method: "GET", path: this.options.path, handler: (request) => { request.reply.status = 405; request.reply.headers.allow = "POST"; request.reply.headers["content-type"] = "application/json"; request.reply.body = JSON.stringify({ error: "Method Not Allowed. Use POST for MCP messages." }); } }); /** * POST endpoint for client-to-server JSON-RPC messages. * Returns `application/json` for single responses; tools that need to * stream progress would upgrade to `text/event-stream` (deferred until a * concrete need exists). */ message = $route({ method: "POST", path: this.options.path, schema: { body: z.json() }, handler: async (request) => { try { const originRaw = request.headers.origin; const origin = Array.isArray(originRaw) ? originRaw[0] : originRaw; if (origin && this.options.allowedOrigins.length > 0 && !this.options.allowedOrigins.includes(origin)) { this.log.warn("Rejected MCP request with non-allowed Origin", { origin, allowed: this.options.allowedOrigins }); request.reply.status = 403; request.reply.headers["content-type"] = "application/json"; request.reply.body = JSON.stringify({ error: "Forbidden: Origin not allowed" }); return; } if (this.options.requireAuth && !request.user) { const url = typeof request.url === "string" ? new URL(request.url) : request.url; const resourceMetadataUrl = `${url.protocol}//${url.host}${this.options.resourceMetadataPath}`; this.log.debug("Rejecting unauthenticated MCP request", { resourceMetadataUrl }); request.reply.status = 401; request.reply.headers["www-authenticate"] = `Bearer resource_metadata="${resourceMetadataUrl}"`; request.reply.headers["content-type"] = "application/json"; request.reply.body = JSON.stringify({ error: "Unauthorized" }); return; } const body = typeof request.body === "string" ? request.body : JSON.stringify(request.body); this.log.debug("MCP request body", { body, bodyType: typeof request.body }); const rpcRequest = parseMessage(body); const headers = { ...request.headers }; if (rpcRequest.method !== "initialize") { const headerRaw = headers["mcp-protocol-version"]; const headerVersion = Array.isArray(headerRaw) ? headerRaw[0] : headerRaw; if (headerVersion && headerVersion !== this.mcpServer.negotiatedVersion) { this.log.warn("MCP-Protocol-Version header mismatch", { header: headerVersion, negotiated: this.mcpServer.negotiatedVersion }); request.reply.status = 400; request.reply.headers["content-type"] = "application/json"; request.reply.body = JSON.stringify({ error: `MCP-Protocol-Version mismatch: expected ${this.mcpServer.negotiatedVersion}, got ${headerVersion}` }); return; } } const context = { headers }; const response = await this.mcpServer.handleMessage(rpcRequest, context); if (response) { request.reply.headers["content-type"] = "application/json"; request.reply.body = JSON.stringify(response); } else request.reply.status = 204; } catch (error) { if (error instanceof JsonRpcParseError) { request.reply.status = 400; request.reply.headers["content-type"] = "application/json"; request.reply.body = JSON.stringify(createErrorResponse(0, createParseError(error.message))); } else { this.log.error("Failed to process MCP message", error); request.reply.status = 500; request.reply.body = JSON.stringify({ error: error.message }); } } } }); }; /** * @deprecated Use {@link StreamableHttpMcpTransport}. The 2024-11-05 * two-endpoint HTTP+SSE pattern was replaced by Streamable HTTP in spec * 2025-03-26. This alias is preserved for one release to ease migration. */ const SseMcpTransport = StreamableHttpMcpTransport; //#endregion //#region ../../src/mcp/index.ts /** * Model Context Protocol for AI tool integration. * * **Features:** * - MCP resource definitions * - MCP tool definitions * - MCP prompt definitions * - JSON-RPC protocol * - Streamable HTTP transport (spec 2025-03-26+) * * @module alepha.mcp */ const AlephaMcp = $module({ name: "alepha.mcp", primitives: [ $tool, $resource, $prompt ], services: [McpServerProvider], variants: [StreamableHttpMcpTransport] }); //#endregion export { $prompt, $resource, $tool, AlephaMcp, JSONRPC_VERSION, JsonRpcErrorCodes, JsonRpcParseError, MCP_PROTOCOL_VERSION, McpError, McpErrorCodes, McpForbiddenError, McpInvalidParamsError, McpMethodNotFoundError, McpPromptNotFoundError, McpResourceNotFoundError, McpServerProvider, McpToolNotFoundError, McpUnauthorizedError, PromptPrimitive, ResourcePrimitive, SUPPORTED_PROTOCOL_VERSIONS, SseMcpTransport, StreamableHttpMcpTransport, ToolPrimitive, createErrorResponse, createInternalError, createInvalidParamsError, createInvalidRequestError, createMethodNotFoundError, createNotification, createParseError, createResponse, isNotification, isSupportedProtocolVersion, isValidJsonRpcRequest, mcpSseOptions, mcpStreamableHttpOptions, parseMessage }; //# sourceMappingURL=index.js.map