* 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:
+30
-2
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user