agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
130 lines • 5.4 kB
JavaScript
;
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.mockMcpClient = void 0;
const toolExtras_js_1 = require("./toolExtras.js");
const terminate_js_1 = require("./terminate.js");
const toolResult_js_1 = require("./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.
*/
function mockMcpClient(options) {
const name = options.name ?? 'mock-mcp';
const resultMode = (0, toolResult_js_1.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 = (0, terminate_js_1.outcomeWithout)('no-session');
return ended;
},
};
}
exports.mockMcpClient = mockMcpClient;
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.
...(0, toolExtras_js_1.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((0, toolExtras_js_1.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 (0, toolResult_js_1.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