From c2f231fe8bcf9b1fa296ad5ee81267eec94de768 Mon Sep 17 00:00:00 2001 From: Serhii Zghama <20826225+serhiizghama@users.noreply.github.com> Date: Tue, 19 May 2026 19:01:21 +0700 Subject: [PATCH] fix(mcp): memory_recall hits the right endpoint and forwards format/token_budget (#507) (#516) * fix(mcp): route memory_recall to /agentmemory/search and forward format/token_budget memory_recall and memory_smart_search were sharing the smart-search endpoint, which always returns compact mode and silently drops the format and token_budget parameters that the tool schema advertises. Split the cases so memory_recall hits /agentmemory/search (which honors format) while memory_smart_search keeps its own endpoint. Default format to "full" for memory_recall so the documented behavior matches the wire call. Signed-off-by: serhiizghama * test(mcp): cover memory_recall endpoint, format forwarding, and defaults Two new proxy tests for issue #507: one asserts memory_recall calls POST /agentmemory/search with the format and token_budget fields, and never falls through to smart-search; the other pins the default format to "full" when the caller omits it. Signed-off-by: serhiizghama --------- Signed-off-by: serhiizghama --- src/mcp/standalone.ts | 32 ++++++++++++++++-- test/mcp-standalone-proxy.test.ts | 55 +++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/mcp/standalone.ts b/src/mcp/standalone.ts index 86678a7..1413cbf 100644 --- a/src/mcp/standalone.ts +++ b/src/mcp/standalone.ts @@ -89,6 +89,8 @@ interface Validated { files?: string[]; query?: string; limit?: number; + format?: string; + tokenBudget?: number; memoryIds?: string[]; reason?: string; } @@ -118,6 +120,17 @@ function validate(toolName: string, args: Record): Validated { } v.query = query.trim(); v.limit = parseLimit(args["limit"]); + const fmt = args["format"]; + if (typeof fmt === "string" && fmt.trim()) { + v.format = fmt.trim().toLowerCase(); + } + const budget = args["token_budget"]; + if (typeof budget === "number" && Number.isFinite(budget) && budget > 0) { + v.tokenBudget = Math.floor(budget); + } else if (typeof budget === "string" && budget.trim()) { + const n = Number(budget); + if (Number.isFinite(n) && n > 0) v.tokenBudget = Math.floor(n); + } return v; } case "memory_sessions": { @@ -159,11 +172,26 @@ async function handleProxy( }); return textResponse(result); } - case "memory_recall": + case "memory_recall": { + const body: Record = { + query: v.query, + limit: v.limit, + format: v.format ?? "full", + }; + if (v.tokenBudget != null) body["token_budget"] = v.tokenBudget; + const result = await handle.call("/agentmemory/search", { + method: "POST", + body: JSON.stringify(body), + }); + return textResponse(result, true); + } case "memory_smart_search": { + const body: Record = { query: v.query, limit: v.limit }; + if (v.format != null) body["format"] = v.format; + if (v.tokenBudget != null) body["token_budget"] = v.tokenBudget; const result = await handle.call("/agentmemory/smart-search", { method: "POST", - body: JSON.stringify({ query: v.query, limit: v.limit }), + body: JSON.stringify(body), }); return textResponse(result, true); } diff --git a/test/mcp-standalone-proxy.test.ts b/test/mcp-standalone-proxy.test.ts index 0d93b22..dc08a02 100644 --- a/test/mcp-standalone-proxy.test.ts +++ b/test/mcp-standalone-proxy.test.ts @@ -75,6 +75,61 @@ describe("@agentmemory/mcp standalone — server proxy (issue #159)", () => { expect(body.results[0].id).toBe("m1"); }); + it("proxies memory_recall to POST /agentmemory/search and forwards format/token_budget (#507)", async () => { + const calls: Array<{ url: string; body?: unknown }> = []; + installFetch((url, init) => { + if (url.endsWith("/agentmemory/livez")) return new Response("ok", { status: 200 }); + const body = init?.body ? JSON.parse(init.body as string) : undefined; + calls.push({ url, body }); + if (url.endsWith("/agentmemory/search")) { + return new Response( + JSON.stringify({ + mode: "full", + facts: [{ id: "m1" }], + narrative: "n", + concepts: ["c"], + files: ["f"], + }), + { status: 200, headers: { "content-type": "application/json" } }, + ); + } + return new Response("not found", { status: 404 }); + }); + const res = await handleToolCall("memory_recall", { + query: "auth bug", + limit: 5, + format: "full", + token_budget: 800, + }); + const body = JSON.parse(res.content[0].text); + expect(body.mode).toBe("full"); + expect(body.facts[0].id).toBe("m1"); + const searchCall = calls.find((c) => c.url.endsWith("/agentmemory/search")); + expect(searchCall).toBeDefined(); + expect(searchCall?.body).toEqual({ + query: "auth bug", + limit: 5, + format: "full", + token_budget: 800, + }); + expect(calls.find((c) => c.url.endsWith("/agentmemory/smart-search"))).toBeUndefined(); + }); + + it("memory_recall defaults format to 'full' when omitted (#507)", async () => { + let recallBody: Record | undefined; + installFetch((url, init) => { + if (url.endsWith("/agentmemory/livez")) return new Response("ok", { status: 200 }); + if (url.endsWith("/agentmemory/search")) { + recallBody = init?.body ? JSON.parse(init.body as string) : undefined; + return new Response(JSON.stringify({ mode: "full", facts: [] }), { status: 200 }); + } + return new Response("not found", { status: 404 }); + }); + await handleToolCall("memory_recall", { query: "x" }); + expect(recallBody?.["format"]).toBe("full"); + expect(recallBody).not.toHaveProperty("token_budget"); + }); + it("proxies memory_governance_delete to the DELETE REST endpoint", async () => { const calls: Array<{ url: string; method: string; body?: unknown }> = []; installFetch((url, init) => {