From dac30bbbbf0ae0932d83f8c4ae7bd396da79a0d3 Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Sun, 21 Jun 2026 08:34:21 +0700 Subject: [PATCH 1/2] fix(mcp): treat empty optionalString args as absent optionalString only checked the type, so an empty or whitespace-only string passed through as a present value while its sibling requireString rejects the same input. In ctx_search this made `file: ""` behave as "filter to a file named ''", which links to no entity and silently returned zero results instead of an unfiltered search. The same gap affected `supersedes`. Return undefined for empty/whitespace-only strings so an optional argument set to "" is equivalent to omitting it, aligning with requireString's non-empty rule. --- packages/agentctx/src/mcp/tools.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/agentctx/src/mcp/tools.ts b/packages/agentctx/src/mcp/tools.ts index 91efc27..b8055b0 100644 --- a/packages/agentctx/src/mcp/tools.ts +++ b/packages/agentctx/src/mcp/tools.ts @@ -582,6 +582,11 @@ function optionalString(args: Record, name: string): string | u if (typeof value !== "string") { throw new McpToolError(`${name} must be a string`); } + // Treat empty/whitespace-only strings as absent, matching requireString's + // non-empty rule. This way an optional filter such as `ctx_search file: ""` + // means "no filter" rather than "filter to a file named ''" (which links to + // no entity and silently returns zero results). + if (value.trim().length === 0) return undefined; return value; } From e662c45f9710872c03f33d8f107367629d85381c Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Sun, 21 Jun 2026 08:34:21 +0700 Subject: [PATCH 2/2] test(mcp): cover empty file filter as unfiltered ctx_search --- packages/agentctx/test/mcp/tools.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/agentctx/test/mcp/tools.test.ts b/packages/agentctx/test/mcp/tools.test.ts index 97254a4..9afd91d 100644 --- a/packages/agentctx/test/mcp/tools.test.ts +++ b/packages/agentctx/test/mcp/tools.test.ts @@ -176,6 +176,29 @@ describe("ctx_search", () => { expect(results.map((r) => r.id)).toEqual([linked.id]); }); + it("treats an empty file filter as absent (unfiltered search)", () => { + const linked = seed({ title: "Auth flow decision", body: "JWT in auth module" }); + const unrelated = seed({ title: "Unrelated auth note", body: "JWT elsewhere" }); + const filePath = resolve(cwd, "src/auth.ts"); + linkRecordToEntity(tmp.db, linked.id, upsertNode(tmp.db, PROJECT, "file", filePath)); + + const omitted = call("ctx_search", { query: "JWT auth" }).payload as { + results: Array<{ id: string }>; + }; + const emptyFilter = call("ctx_search", { query: "JWT auth", file: "" }).payload as { + results: Array<{ id: string }>; + }; + const blankFilter = call("ctx_search", { query: "JWT auth", file: " " }).payload as { + results: Array<{ id: string }>; + }; + + const ids = (r: { results: Array<{ id: string }> }) => r.results.map((hit) => hit.id).sort(); + // An empty/whitespace file filter must search unfiltered, not return zero results. + expect(ids(emptyFilter)).toEqual(ids(omitted)); + expect(ids(blankFilter)).toEqual(ids(omitted)); + expect(ids(emptyFilter)).toEqual([linked.id, unrelated.id].sort()); + }); + it("marks degraded like-search when FTS5 is unavailable", () => { seed({ title: "Fallback fact", body: "search should still work" }); tmp.db.exec("DROP TABLE records_fts");