/** * vapor-chamber — Model Context Protocol (MCP) server layer * * Exposes a schema command bus as an MCP server: every schema action becomes * an MCP tool, and `tools/call` requests dispatch through the bus. Zero * dependencies — the JSON-RPC 2.0 / MCP handshake is implemented inline, no * SDK required. * * Three layers, use what you need: * - `busToMcpTools(schema)` — schema → MCP tool definitions (pure mapping) * - `createMcpHandler(bus)` — transport-agnostic JSON-RPC message handler * - `serveMcpStdio(bus)` — Node-only newline-delimited stdio transport * * @example * import { createSchemaCommandBus } from 'vapor-chamber'; * import { createMcpHandler, agentOrigin, serveMcpStdio } from 'vapor-chamber/mcp'; * * const bus = createSchemaCommandBus(schema); * bus.use(agentOrigin(), { priority: 150 }); // stamp meta.origin='agent' on MCP dispatches * bus.register('cartAdd', (cmd) => addToCart(cmd.target.id, cmd.payload.qty)); * * // Wire to any transport (HTTP body, WebSocket message, test harness, ...): * const handle = createMcpHandler(bus, { actions: ['cartAdd', 'cart*'] }); * const reply = await handle(jsonRpcMessage); // null for notifications * * // Or run as a stdio MCP server (e.g. for Claude Desktop / claude_desktop_config.json): * const stop = serveMcpStdio(bus, { actions: ['cart*'] }); */ import type { CommandResult, Plugin } from './command-bus'; import type { BusSchema } from './schema'; /** An MCP tool definition, as returned by the `tools/list` method. */ export type McpTool = { name: string; description?: string; inputSchema: { type: 'object'; properties: Record; required?: string[]; }; }; /** * Convert a BusSchema into MCP tool definitions (the `tools/list` shape). * * Mirrors {@link toAnthropicTools}: each action becomes one tool, with * `target` and `payload` as nested object properties. Field types map 1:1 to * JSON Schema types; `'any'` fields get no type constraint and are excluded * from `required` (all other fields are required). * * @example * const tools = busToMcpTools({ * cartAdd: { description: 'Add item', target: { id: 'number' }, payload: { qty: 'number' } }, * }); * // → [{ name: 'cartAdd', description: 'Add item', inputSchema: { * // type: 'object', * // properties: { * // target: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }, * // payload: { type: 'object', properties: { qty: { type: 'number' } }, required: ['qty'] }, * // }, * // required: ['target', 'payload'], * // } }] */ export declare function busToMcpTools(schema: BusSchema): McpTool[]; /** * Plugin factory: stamps `cmd.meta.origin = 'agent'` on commands dispatched * through an MCP handler ({@link createMcpHandler} / {@link serveMcpStdio}), * and leaves direct `bus.dispatch()` calls untouched. * * Install it with a high priority so the stamp is visible to every other * plugin, hook, and listener in the chain: * * @example * const bus = createSchemaCommandBus(schema); * bus.use(agentOrigin(), { priority: 150 }); * bus.use((cmd, next) => { * if (cmd.meta?.origin === 'agent') auditLog(cmd); // only MCP traffic * return next(); * }); * * Known limitation — the detection is a module-scoped flag set synchronously * around the handler's dispatch call. On a sync bus this is exact. On an * async bus with concurrent mixed traffic (an MCP tool call awaiting an async * handler while local code dispatches on the same bus), an interleaved local * dispatch that enters the plugin chain during that window can be stamped * too. It is best-effort for that scenario; treat `origin === 'agent'` as * advisory, not a security boundary. */ export declare function agentOrigin(): Plugin; /** Minimal bus surface the MCP layer needs — any schema bus (sync or async) satisfies it. */ export type McpBus = { dispatch: (action: string, target: any, payload?: any) => CommandResult | Promise; getSchema: () => BusSchema; }; export type McpHandlerOptions = { /** * Action whitelist — glob patterns matched with {@link matchesPattern} * (`'cart*'`, exact names, or `'*'`). Only matching schema actions are * listed by `tools/list` and callable via `tools/call`. * * Default: ALL schema actions are exposed. That is convenient for demos, * but for anything that mutates state you should pass an explicit * whitelist — an MCP client is an LLM-driven caller, and least privilege * applies: expose reads broadly, writes narrowly. */ actions?: string[]; /** Server name reported by `initialize`. Default: `'vapor-chamber'`. */ serverName?: string; /** Server version reported by `initialize`. Default: `'1.7.0'`. */ serverVersion?: string; }; /** * Create a transport-agnostic MCP message handler for a schema command bus. * * Takes one parsed JSON-RPC 2.0 message, returns the reply object — or `null` * for notifications (messages without an `id`), which MUST NOT be answered. * Wire it to any transport: stdio (see {@link serveMcpStdio}), an HTTP POST * body, a WebSocket frame, or a test harness. * * Protocol methods handled: * - `initialize` — echoes the client's `protocolVersion` (or advertises * `'2025-06-18'`), declares `capabilities: { tools: {} }` * - `notifications/initialized` — notification, no reply * - `ping` — replies `{}` * - `tools/list` — whitelisted schema actions as {@link McpTool}s * - `tools/call` — dispatches `{ target, payload }` from `params.arguments` * through the bus; the CommandResult is serialized as a text content * block (`result.value` as JSON on success; `error.message` with * `isError: true` on failure — tool errors are results, not JSON-RPC errors) * - anything else with an `id` — JSON-RPC error `-32601` (method not found) * * Origin stamping: install {@link agentOrigin} on the bus * (`bus.use(agentOrigin(), { priority: 150 })`) to stamp `meta.origin='agent'` * on MCP-driven dispatches. See {@link agentOrigin} for the concurrency * caveat on async buses. * * @example * const handle = createMcpHandler(bus, { actions: ['cartGet', 'cartAdd'] }); * const reply = await handle({ jsonrpc: '2.0', id: 1, method: 'tools/list' }); * // → { jsonrpc: '2.0', id: 1, result: { tools: [...] } } */ export declare function createMcpHandler(bus: McpBus, options?: McpHandlerOptions): (message: unknown) => Promise; /** * Serve the bus as an MCP server over stdio (Node only): newline-delimited * JSON-RPC 2.0 on `process.stdin` in, `process.stdout` out. This is the * transport MCP clients like Claude Desktop spawn subprocess servers with. * * Unparseable lines get a JSON-RPC `-32700` parse error; everything else is * routed through {@link createMcpHandler}. Returns a `stop()` function that * detaches from stdin. * * IMPORTANT: while serving, do not `console.log` to stdout — it would corrupt * the protocol stream. Log to stderr instead. * * @example * // mcp-server.ts — spawned by an MCP client * const bus = createSchemaCommandBus(schema); * bus.use(agentOrigin(), { priority: 150 }); * registerHandlers(bus); * const stop = serveMcpStdio(bus, { actions: ['cart*', 'productGet'] }); * process.on('SIGTERM', stop); */ export declare function serveMcpStdio(bus: McpBus, options?: McpHandlerOptions): () => void; //# sourceMappingURL=mcp.d.ts.map