diff --git a/docs/mcp/reference.md b/docs/mcp/reference.md index ddbdb0b2..653a8468 100644 --- a/docs/mcp/reference.md +++ b/docs/mcp/reference.md @@ -139,7 +139,7 @@ Set `MEMWAL_MCP_DEBUG=1` to enable verbose stderr logging. ## Default namespace -Set a default memory namespace once in your client config instead of having the agent pass `namespace` on every call. The package injects it into `memwal_remember`, `memwal_recall`, `memwal_analyze`, and `memwal_restore` calls that don't already carry one. +Set a default memory namespace once in your client config instead of having the agent pass `namespace` on every call. The package injects it into `memwal_remember`, `memwal_remember_bulk`, `memwal_recall`, `memwal_analyze`, and `memwal_restore` calls that don't already carry one. Precedence, highest first: diff --git a/packages/mcp/README.md b/packages/mcp/README.md index eabdc0ef..4a21e858 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -58,9 +58,9 @@ Enable verbose stderr logging with `MEMWAL_MCP_DEBUG=1`. ## Default Namespace By default the MCP tool schemas expose an optional `namespace` argument and the -agent has to pass it on every `memwal_remember` / `memwal_recall` / -`memwal_analyze` call (and `memwal_restore` requires it). Set a default once in -your client config instead: +agent has to pass it on every `memwal_remember` / `memwal_remember_bulk` / +`memwal_recall` / `memwal_analyze` call (and `memwal_restore` requires it). Set +a default once in your client config instead: ```json { diff --git a/packages/mcp/src/bridge.ts b/packages/mcp/src/bridge.ts index 280a6f73..06fe72e3 100644 --- a/packages/mcp/src/bridge.ts +++ b/packages/mcp/src/bridge.ts @@ -38,10 +38,10 @@ export interface BridgeConfig { } /** Memory tools that take a `namespace` argument. `memwal_remember`, - * `memwal_recall`, and `memwal_analyze` treat it as optional; `memwal_restore` - * requires it (its upstream schema still lists `namespace` as required, so - * agents normally pass one — but a configured default is filled in if the - * agent calls it without). */ + * `memwal_remember_bulk`, `memwal_recall`, and `memwal_analyze` treat it as + * optional; `memwal_restore` requires it (its upstream schema still lists + * `namespace` as required, so agents normally pass one — but a configured + * default is filled in if the agent calls it without). */ const NAMESPACE_TOOLS = new Set([ "memwal_remember", "memwal_remember_bulk", diff --git a/packages/mcp/test/default-namespace.test.mjs b/packages/mcp/test/default-namespace.test.mjs new file mode 100644 index 00000000..cca141c8 --- /dev/null +++ b/packages/mcp/test/default-namespace.test.mjs @@ -0,0 +1,123 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { applyDefaultNamespace } from "../dist/bridge.js"; + +test("applyDefaultNamespace injects configured namespace into memwal_remember_bulk", () => { + const msg = { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "memwal_remember_bulk", + arguments: { + facts: ["fact 1", "fact 2"], + }, + }, + }; + + const updated = applyDefaultNamespace(msg, "project-alpha"); + assert.equal(updated.params.arguments.namespace, "project-alpha"); + assert.deepEqual(updated.params.arguments.facts, ["fact 1", "fact 2"]); + // The call site ignores the return value — in-place mutation is the real contract. + assert.equal(msg.params.arguments.namespace, "project-alpha"); +}); + +test("applyDefaultNamespace respects explicit namespace on memwal_remember_bulk", () => { + const msg = { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { + name: "memwal_remember_bulk", + arguments: { + facts: ["fact 1"], + namespace: "explicit-scope", + }, + }, + }; + + const updated = applyDefaultNamespace(msg, "project-alpha"); + assert.equal(updated.params.arguments.namespace, "explicit-scope"); +}); + +test("applyDefaultNamespace injects into all namespace-aware tools", () => { + const tools = [ + "memwal_remember", + "memwal_remember_bulk", + "memwal_recall", + "memwal_analyze", + "memwal_restore", + ]; + + for (const toolName of tools) { + const msg = { + jsonrpc: "2.0", + id: 3, + method: "tools/call", + params: { + name: toolName, + arguments: {}, + }, + }; + + const updated = applyDefaultNamespace(msg, "shared-namespace"); + assert.equal( + updated.params.arguments.namespace, + "shared-namespace", + `expected default namespace to be injected for ${toolName}` + ); + } +}); + +test("applyDefaultNamespace does not touch unrelated tools or non-call RPC messages", () => { + const loginMsg = { + jsonrpc: "2.0", + id: 4, + method: "tools/call", + params: { + name: "memwal_login", + arguments: {}, + }, + }; + const updatedLogin = applyDefaultNamespace(loginMsg, "test-ns"); + assert.equal(updatedLogin.params.arguments.namespace, undefined); + + const listMsg = { + jsonrpc: "2.0", + id: 5, + method: "tools/list", + params: { name: "memwal_remember_bulk", arguments: {} }, + }; + const updatedList = applyDefaultNamespace(listMsg, "test-ns"); + assert.equal(updatedList.params.arguments.namespace, undefined); +}); + +test("applyDefaultNamespace is a no-op when no default is configured", () => { + const msg = { + jsonrpc: "2.0", + id: 6, + method: "tools/call", + params: { name: "memwal_remember_bulk", arguments: { facts: ["fact 1"] } }, + }; + + applyDefaultNamespace(msg, undefined); + assert.equal(msg.params.arguments.namespace, undefined); +}); + +test("applyDefaultNamespace overrides a blank explicit namespace", () => { + for (const blank of ["", " "]) { + const msg = { + jsonrpc: "2.0", + id: 7, + method: "tools/call", + params: { + name: "memwal_remember_bulk", + arguments: { facts: ["fact 1"], namespace: blank }, + }, + }; + + applyDefaultNamespace(msg, "project-alpha"); + assert.equal(msg.params.arguments.namespace, "project-alpha"); + } +});