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 <zmrser@gmail.com>

* 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 <zmrser@gmail.com>

---------

Signed-off-by: serhiizghama <zmrser@gmail.com>
This commit is contained in:
Serhii Zghama
2026-05-19 19:01:21 +07:00
committed by GitHub
parent 48bf700f62
commit c2f231fe8b
2 changed files with 85 additions and 2 deletions
+30 -2
View File
@@ -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<string, unknown>): 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<string, unknown> = {
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<string, unknown> = { 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);
}
+55
View File
@@ -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<string, unknown> | 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) => {