From 3c09a58de0cdf4230151dd2a26472d36c4ab1609 Mon Sep 17 00:00:00 2001 From: honor2030 <19909783+honor2030@users.noreply.github.com> Date: Mon, 18 May 2026 16:32:35 +0900 Subject: [PATCH] fix(mcp): preserve recall format option --- src/mcp/standalone.ts | 20 ++++++++++++++-- test/mcp-standalone.test.ts | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/mcp/standalone.ts b/src/mcp/standalone.ts index 86678a769b..39d93828c4 100644 --- a/src/mcp/standalone.ts +++ b/src/mcp/standalone.ts @@ -64,6 +64,8 @@ function normalizeList(value: unknown): string[] { const DEFAULT_LIMIT = 10; const MAX_LIMIT = 100; +type SearchFormat = "compact" | "full"; + function parseLimit(raw: unknown, fallback = DEFAULT_LIMIT): number { if (typeof raw !== "number" && typeof raw !== "string") return fallback; const n = Number(raw); @@ -71,6 +73,12 @@ function parseLimit(raw: unknown, fallback = DEFAULT_LIMIT): number { return Math.min(Math.floor(n), MAX_LIMIT); } +function parseSearchFormat(raw: unknown): SearchFormat | undefined { + if (typeof raw !== "string") return undefined; + const format = raw.trim().toLowerCase(); + return format === "compact" || format === "full" ? format : undefined; +} + function textResponse(payload: unknown, pretty = false): { content: Array<{ type: string; text: string }>; } { @@ -89,6 +97,7 @@ interface Validated { files?: string[]; query?: string; limit?: number; + format?: SearchFormat; memoryIds?: string[]; reason?: string; } @@ -118,6 +127,7 @@ function validate(toolName: string, args: Record): Validated { } v.query = query.trim(); v.limit = parseLimit(args["limit"]); + v.format = parseSearchFormat(args["format"]); return v; } case "memory_sessions": { @@ -161,9 +171,14 @@ async function handleProxy( } case "memory_recall": case "memory_smart_search": { + const body: { query?: string; limit?: number; format?: SearchFormat } = { + query: v.query, + limit: v.limit, + }; + if (v.format) body.format = v.format; 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); } @@ -227,6 +242,7 @@ async function handleLocal( case "memory_smart_search": { const query = (v.query || "").toLowerCase(); const limit = v.limit ?? DEFAULT_LIMIT; + const format = v.format ?? "compact"; const all = await kvInstance.list>("mem:memories"); const results = all @@ -244,7 +260,7 @@ async function handleLocal( return query.split(/\s+/).every((word) => text.includes(word)); }) .slice(0, limit); - return textResponse({ mode: "compact", results }, true); + return textResponse({ mode: format, format, results }, true); } case "memory_sessions": { diff --git a/test/mcp-standalone.test.ts b/test/mcp-standalone.test.ts index e5670ec646..ae37c85bb8 100644 --- a/test/mcp-standalone.test.ts +++ b/test/mcp-standalone.test.ts @@ -222,6 +222,52 @@ describe("handleToolCall", () => { expect(parsed.results[0].content).toBe("TypeScript is great"); }); + it("memory_recall preserves a safe format option in local fallback (#440)", async () => { + const kv = new InMemoryKV(); + await handleToolCall("memory_save", { content: "TypeScript is great" }, kv); + + const result = await handleToolCall( + "memory_recall", + { query: "typescript", format: "full" }, + kv, + ); + + const parsed = JSON.parse(result.content[0].text); + expect(parsed.format).toBe("full"); + expect(parsed.mode).toBe("full"); + expect(parsed.results[0].content).toBe("TypeScript is great"); + }); + + it("memory_recall and memory_smart_search forward safe format to smart-search proxy (#440)", async () => { + const proxyProbe = vi.fn(async () => ({ ok: true, status: 200 })); + setLivezProbe(proxyProbe); + + const proxyFetch = vi.fn(async () => + new Response(JSON.stringify({ results: [] }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + (globalThis as { fetch: typeof fetch }).fetch = proxyFetch as unknown as typeof fetch; + + for (const toolName of ["memory_recall", "memory_smart_search"]) { + resetHandleForTests(); + setLivezProbe(proxyProbe); + await handleToolCall( + toolName, + { query: "typescript", limit: 3, format: "full" }, + new InMemoryKV(), + ); + } + + expect(proxyFetch).toHaveBeenCalledTimes(2); + for (const [, init] of proxyFetch.mock.calls) { + expect((init as RequestInit).method).toBe("POST"); + const body = JSON.parse((init as RequestInit).body as string); + expect(body).toEqual({ query: "typescript", limit: 3, format: "full" }); + } + }); + it("memory_save accepts concepts/files as arrays (plugin skill format, #139)", async () => { const kv = new InMemoryKV(); const result = await handleToolCall(