Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/mcp/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ questions:
- What is the difference between the MemWal plugin and MCP-only installation?
- Which MCP clients support the MemWal automatic memory plugin?
answer: >-
The MemWal MCP server exposes portable Walrus Memory as Model Context Protocol tools so AI agents can save and recall memories on their own. It can be installed as a plugin (with lifecycle hooks for automatic memory) on Claude Code, Codex, Cursor, and Antigravity, or as an MCP-only server on any MCP-aware client. Available tools include memwal_remember, memwal_recall, memwal_analyze, memwal_restore, memwal_health, memwal_login, and memwal_logout.
The MemWal MCP server exposes portable Walrus Memory as Model Context Protocol tools so AI agents can save and recall memories on their own. It can be installed as a plugin (with lifecycle hooks for automatic memory) on Claude Code, Codex, Cursor, and Antigravity, or as an MCP-only server on any MCP-aware client. Available tools include memwal_remember, memwal_recall, memwal_namespaces, memwal_analyze, memwal_restore, memwal_health, memwal_login, and memwal_logout.
---

The **MemWal MCP server** exposes your portable Walrus Memory as Model Context Protocol tools, so an AI agent can decide when to save and recall memories on its own. It works with any MCP-aware client, and on **Claude Code**, **Codex**, **Cursor**, and **Antigravity** it can be installed as a **plugin** that adds automatic memory through lifecycle hooks.
Expand Down Expand Up @@ -113,6 +113,7 @@ Connectors UI exposes only a single bearer field and cannot supply the required
| `memwal_remember` | Save a durable fact for the user (preference, decision, constraint, identity). |
| `memwal_remember_bulk` | Save several distinct facts in one call. |
| `memwal_recall` | Semantic search across stored memories for relevant context. |
| `memwal_namespaces` | List namespaces that hold memories, with memory counts and storage used. |
| `memwal_analyze` | Extract and save multiple facts from a passage of text. |
| `memwal_restore` | Rebuild the search index from Walrus when recall is unexpectedly empty. |
| `memwal_health` | Fast connectivity check (no search or decryption). |
Expand Down Expand Up @@ -181,4 +182,4 @@ You should see the `memwal_*` tools. Then state a durable fact (for example, a p

## Quick recovery

If `memwal_recall` returns nothing although you saved before (a new machine, a fresh relayer, or after switching servers), run `memwal_restore <namespace>` to rebuild the search index from the durable Walrus blobs, then recall again.
If `memwal_recall` returns nothing although you saved before (a new machine, a fresh relayer, or after switching servers), first verify your namespace spelling with `memwal_namespaces` (a mistyped namespace reads as 0 memories), or run `memwal_restore <namespace>` to rebuild the search index from the durable Walrus blobs, then recall again.
2 changes: 2 additions & 0 deletions docs/mcp/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ Every client runs the same server and differs only in the file format. The serve

Every recall runs inside one account and namespace. To keep a client's memories in their own space, set the `MEMWAL_NAMESPACE` environment variable in the server entry (`env` in JSON configs, `environment` in OpenCode), or pass `"--namespace", "<name>"` in `args`. Without it, memories go to the `default` namespace.

Verify your namespace spelling carefully: a mistyped namespace reads as empty (0 memories) rather than raising an error. If `memwal_recall` or `memwal_restore` reports 0 memories in your namespace, check the close-match suggestions in its reply or call `memwal_namespaces` to list every populated namespace on the account.

## Verify

1. Open your client's MCP status view (for example `/mcp` in Claude Code) and confirm the status view lists `memwal` as connected with its tools.
Expand Down
21 changes: 18 additions & 3 deletions docs/mcp/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Reference
description: >-
Complete reference for Walrus Memory MCP tools, CLI flags, environment presets, transport routes, and self-hosting.
Covers all eight tools, credential management, stdio and HTTP transports, and runtime safety details.
Covers all tools, credential management, stdio and HTTP transports, and runtime safety details.
keywords:
- MCP
- Walrus Memory
Expand Down Expand Up @@ -30,14 +30,14 @@ questions:
- How do I configure the MemWal MCP CLI flags and environment variables?
- How do I set up Streamable HTTP transport for Walrus Memory MCP?
answer: >-
The MemWal MCP reference documents eight tools (memwal_remember, memwal_remember_bulk, memwal_recall, memwal_analyze, memwal_restore, memwal_health, memwal_login, memwal_logout) with full parameter details. It covers CLI flags such as --relayer, --namespace, and --label, environment presets (prod, staging, local), two transport modes (stdio and Streamable HTTP), credential file format, default namespace precedence, self-hosting configuration, and runtime safety notes for 401 handling and relayer overrides.
The MemWal MCP reference documents the MCP tools (memwal_remember, memwal_remember_bulk, memwal_remember_status, memwal_recall, memwal_namespaces, memwal_analyze, memwal_restore, memwal_health, memwal_login, memwal_logout) with full parameter details. It covers CLI flags such as --relayer, --namespace, and --label, environment presets (prod, staging, local), two transport modes (stdio and Streamable HTTP), credential file format, default namespace precedence, self-hosting configuration, and runtime safety notes for 401 handling and relayer overrides.
---

The Walrus Memory MCP package exposes the tools, flags, environment variables, and transport routes below. For per-client setup, start with the [MCP overview](/mcp/overview).

## Tools

The MCP server exposes **eight tools**: six **relayer tools** (memory operations plus a health check) and two **session tools** served locally by the stdio package. For the lifecycle hooks that drive these tools automatically, see [Claude Code](/mcp/claude-code) or [Codex](/mcp/codex).
The MCP server exposes **relayer tools** (memory operations, namespace discovery, and a health check) plus two **session tools** served locally by the stdio package (`memwal_login` and `memwal_logout`). For the lifecycle hooks that drive these tools automatically, see [Claude Code](/mcp/claude-code) or [Codex](/mcp/codex).

## First-run behavior

Expand Down Expand Up @@ -79,13 +79,24 @@ Each result line includes `score` and `distance`. `distance` is cosine distance

The cutoff is applied to the `limit` nearest matches, not before them, so a tight `maxDistance` returns fewer than `limit` results — that is the cutoff biting, not an empty namespace. Raise `limit` to widen the candidate pool the cutoff sees.

When `memwal_recall` returns 0 matches, it checks the account's namespaces and distinguishes an empty or misspelled namespace (`namespace "<name>" currently holds 0 memories`, plus close-match suggestions) from a query that had 0 semantic matches in a populated namespace.

| **Parameter** | **Type** | **Required** | **Description** |
| --- | --- | --- | --- |
| `query` | string | yes | Natural-language query to match against stored memories. |
| `limit` | integer (1–100) | no | Max memories to return. Default `10`. |
| `namespace` | string | no | Namespace bucket to search. |
| `maxDistance` | number (>= 0) | no | Cosine-distance cutoff (low = similar). Hits with `distance >= maxDistance` are dropped. Omit for no cutoff. |

### memwal_namespaces

List the namespaces that hold memories in the user's Walrus Memory account, with each namespace's `memory_count`, `storage_used`, and last `updated` date (metadata only, no decryption). Use this to discover existing namespaces on an account or verify namespace spelling when `memwal_recall` or `memwal_restore` reports 0 memories.

| **Parameter** | **Type** | **Required** | **Description** |
| --- | --- | --- | --- |
| `limit` | integer (1–500) | no | Max namespaces to return per page. Default `100`. |
| `cursor` | string | no | Pagination cursor (`next_cursor` from a previous call). |

### memwal_analyze

Extract memorable facts from a longer passage of text (preferences, habits, biographical info, constraints) and save each as a separate memory.
Expand Down Expand Up @@ -234,6 +245,10 @@ Precedence, highest first:

CLI wins over the environment variable when you set both, which matches every other flag in the table above.

<Warning>
A mistyped namespace reads as empty (0 memories) on read and silently writes new memories into a separate bucket on write. Double-check your `--namespace` or `MEMWAL_NAMESPACE` spelling in your client config. When `memwal_recall` or `memwal_restore` sees 0 memories in the target namespace, it automatically checks your account's existing namespaces and suggests close matches; you can also call `memwal_namespaces` directly to list every populated namespace and its memory count.
</Warning>

<Note>
`memwal_restore` still lists `namespace` as **required** in its tool schema, so agents normally pass one explicitly. The configured default only acts as a fallback when the agent calls `memwal_restore` without a namespace.
</Note>
Expand Down
227 changes: 227 additions & 0 deletions services/server/scripts/mcp/__tests__/namespace-diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import assert from "node:assert/strict";
import test, { type TestContext } from "node:test";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import type { MemWalSession } from "../auth.js";
import { createMcpServer } from "../server.js";
import {
editDistance,
formatNamespacesResult,
formatNamespaceSuggestionHint,
isCloseNamespaceMatch,
type NamespaceEntry,
} from "../tools/namespaces.js";
import { emptyRecallText } from "../tools/recall.js";

async function openTestClient(
t: TestContext,
memwalStub: Record<string, unknown>,
sessionOverrides: Partial<MemWalSession> = {},
): Promise<Client> {
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const server = createMcpServer({
oauthScope: "memwal:read memwal:write",
memwal: memwalStub,
...sessionOverrides,
} as unknown as MemWalSession);
const client = new Client({ name: "namespace-diagnostics-test", version: "1.0.0" });
t.after(async () => {
await client.close();
await server.close();
});
await server.connect(serverTransport);
await client.connect(clientTransport);
return client;
}

function textFromResult(res: unknown): string {
const content = (res as { content?: Array<{ type: string; text: string }> }).content ?? [];
return content.map((c) => c.text).join("\n");
}

test("editDistance and isCloseNamespaceMatch detect typos and case/prefix matches", () => {
assert.equal(editDistance("pcl301", "pcl302"), 1);
assert.equal(editDistance("study-note", "study-notes"), 1);
assert.equal(isCloseNamespaceMatch("pcl301", "pcl302"), true);
assert.equal(isCloseNamespaceMatch("pcl3o2", "pcl302"), true);
assert.equal(isCloseNamespaceMatch("study", "study-notes"), true);
assert.equal(isCloseNamespaceMatch("pcl301", "completely-different-bucket"), false);
});

test("formatNamespaceSuggestionHint prioritizes close edit-distance matches", () => {
const namespaces: NamespaceEntry[] = [
{ name: "work-projects", memory_count: 50 },
{ name: "study-notes", memory_count: 12 },
{ name: "default", memory_count: 3 },
];
const hint = formatNamespaceSuggestionHint("study-note", namespaces);
assert.match(hint, /Did you mean namespace "study-notes" \(12 memories\)\?/);
assert.match(
hint,
/Existing namespaces: "study-notes" \(12\), "work-projects" \(50\), "default" \(3\)\./,
);
});

test("emptyRecallText distinguishes empty namespace from populated namespace with 0 semantic hits", () => {
const namespaces: NamespaceEntry[] = [
{ name: "pcl302", memory_count: 15 },
];

const emptyNs = emptyRecallText(0, 0, {
activeNamespace: "pcl301",
namespaces,
});
assert.match(
emptyNs,
/No matching memories found \(namespace "pcl301" currently holds 0 memories\)\./,
);
assert.match(emptyNs, /Did you mean namespace "pcl302" \(15 memories\)\?/);

const populatedNs = emptyRecallText(0, 0, {
activeNamespace: "pcl302",
namespaces,
});
assert.equal(
populatedNs,
'No matching memories found in namespace "pcl302" (0 matches among 15 memories).',
);
});

test("memwal_recall on a misspelled namespace suggests close existing namespaces from listNamespaces()", async (t) => {
const client = await openTestClient(
t,
{
recall: async () => ({ results: [], total: 0 }),
listNamespaces: async () => ({
namespaces: [
{
id: "ns-1",
name: "pcl302",
memory_count: 15,
storage_used: 3200,
updated_at: "2026-08-20T10:00:00Z",
},
{
id: "ns-2",
name: "default",
memory_count: 4,
storage_used: 800,
updated_at: "2026-08-19T10:00:00Z",
},
],
next_cursor: null,
has_more: false,
snapshot_version: 1,
}),
},
{ namespace: "default" },
);

const res = await client.callTool({
name: "memwal_recall",
arguments: { query: "pharmacology lecture notes", namespace: "pcl301" },
});
const text = textFromResult(res);

assert.match(text, /namespace "pcl301" currently holds 0 memories/);
assert.match(text, /Did you mean namespace "pcl302" \(15 memories\)\?/);
assert.match(text, /Existing namespaces: "pcl302" \(15\), "default" \(4\)\./);
});

test("memwal_recall falls back gracefully when listNamespaces is not stubbed or throws", async (t) => {
const client = await openTestClient(t, {
recall: async () => ({ results: [], total: 0 }),
});

const res = await client.callTool({
name: "memwal_recall",
arguments: { query: "anything" },
});
assert.equal(textFromResult(res), "No matching memories found.");
});

test("memwal_restore on a misspelled namespace reports 0 blobs and suggests close existing namespaces", async (t) => {
const client = await openTestClient(t, {
restore: async (namespace: string) => ({
namespace,
total: 0,
restored: 0,
skipped: 0,
failed: 0,
truncated: false,
}),
listNamespaces: async () => ({
namespaces: [
{
id: "ns-1",
name: "study-notes",
memory_count: 12,
storage_used: 2400,
updated_at: "2026-08-20T10:00:00Z",
},
],
next_cursor: null,
has_more: false,
snapshot_version: 1,
}),
});

const res = await client.callTool({
name: "memwal_restore",
arguments: { namespace: "study-note", limit: 10 },
});
const text = textFromResult(res);

assert.match(text, /Namespace "study-note" holds 0 on-chain blobs\./);
assert.match(text, /Did you mean namespace "study-notes" \(12 memories\)\?/);
assert.match(text, /Existing namespaces: "study-notes" \(12\)\./);
});

test("memwal_namespaces tool lists namespaces with memory counts and storage used", async (t) => {
let receivedArgs: { limit?: number; cursor?: string } | undefined;
const client = await openTestClient(t, {
listNamespaces: async (opts: { limit?: number; cursor?: string }) => {
receivedArgs = opts;
return {
namespaces: [
{
id: "ns-1",
name: "pcl302",
memory_count: 15,
storage_used: 4096,
updated_at: "2026-08-20T10:00:00Z",
},
{
id: "ns-2",
name: "default",
memory_count: 1,
storage_used: 256,
updated_at: "2026-08-18T09:30:00Z",
},
],
next_cursor: "cursor-2",
has_more: true,
snapshot_version: 3,
};
},
});

const res = await client.callTool({
name: "memwal_namespaces",
arguments: { limit: 25 },
});
const text = textFromResult(res);

assert.deepEqual(receivedArgs, { limit: 25, cursor: undefined });
assert.match(text, /^Namespaces \(2\+\):/);
assert.match(text, /- "pcl302": 15 memories, storage_used=4096B, updated=2026-08-20/);
assert.match(text, /- "default": 1 memory, storage_used=256B, updated=2026-08-18/);
assert.match(text, /next_cursor="cursor-2"/);
});

test("formatNamespacesResult reports when an account has no populated namespaces", () => {
assert.equal(
formatNamespacesResult({ namespaces: [], has_more: false, next_cursor: null }),
"No namespaces with stored memories found for this account.",
);
});
22 changes: 22 additions & 0 deletions services/server/scripts/mcp/__tests__/restore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,25 @@ test("memwal_restore still tells agents to raise limit for WALM-431 cap truncati
assert.match(text, /increase limit and call again/);
assert.doesNotMatch(text, /download\/embed blip/);
});

