fix(mcp): local fallback exposes all 7 IMPLEMENTED_TOOLS, not 4 (#234)#283
Conversation
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).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe PR centralizes MCP ChangesMCP tools/list Handler Centralization
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/mcp-standalone-proxy.test.ts`:
- Around line 250-274: The test leaks process.env["AGENTMEMORY_TOOLS"] if an
assertion fails; wrap the environment mutation and cleanup around the
handleToolsList calls in a try/finally so AGENTMEMORY_TOOLS is always restored:
save the original value into a variable, set/delete
process.env["AGENTMEMORY_TOOLS"] as needed before calling handleToolsList (and
resetHandleForTests()/installFetch() usage stays the same), then restore the
original value in a finally block to guarantee isolation for subsequent tests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d8b16a17-bee7-4dce-9ad4-695e0a537b5e
📒 Files selected for processing (2)
src/mcp/standalone.tstest/mcp-standalone-proxy.test.ts
| 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"]; | ||
| }); |
There was a problem hiding this comment.
Ensure AGENTMEMORY_TOOLS is restored via finally for test isolation.
process.env["AGENTMEMORY_TOOLS"] is only cleaned up at the end of the happy path. If an assertion throws earlier, this can leak into later tests and create flakiness.
💡 Suggested patch
it("local fallback tools/list returns all 7 IMPLEMENTED_TOOLS regardless of AGENTMEMORY_TOOLS env (`#234`)", async () => {
+ const previousToolsEnv = process.env["AGENTMEMORY_TOOLS"];
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"];
+ try {
+ 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);
+ } finally {
+ if (previousToolsEnv === undefined) {
+ delete process.env["AGENTMEMORY_TOOLS"];
+ } else {
+ process.env["AGENTMEMORY_TOOLS"] = previousToolsEnv;
+ }
+ }
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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("local fallback tools/list returns all 7 IMPLEMENTED_TOOLS regardless of AGENTMEMORY_TOOLS env (`#234`)", async () => { | |
| const previousToolsEnv = process.env["AGENTMEMORY_TOOLS"]; | |
| const { handleToolsList } = await import("../src/mcp/standalone.js"); | |
| installFetch(() => { | |
| throw new Error("ECONNREFUSED"); | |
| }); | |
| try { | |
| 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); | |
| } finally { | |
| if (previousToolsEnv === undefined) { | |
| delete process.env["AGENTMEMORY_TOOLS"]; | |
| } else { | |
| process.env["AGENTMEMORY_TOOLS"] = previousToolsEnv; | |
| } | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/mcp-standalone-proxy.test.ts` around lines 250 - 274, The test leaks
process.env["AGENTMEMORY_TOOLS"] if an assertion fails; wrap the environment
mutation and cleanup around the handleToolsList calls in a try/finally so
AGENTMEMORY_TOOLS is always restored: save the original value into a variable,
set/delete process.env["AGENTMEMORY_TOOLS"] as needed before calling
handleToolsList (and resetHandleForTests()/installFetch() usage stays the same),
then restore the original value in a finally block to guarantee isolation for
subsequent tests.
) Single-issue release: PR #283 fixed the local-mode tools/list path in @agentmemory/mcp to expose all 7 IMPLEMENTED_TOOLS instead of the 4-tool intersection ESSENTIAL_TOOLS ∩ IMPLEMENTED_TOOLS that bug manifested as in MCP clients (Cursor / Roo Code / others) when no agentmemory server was reachable on localhost:3111. Bumping 0.9.7 -> 0.9.8 across the 8 standard files (package.json, packages/mcp/package.json, plugin/.claude-plugin/plugin.json, src/version.ts, src/types.ts ExportData literal, src/functions/export-import.ts supportedVersions, the export round-trip test expectation, and CHANGELOG.md).
Reproduction
When no agentmemory server is reachable on
localhost:3111, the@agentmemory/mcpshim showed exactly 4 tools in MCP clients (Cursor, Roo Code, etc.):Should be 7 — those four plus
memory_export,memory_audit,memory_governance_delete. The InMemoryKV that backs local mode implements all seven operations; only four were advertised.Reproduced live via stdio against a downed server before this PR:
Same four tools showing in @rohitghumare's Cursor against a downed server, matching the bug @jcalfee originally reported in #234.
Root cause
The local-fallback branch of
tools/listinsrc/mcp/standalone.tsintersected the shim'sIMPLEMENTED_TOOLS(7) withgetVisibleTools():getVisibleTools()honors the shim process'sAGENTMEMORY_TOOLSenv var. Default unset → returns 8ESSENTIAL_TOOLS(the server's default "core" tier). Intersection:Exactly the four tools that showed up.
The intersection was meaningless from the start: the shim dispatches by
IMPLEMENTED_TOOLS, not byESSENTIAL_TOOLS. The shim's local-mode capabilities are fixed by its in-memory KV, not by an env knob that's only meant to filter the server's catalog.Fix
Switch the filter source from
getVisibleTools()togetAllTools():IMPLEMENTED_TOOLS.has(t.name)still picks the right 7 names, but now from the unfiltered universe instead of an env-filtered subset.Also refactored the
tools/listhandler out of the inlinecreateStdioTransportcallback into an exportedhandleToolsList()for direct testability. Added a regression test asserting the local-fallback path returns exactly the 7IMPLEMENTED_TOOLSregardless ofAGENTMEMORY_TOOLS=coreor unset.Verified
memory_save,memory_recall,memory_smart_search,memory_sessions,memory_export,memory_audit,memory_governance_delete).npm test— 860 / 860 (859 baseline + 1 regression test).npm run build— tsdown clean.Next
Cut v0.9.8 after this lands and publish. v0.9.7's local-mode count was wrong; v0.9.8 restores the documented 7-tool local fallback.
Summary by CodeRabbit
Bug Fixes
Tests