UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

126 lines • 5.18 kB
/** * mockMcpClient — in-memory MCP client for development and tests. * * const slack = mockMcpClient({ * tools: [ * { * name: 'send_message', * description: 'Post a message to a channel', * inputSchema: { type: 'object' }, * handler: async ({ text }) => `Posted: ${text}`, * }, * ], * }); * * const agent = Agent.create({ provider: mock({ reply: 'ok' }) }) * .tools(await slack.tools()) * .build(); * * Pattern: Adapter (GoF) — produces an `McpClient` with the same shape * as `mcpClient(opts)` but driven by an in-memory tool table * instead of the MCP SDK + transport. Drop-in for development: * start with `mockMcpClient`, swap to `mcpClient` once the * real server is ready. * * Why public: `mcpClient`'s `_client` injection is `@internal` because * the SDK shape isn't a stable public surface. `mockMcpClient` exposes * a curated tool-handler shape that's tied to OUR Tool contract instead * — stable, documented, and the right level of abstraction for * mock-first development. */ import { callMetaOf, readToolExtras } from './toolExtras.js'; import { outcomeWithout } from './terminate.js'; import { readToolResult, resultModeOf } from './toolResult.js'; /** * Build an in-memory `McpClient`. Useful when you want to develop * against MCP semantics without spawning subprocesses, hitting the * network, or installing `@modelcontextprotocol/sdk`. Same `McpClient` * shape as `mcpClient(opts)` — code that consumes one accepts the other. */ export function mockMcpClient(options) { const name = options.name ?? 'mock-mcp'; const resultMode = resultModeOf(options.resultMode, name); const toolMap = new Map(options.tools.map((t) => [t.name, t])); let cache = null; let closed = false; let ended; const ensureOpen = (op) => { if (closed) { throw new Error(`mockMcpClient[${name}].${op}() called after close(). Construct a new client to reuse.`); } }; const buildTools = () => options.tools.map((mcp) => wrapMockTool(name, toolMap, mcp, resultMode)); return { name, async tools() { ensureOpen('tools'); if (!cache) cache = buildTools(); return cache; }, async refresh() { ensureOpen('refresh'); cache = buildTools(); return cache; }, async close() { if (closed) return; closed = true; cache = null; }, // In memory, no session id is ever issued: nothing remote to end, so the // ending is resolved, and the client is closed (`terminate.ts`). async terminate() { if (ended !== undefined) return ended; ensureOpen('terminate'); closed = true; cache = null; ended = outcomeWithout('no-session'); return ended; }, }; } function wrapMockTool(serverName, toolMap, mcp, resultMode) { const tool = { schema: { name: mcp.name, description: mcp.description ?? `Mock MCP tool: ${mcp.name}`, inputSchema: mcp.inputSchema, }, // Same provenance a real `mcpClient` stamps, for the same reason: code that // governs by server name has to be testable against the mock, or the swap // from mock to real server changes what the policy sees. source: serverName, // …and the same declarations, through the same reader (9.71.0). A rail // that arms on a remotely-declared `argumentsFrom` must arm here too, or // the mock proves nothing about the server it stands in for. ...readToolExtras(mcp._meta, { server: serverName, tool: mcp.name, inputSchema: mcp.inputSchema, }), execute: async (args, ctx) => { const argsObj = args !== null && typeof args === 'object' && !Array.isArray(args) ? args : {}; // The same request `_meta` the real client sends (the time layer's `ctx.time`), as data. const request = ctx?.time !== undefined ? { _meta: structuredClone(callMetaOf(ctx.time)) } : {}; // Look up by name at call time so mid-test handler swaps via a // mutable Map could be supported later. For now `toolMap` is // built once at factory time. const handler = toolMap.get(mcp.name)?.handler; try { const result = handler ? await handler(argsObj, request) : '[mock result]'; return readToolResult(typeof result === 'string' ? { content: [{ type: 'text', text: result }] } : result, mcp.name, serverName, resultMode); } catch (err) { const msg = err instanceof Error ? err.message : String(err); throw new Error(`Mock MCP tool '${mcp.name}' (server '${serverName}') threw: ${msg}`); } }, }; return tool; } //# sourceMappingURL=mockMcpClient.js.map