test("memwal_restore states 0 on-chain blobs and suggests close namespaces when namespace is empty", () => {
const text = formatRestoreResult(
{
namespace: "pcl301",
total: 0,
restored: 0,
skipped: 0,
failed: 0,
truncated: false,
},
10,
[
{ name: "pcl302", memory_count: 15, storage_used: 4096 },
{ name: "work", memory_count: 8, storage_used: 2048 },
],
);

assert.match(text, /Namespace "pcl301" holds 0 on-chain blobs\./);
assert.match(text, /Did you mean namespace "pcl302" \(15 memories\)\?/);
assert.match(text, /Existing namespaces: "pcl302" \(15\), "work" \(8\)\./);
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ test("tools/list publishes safe titles and behavior annotations for every remote
title: "Recall Memories",
annotations: { readOnlyHint: true, destructiveHint: false },
},
memwal_namespaces: {
title: "List Memory Namespaces",
annotations: { readOnlyHint: true, destructiveHint: false },
},
memwal_health: {
title: "Check Walrus Memory Health",
annotations: { readOnlyHint: true, destructiveHint: false },
Expand Down
2 changes: 1 addition & 1 deletion services/server/scripts/mcp/__tests__/tool-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const WRITE_TOOLS = [
"memwal_analyze",
"memwal_restore",
];
const READ_TOOLS = ["memwal_recall", "memwal_health"];
const READ_TOOLS = ["memwal_recall", "memwal_namespaces", "memwal_health"];

async function toolNamesFor(oauthScope: string | undefined, t: TestContext): Promise<string[]> {
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
Expand Down
4 changes: 4 additions & 0 deletions services/server/scripts/mcp/tools/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const TOOL_METADATA = {
// proactive recall this tool exists to encourage.
annotations: { readOnlyHint: true, destructiveHint: false },
},
memwal_namespaces: {
title: "List Memory Namespaces",
annotations: { readOnlyHint: true, destructiveHint: false },
},
memwal_health: {
title: "Check Walrus Memory Health",
annotations: { readOnlyHint: true, destructiveHint: false },
Expand Down
Loading
Loading