From 4d786f02488844152d76d19957e44724f3be5931 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare Date: Mon, 11 May 2026 14:32:18 +0100 Subject: [PATCH] fix(mcp): local fallback exposes all 7 IMPLEMENTED_TOOLS, not 4 (#234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported live in Cursor: agentmemory MCP server shows exactly four tools — memory_recall, memory_save, memory_sessions, memory_smart_search — when no agentmemory server is running on localhost:3111. Should be seven: those four plus memory_export, memory_audit, memory_governance_delete (the full InMemoryKV-backed local capability set the shim ships). Root cause: the local-fallback branch of tools/list in src/mcp/standalone.ts intersected the shim's IMPLEMENTED_TOOLS set (7) with getVisibleTools(), which honors the shim's own AGENTMEMORY_TOOLS env. Default env is unset, so getVisibleTools() returns the 8 ESSENTIAL_TOOLS (the server's default "core" tier). The 4 visible tools are exactly that intersection: ESSENTIAL_TOOLS ∩ IMPLEMENTED_TOOLS = {memory_save, memory_recall, memory_smart_search, memory_sessions} The intersection was meaningless: the shim doesn't dispatch by ESSENTIAL_TOOLS, it dispatches by IMPLEMENTED_TOOLS. Whatever AGENTMEMORY_TOOLS the SERVER honored shouldn't shape the SHIM's local-fallback list — the shim's local capabilities are fixed by its in-memory KV, not by an env knob. Fix: switch the filter source from getVisibleTools() to getAllTools(). The filter through IMPLEMENTED_TOOLS still picks the right 7 names, but now from the unfiltered universe of tool definitions instead of the env-filtered subset. Refactored the tools/list handler out of the inline createStdioTransport callback into an exported handleToolsList() so it's directly testable. Added a regression test that asserts the local-fallback path returns exactly the 7 IMPLEMENTED_TOOLS regardless of AGENTMEMORY_TOOLS=core (default) or unset. Verified live via stdio against a downed server: shim now returns all 7 names. Verified the previous bug repro (4 names) is gone. 860 / 860 tests pass on the branch (859 baseline + 1 regression test). --- src/mcp/standalone.ts | 105 +++++++++++++++--------------- test/mcp-standalone-proxy.test.ts | 26 ++++++++ 2 files changed, 80 insertions(+), 51 deletions(-) diff --git a/src/mcp/standalone.ts b/src/mcp/standalone.ts index 6026f715..ae1f4f36 100644 --- a/src/mcp/standalone.ts +++ b/src/mcp/standalone.ts @@ -2,7 +2,7 @@ import { InMemoryKV } from "./in-memory-kv.js"; import { createStdioTransport } from "./transport.js"; -import { getVisibleTools } from "./tools-registry.js"; +import { getAllTools } from "./tools-registry.js"; import { getStandalonePersistPath } from "../config.js"; import { VERSION } from "../version.js"; import { generateId } from "../state/schema.js"; @@ -354,6 +354,57 @@ export async function handleToolCall( return handleLocal(validated, kvInstance); } +export async function handleToolsList(): Promise<{ tools: unknown[] }> { + const debug = process.env["AGENTMEMORY_DEBUG"] === "1" || process.env["AGENTMEMORY_DEBUG"] === "true"; + const handle = await resolveHandle(); + announceMode(handle); + if (debug) { + process.stderr.write( + `[@agentmemory/mcp] tools/list: handle.mode=${handle.mode}${handle.mode === "proxy" ? ` baseUrl=${handle.baseUrl}` : ""}\n`, + ); + } + if (handle.mode === "proxy") { + try { + const remote = (await handle.call("/agentmemory/mcp/tools", { + method: "GET", + })) as { tools?: unknown } | null; + if (debug) { + const shape = remote === null + ? "null" + : typeof remote !== "object" + ? typeof remote + : `keys=${Object.keys(remote as object).join(",")} toolsType=${Array.isArray((remote as { tools?: unknown }).tools) ? `array(len=${((remote as { tools: unknown[] }).tools).length})` : typeof (remote as { tools?: unknown }).tools}`; + process.stderr.write( + `[@agentmemory/mcp] tools/list: remote response shape: ${shape}\n`, + ); + } + if (remote && Array.isArray(remote.tools)) { + if (debug) { + process.stderr.write( + `[@agentmemory/mcp] tools/list: returning ${remote.tools.length} tools from server\n`, + ); + } + return { tools: remote.tools }; + } + process.stderr.write( + `[@agentmemory/mcp] tools/list: server returned unexpected shape (no .tools array); falling back to local IMPLEMENTED_TOOLS list. Set AGENTMEMORY_DEBUG=1 to inspect response.\n`, + ); + } catch (err) { + process.stderr.write( + `[@agentmemory/mcp] tools/list proxy failed: ${err instanceof Error ? err.message : String(err)}; falling back to local list\n`, + ); + invalidateHandle(); + } + } + const fallback = getAllTools().filter((t) => IMPLEMENTED_TOOLS.has(t.name)); + if (debug) { + process.stderr.write( + `[@agentmemory/mcp] tools/list: returning ${fallback.length} local fallback tools (${fallback.map((t) => t.name).join(",")})\n`, + ); + } + return { tools: fallback }; +} + const transport = createStdioTransport(async (method, params) => { switch (method) { case "initialize": @@ -369,56 +420,8 @@ const transport = createStdioTransport(async (method, params) => { case "notifications/initialized": return {}; - case "tools/list": { - const debug = process.env["AGENTMEMORY_DEBUG"] === "1" || process.env["AGENTMEMORY_DEBUG"] === "true"; - const handle = await resolveHandle(); - announceMode(handle); - if (debug) { - process.stderr.write( - `[@agentmemory/mcp] tools/list: handle.mode=${handle.mode}${handle.mode === "proxy" ? ` baseUrl=${handle.baseUrl}` : ""}\n`, - ); - } - if (handle.mode === "proxy") { - try { - const remote = (await handle.call("/agentmemory/mcp/tools", { - method: "GET", - })) as { tools?: unknown } | null; - if (debug) { - const shape = remote === null - ? "null" - : typeof remote !== "object" - ? typeof remote - : `keys=${Object.keys(remote as object).join(",")} toolsType=${Array.isArray((remote as { tools?: unknown }).tools) ? `array(len=${((remote as { tools: unknown[] }).tools).length})` : typeof (remote as { tools?: unknown }).tools}`; - process.stderr.write( - `[@agentmemory/mcp] tools/list: remote response shape: ${shape}\n`, - ); - } - if (remote && Array.isArray(remote.tools)) { - if (debug) { - process.stderr.write( - `[@agentmemory/mcp] tools/list: returning ${remote.tools.length} tools from server\n`, - ); - } - return { tools: remote.tools }; - } - process.stderr.write( - `[@agentmemory/mcp] tools/list: server returned unexpected shape (no .tools array); falling back to local IMPLEMENTED_TOOLS list. Set AGENTMEMORY_DEBUG=1 to inspect response.\n`, - ); - } catch (err) { - process.stderr.write( - `[@agentmemory/mcp] tools/list proxy failed: ${err instanceof Error ? err.message : String(err)}; falling back to local list\n`, - ); - invalidateHandle(); - } - } - const fallback = getVisibleTools().filter((t) => IMPLEMENTED_TOOLS.has(t.name)); - if (debug) { - process.stderr.write( - `[@agentmemory/mcp] tools/list: returning ${fallback.length} local fallback tools (${fallback.map((t) => t.name).join(",")})\n`, - ); - } - return { tools: fallback }; - } + case "tools/list": + return handleToolsList(); case "tools/call": { const toolName = params.name as string; diff --git a/test/mcp-standalone-proxy.test.ts b/test/mcp-standalone-proxy.test.ts index debc7eb9..6522691d 100644 --- a/test/mcp-standalone-proxy.test.ts +++ b/test/mcp-standalone-proxy.test.ts @@ -247,6 +247,32 @@ describe("@agentmemory/mcp standalone — server proxy (issue #159)", () => { expect(joined).toMatch(/AGENTMEMORY_FORCE_PROXY/); }); + it("local fallback tools/list returns all 7 IMPLEMENTED_TOOLS regardless of AGENTMEMORY_TOOLS env (#234)", async () => { + const { handleToolsList } = await import("../src/mcp/standalone.js"); + installFetch(() => { + throw new Error("ECONNREFUSED"); + }); + delete process.env["AGENTMEMORY_TOOLS"]; + const before = await handleToolsList(); + const beforeTools = before.tools as Array<{ name: string }>; + expect(beforeTools.map((t) => t.name).sort()).toEqual([ + "memory_audit", + "memory_export", + "memory_governance_delete", + "memory_recall", + "memory_save", + "memory_sessions", + "memory_smart_search", + ]); + expect(beforeTools).toHaveLength(7); + + resetHandleForTests(); + process.env["AGENTMEMORY_TOOLS"] = "core"; + const core = await handleToolsList(); + expect((core.tools as unknown[]).length).toBe(7); + delete process.env["AGENTMEMORY_TOOLS"]; + }); + it("AGENTMEMORY_PROBE_TIMEOUT_MS overrides the default probe timeout", async () => { process.env["AGENTMEMORY_PROBE_TIMEOUT_MS"] = "50"; let probeStarted = 0;