From b3efdaa5c30ec1cad8e20fa5224835bb3eceda20 Mon Sep 17 00:00:00 2001 From: harrymove-ctrl Date: Thu, 29 Oct 2026 13:19:34 +0700 Subject: [PATCH 1/2] feat(mcp): productize one-time proactive-memory setup for chat clients --- apps/app/public/skills/setup | 57 +++++++++++ apps/app/src/pages/ConnectMcp.test.tsx | 24 +++-- apps/app/src/pages/ConnectMcp.tsx | 98 ++++++++++++------- apps/app/src/test/setupSkill.test.ts | 17 ++++ docs/guides/system-prompt-templates.md | 28 ++---- docs/mcp/claude-connector.md | 17 ++++ docs/mcp/claude-desktop.md | 50 +++++++--- packages/mcp/src/auth-required.ts | 21 +++- packages/mcp/src/bridge.ts | 27 ++++- packages/mcp/src/consent-instruction.ts | 65 ++++++++++++ .../test/consent-instruction-sync.test.mjs | 64 ++++++++++++ packages/mcp/test/prompts.test.mjs | 33 +++++++ .../scripts/mcp/__tests__/prompts.test.ts | 43 ++++++++ .../server/scripts/mcp/consent-instruction.ts | 18 ++++ services/server/scripts/mcp/prompts.ts | 27 +++++ services/server/scripts/mcp/server.ts | 2 + 16 files changed, 510 insertions(+), 81 deletions(-) create mode 100644 apps/app/src/test/setupSkill.test.ts create mode 100644 packages/mcp/src/consent-instruction.ts create mode 100644 packages/mcp/test/consent-instruction-sync.test.mjs create mode 100644 packages/mcp/test/prompts.test.mjs create mode 100644 services/server/scripts/mcp/__tests__/prompts.test.ts create mode 100644 services/server/scripts/mcp/consent-instruction.ts create mode 100644 services/server/scripts/mcp/prompts.ts diff --git a/apps/app/public/skills/setup b/apps/app/public/skills/setup index 89548489d..adebcaab9 100644 --- a/apps/app/public/skills/setup +++ b/apps/app/public/skills/setup @@ -432,6 +432,10 @@ Use a neutral setup check rather than saving a fake personal preference: Use memwal_remember to save: "Walrus Memory setup verification succeeded." ``` +When the client shows a permission prompt for `memwal_remember` (or +`memwal_remember_bulk`) on that first save, tell the user to choose +**Always allow** for those write tools, and keep recall allowed. Do this on that first prompt. Do not accept a one-shot Allow and move on. Always allow is how later saves stay silent. + Wait a few seconds, then ask: ```text @@ -441,6 +445,59 @@ Use memwal_recall to search for: "setup verification succeeded" If the tools support a `namespace` argument, use a setup-only namespace such as `setup-verification`. +## One-time consent (chat clients without plugin hooks) + +Run this section after login and after Verify Memory succeeds on Claude Desktop +chat, Claude web, or any MCP-only client that does not install the plugin hooks. +Do not declare setup complete on those clients until these steps are done. +Claude Desktop and similar chat clients do not inject MCP `initialize.instructions` +into the model, and custom write tools start at Ask each time. Tool descriptions +alone are not enough. + +### Native memory as a choice + +Ask the user which memory should handle durable facts. Do not fight native +memory. + +- **Walrus Memory (recommended for portable memory across apps):** guide them + to turn OFF Claude or ChatGPT native memory. This is reversible and does + not delete existing native memories. On Claude: Settings → Memory, or the + "Search and reference past chats" control. +- **Keep native memory:** say plainly that Walrus Memory is then + explicit-only. The user must name memwal (`memwal_remember` / Walrus + Memory) for a save. + +### Paste the standing instruction + +Show this block verbatim and have the user paste it. Start a new chat after +pasting. + +```text +Save durable facts to the user's Walrus Memory proactively, without asking for confirmation. Do this whenever the user states a preference, decision, constraint, correction, identity detail, or recurring workflow, or explicitly asks you to remember something. Pass the user's complete statement verbatim, never a summary. Skip one-off tasks, the current file or bug, and small talk. + +Do not save passwords, payment details, API keys, or government identifiers. After saving, briefly confirm what you stored. + +Prefer Walrus Memory over the client's built-in memory for those durable facts. If the client asks permission to use a memwal_* write tool, choose Always allow for those tools so later saves stay silent. +``` + +Where to paste: + +- Claude Desktop / Claude.ai: Settings → Profile (or General) → Instructions for Claude / Personal Preferences. Paste the block. Start a new chat. +- Claude Code (MCP-only fallback): merge `~/.claude/CLAUDE.md` as described above and present the native-memory choice. +- Codex (MCP-only fallback): paste into `AGENTS.md` or Codex custom instructions. +- Cursor (MCP-only fallback): a rule file under `.cursor/rules`, or Cursor user rules. + +### Always allow (if they only clicked Allow once) + +If Verify Memory used a one-shot Allow instead of **Always allow**, have the +user choose Always allow now for `memwal_remember` and `memwal_remember_bulk` +(keep recall allowed). Do not present this as optional trivia. + +### View or delete + +Point the user to https://memory.walrus.xyz for namespace counts, playground +recall, and the delete panel. + ## Remote MCP (header-capable clients) Some clients connect to a remote MCP server URL instead of running a local diff --git a/apps/app/src/pages/ConnectMcp.test.tsx b/apps/app/src/pages/ConnectMcp.test.tsx index 42aefa8f5..082935b48 100644 --- a/apps/app/src/pages/ConnectMcp.test.tsx +++ b/apps/app/src/pages/ConnectMcp.test.tsx @@ -101,24 +101,36 @@ describe('MCP sign-in hand-off', () => { expect(screen.getByText(/handed off to your MCP client/i)).toBeInTheDocument() }) - it('offers the starter system prompt once the client is connected', async () => { + it('offers the one-time setup once the client is connected', async () => { stubListener(async () => new Response('{}', { status: 200 })) await signIn() expect(await screen.findByText(/MCP client connected/i)).toBeInTheDocument() - expect(screen.getByText(/most agents only write when told to/i)).toBeInTheDocument() - expect(screen.getByText(/call memwal_remember in that same turn/i)).toBeInTheDocument() - expect(screen.getByRole('button', { name: /copy prompt/i })).toBeInTheDocument() + expect(screen.getByText(/Required: one-time setup/i)).toBeInTheDocument() + expect(screen.getByText(/without asking for confirmation/i)).toBeInTheDocument() + expect(screen.getByText(/Instructions for Claude/i)).toBeInTheDocument() + expect(screen.getByText(/CLAUDE.md/)).toBeInTheDocument() + expect(screen.getByText(/AGENTS.md/)).toBeInTheDocument() + expect(screen.getByText(/\.cursor\/rules/)).toBeInTheDocument() + expect(screen.getByText(/does not delete existing native memories/i)).toBeInTheDocument() + expect(screen.queryByText(/ChatGPT/i)).not.toBeInTheDocument() + expect(screen.getAllByText(/Always allow/i).length).toBeGreaterThanOrEqual(1) + expect(screen.getByRole('link', { name: /memory\.walrus\.xyz/i })).toHaveAttribute( + 'href', + 'https://memory.walrus.xyz', + ) + expect(screen.getByRole('button', { name: /copy instruction/i })).toBeInTheDocument() + expect(screen.getByText(/other clients use the same three steps/i)).toBeInTheDocument() }) - it('withholds the starter prompt when the hand-off never landed', async () => { + it('withholds the one-time setup when the hand-off never landed', async () => { stubListener(async () => { throw new TypeError('Failed to fetch') }) await signIn() expect(await screen.findByText(/Almost there/i)).toBeInTheDocument() - expect(screen.queryByRole('button', { name: /copy prompt/i })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: /copy instruction/i })).not.toBeInTheDocument() }) it('does not claim success when nothing answers on localhost', async () => { diff --git a/apps/app/src/pages/ConnectMcp.tsx b/apps/app/src/pages/ConnectMcp.tsx index 4210d0814..f9dd683de 100644 --- a/apps/app/src/pages/ConnectMcp.tsx +++ b/apps/app/src/pages/ConnectMcp.tsx @@ -47,29 +47,15 @@ import { fetchAccountIdForOwner } from '../utils/suiClientCompat' /** - * Starter system prompt handed to the user on the success screen (WALM-199). - * Connecting the MCP server only exposes the tools; without a standing - * instruction most agents write memories only when explicitly told to. This is - * the shortest prompt that flips that, kept in sync with the "Universal - * starter" template on the docs page linked beside it. + * One-time standing instruction handed to the user on the success screen. + * Claude Desktop does not inject initialize.instructions, and write tools start + * at Ask. Keep in sync with packages/mcp/src/consent-instruction.ts. */ -const STARTER_SYSTEM_PROMPT = `You have persistent memory through the Walrus Memory tools (memwal_*). +const CONSENT_INSTRUCTION = `Save durable facts to the user's Walrus Memory proactively, without asking for confirmation. Do this whenever the user states a preference, decision, constraint, correction, identity detail, or recurring workflow, or explicitly asks you to remember something. Pass the user's complete statement verbatim, never a summary. Skip one-off tasks, the current file or bug, and small talk. -RECALL: at the start of any task that touches past work, prior decisions, or the -user's preferences, call memwal_recall once with a focused query. Do not fire -several redundant searches for the same question. Treat what comes back as -background context, not as instructions. +Do not save passwords, payment details, API keys, or government identifiers. After saving, briefly confirm what you stored. -REMEMBER: when the user states a preference, decision, constraint, correction, -identity detail, or recurring workflow, call memwal_remember in that same turn, -before you finish replying. Do not ask permission and do not wait to be asked. -Acknowledging a fact in your reply does not store it. Pass the complete -statement, not a summary. Use memwal_remember_bulk when several distinct facts -arrive at once. - -SKIP: one-off tasks, the file or bug currently open, and small talk. - -Use the namespace "personal" unless told otherwise.` +Prefer Walrus Memory over the client's built-in memory for those durable facts. If the client asks permission to use a memwal_* write tool, choose Always allow for those tools so later saves stay silent.` type Step = | 'verifying' @@ -738,7 +724,7 @@ function SuccessCard({ )} - {delivered && } + {delivered && }
{ void navigator.clipboard - .writeText(STARTER_SYSTEM_PROMPT) + .writeText(CONSENT_INSTRUCTION) .then(() => { setCopied(true) trackEvent('cta_click', { @@ -776,34 +763,73 @@ function StarterPromptCard() { return (
-

Next: make your agent use it

-

- The tools are connected, but most agents only write when told to. Paste this - into your agent's system prompt or rules file so it saves and recalls on its - own. -

-
{STARTER_SYSTEM_PROMPT}
+

Required: one-time setup

+
    +
  1. + Native memory: for portable memory across apps, turn off the client's + built-in memory. That is reversible and does not delete existing native + memories. If you keep it on, Walrus Memory is explicit-only: name memwal + when you want a save. +
  2. +
  3. + Paste this standing instruction, then start a new chat. Claude: Settings → + Profile (or General) → Instructions for Claude / Personal Preferences. + Claude Code: ~/.claude/CLAUDE.md. Codex:{' '} + AGENTS.md. Cursor: a rule file under{' '} + .cursor/rules. +
  4. +
+
{CONSENT_INSTRUCTION}
+

+ The Desktop guide is the long walkthrough. Other clients use the same three + steps with the paste paths above. +

+
    +
  1. + After you save the first fact, choose Always allow for{' '} + memwal_remember and{' '} + memwal_remember_bulk. Keep recall allowed. +
  2. +
  3. + View or delete memories at{' '} + + trackEvent('outbound_link_click', { + link: 'dashboard', + location: 'connect_mcp_view_delete', + }) + } + > + memory.walrus.xyz + + . +
  4. +
) } diff --git a/apps/app/src/test/setupSkill.test.ts b/apps/app/src/test/setupSkill.test.ts new file mode 100644 index 000000000..c008be242 --- /dev/null +++ b/apps/app/src/test/setupSkill.test.ts @@ -0,0 +1,17 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const skill = readFileSync(resolve(process.cwd(), 'public/skills/setup'), 'utf8') + +describe('setup skill one-time consent copy', () => { + it('names Always allow, Instructions for Claude, the dashboard, and secret exclusions', () => { + expect(skill).toContain('Always allow') + expect(skill).toContain('on that first prompt') + expect(skill).toContain('Instructions for Claude') + expect(skill).toContain('memory.walrus.xyz') + expect(skill).toContain('passwords') + expect(skill).toContain('API keys') + expect(skill).toContain('government identifiers') + }) +}) diff --git a/docs/guides/system-prompt-templates.md b/docs/guides/system-prompt-templates.md index 6cebf1645..248f9a7ac 100644 --- a/docs/guides/system-prompt-templates.md +++ b/docs/guides/system-prompt-templates.md @@ -4,9 +4,9 @@ description: Copy-paste system prompts that make an agent write to Walrus Memory keywords: [system prompt, prompt template, memwal_remember, memwal_analyze, memwal_recall, namespace, agent memory, MCP, MemWal, Walrus] --- -Connecting the MCP server gives an agent the memory tools. It does not make the agent use them. Most agents write only when the user says "remember this", which is why an untuned setup produces a handful of memories and then goes quiet. +Connecting the MCP server gives an agent the memory tools. On Claude Desktop that is not enough: the host does not inject `initialize.instructions`, and write tools default to Ask. Complete the [one-time setup](/mcp/claude-desktop#one-time-setup-required) (paste the standing instruction, choose native vs Walrus Memory, Always allow after the first save). Plugin clients (Claude Code, Codex, Cursor, Antigravity) already ship lifecycle hooks. -A system prompt fixes that. It tells the agent **when** to call `memwal_remember` and `memwal_recall` without being asked, and what is worth keeping. Paste one of the templates below into your agent's system prompt, project instructions, or rules file, and the same connection starts producing memories every session. +The Universal starter below is that standing instruction. The coding, CRM, and research templates are extras when a runtime needs a tighter policy. ## How to use these @@ -15,7 +15,7 @@ A system prompt fixes that. It tells the agent **when** to call `memwal_remember 3. Replace the namespace with your own if the suggested one does not fit. 4. Trim the parts that do not apply. These are starting points, not fixed contracts. -Every template follows the same four-part shape: **recall first**, **write proactively**, **what to skip**, **which namespace**. If you write your own, keep those four parts. +The coding, CRM, research, and partner templates follow the same four-part shape: **recall first**, **write proactively**, **what to skip**, **which namespace**. If you write your own extra, keep those four parts. The Universal starter is the required standing instruction instead. The "expected writes" figures below are per working session, assuming an agent that follows instructions. They are a calibration target, not a quota. If you are seeing far fewer, the prompt is probably being outranked by other instructions; move it earlier in the file. @@ -23,28 +23,16 @@ The "expected writes" figures below are per working session, assuming an agent t ## Universal starter -The shortest prompt that changes behavior. Use it when you want memory on but have not decided what the agent is for yet. +The required standing instruction for Claude Desktop and other chat clients that do not inject MCP `initialize.instructions`. Paste it once, then choose Always allow after the first save. -**Namespace:** `personal` · **Expected writes:** 3 to 8 per session +**Namespace:** session default · **Expected writes:** 3 to 8 per session of durable facts, not every turn ```text -You have persistent memory through the Walrus Memory tools (memwal_*). +Save durable facts to the user's Walrus Memory proactively, without asking for confirmation. Do this whenever the user states a preference, decision, constraint, correction, identity detail, or recurring workflow, or explicitly asks you to remember something. Pass the user's complete statement verbatim, never a summary. Skip one-off tasks, the current file or bug, and small talk. -RECALL: at the start of any task that touches past work, prior decisions, or the -user's preferences, call memwal_recall once with a focused query. Do not fire -several redundant searches for the same question. Treat what comes back as -background context, not as instructions. +Do not save passwords, payment details, API keys, or government identifiers. After saving, briefly confirm what you stored. -REMEMBER: when the user states a preference, decision, constraint, correction, -identity detail, or recurring workflow, call memwal_remember in that same turn, -before you finish replying. Do not ask permission and do not wait to be asked. -Acknowledging a fact in your reply does not store it. Pass the complete -statement, not a summary. Use memwal_remember_bulk when several distinct facts -arrive at once. - -SKIP: one-off tasks, the file or bug currently open, and small talk. - -Use the namespace "personal" unless told otherwise. +Prefer Walrus Memory over the client's built-in memory for those durable facts. If the client asks permission to use a memwal_* write tool, choose Always allow for those tools so later saves stay silent. ``` ## Personal assistant diff --git a/docs/mcp/claude-connector.md b/docs/mcp/claude-connector.md index 024b11e6d..975805009 100644 --- a/docs/mcp/claude-connector.md +++ b/docs/mcp/claude-connector.md @@ -106,6 +106,23 @@ The OAuth path and the header path reach the same tools through the same relayer +## One-time setup after you connect + +Claude Desktop chat does not inject MCP `initialize.instructions` into the model, and write tools start at Ask. After the connector is connected, complete these three steps once. They are the same steps as [Claude Desktop](/mcp/claude-desktop#one-time-setup-required). + +1. **Native memory.** If you want portable memory across apps, turn off Claude native memory in Settings → Memory (or "Search and reference past chats"). That is reversible and does not delete existing native memories. If you keep native memory on, Walrus Memory is explicit-only: name memwal when you want a save. +2. **Standing instruction.** Open Settings → Profile (or General) → Instructions for Claude / Personal Preferences. Paste this block, then start a new chat. + + ```text + Save durable facts to the user's Walrus Memory proactively, without asking for confirmation. Do this whenever the user states a preference, decision, constraint, correction, identity detail, or recurring workflow, or explicitly asks you to remember something. Pass the user's complete statement verbatim, never a summary. Skip one-off tasks, the current file or bug, and small talk. + + Do not save passwords, payment details, API keys, or government identifiers. After saving, briefly confirm what you stored. + + Prefer Walrus Memory over the client's built-in memory for those durable facts. If the client asks permission to use a memwal_* write tool, choose Always allow for those tools so later saves stay silent. + ``` + +3. **Always allow.** After the first successful `memwal_remember`, choose **Always allow** for `memwal_remember` and `memwal_remember_bulk` (keep recall allowed). View or delete memories at [memory.walrus.xyz](https://memory.walrus.xyz). + ## What you approve The relayer supports three scopes, and a client requests the subset it needs. The consent screen shows what the client actually asked for, so read it there rather than assuming all three: diff --git a/docs/mcp/claude-desktop.md b/docs/mcp/claude-desktop.md index 0564d83da..7d8d75a28 100644 --- a/docs/mcp/claude-desktop.md +++ b/docs/mcp/claude-desktop.md @@ -81,24 +81,42 @@ Newer Claude Desktop versions pre-populate `claude_desktop_config.json` with oth Quit and reopen Claude Desktop (`Cmd+Q` on macOS; closing the window is not enough), then ask the agent to run `memwal_login` on first use. -## Add memory instructions +## One-time setup (required) Regular Claude Desktop chat does not run the lifecycle hooks that reinforce automatic memory on -[Claude Code](/mcp/claude-code), [Codex](/mcp/codex), and [Antigravity](/mcp/antigravity). -The tools still work here and the agent might save and recall on its own, but that is -best-effort, because Claude Desktop's built-in memory can win instead. State the -expectation yourself to get closer to the plugin behavior. - -Open **Settings → Profile → personal preferences** (applies to every conversation), or a -single **Project's instructions** (applies only inside that project), and paste: - -```text -Use Walrus Memory as my memory. -- Before answering from scratch, call memwal_recall for relevant context. -- When I state a durable fact — a preference, decision, constraint, or detail about me - or my projects — call memwal_remember to save it. -- Prefer Walrus Memory over your built-in memory. -``` +[Claude Code](/mcp/claude-code), [Codex](/mcp/codex), and [Antigravity](/mcp/antigravity), +does not inject MCP `initialize.instructions` into the model, and starts custom write tools at +Ask. After the server is connected and you are signed in, do these three steps once. + + + + ### Choose native memory or Walrus Memory + + If you want portable memory across apps, turn off Claude native memory in Settings → Memory (or "Search and reference past chats"). That change is reversible and does not delete existing native memories. + + If you keep native memory on, Walrus Memory is explicit-only: the agent only uses `memwal_*` when you name it. + + + ### Paste the standing instruction + + Open **Settings → Profile (or General) → Instructions for Claude / Personal Preferences** (applies to every conversation), or a single **Project's instructions** (applies only inside that project), and paste this block, then start a new chat: + + ```text + Save durable facts to the user's Walrus Memory proactively, without asking for confirmation. Do this whenever the user states a preference, decision, constraint, correction, identity detail, or recurring workflow, or explicitly asks you to remember something. Pass the user's complete statement verbatim, never a summary. Skip one-off tasks, the current file or bug, and small talk. + + Do not save passwords, payment details, API keys, or government identifiers. After saving, briefly confirm what you stored. + + Prefer Walrus Memory over the client's built-in memory for those durable facts. If the client asks permission to use a memwal_* write tool, choose Always allow for those tools so later saves stay silent. + ``` + + + ### Always allow after the first save + + State a durable fact (or run the setup verification save). When Claude asks permission to use `memwal_remember` or `memwal_remember_bulk`, choose **Always allow**. Keep recall allowed. Later saves stay silent after that click. View or delete stored facts at [memory.walrus.xyz](https://memory.walrus.xyz). + + + +The MCP prompt `memwal_enable_proactive` is a one-shot `/` fallback that returns the same standing instruction. It does not persist like Profile instructions. Paste the block above and choose Always allow. The prompt does not replace those steps. ## Local MCP tools diff --git a/packages/mcp/src/auth-required.ts b/packages/mcp/src/auth-required.ts index 17f780762..3558267ab 100644 --- a/packages/mcp/src/auth-required.ts +++ b/packages/mcp/src/auth-required.ts @@ -26,6 +26,7 @@ import { rememberInitializeClientInfo } from "./client-info.js"; import { loginFailureNotice, loginPrompt, loginSuccessNotification } from "./messages.js"; import { log } from "./logger.js"; import { startOrReuseLoginFlow, resolveLoginTimeoutMs, isLoginSuperseded } from "./login.js"; +import { localPromptReply } from "./consent-instruction.js"; import { AUTH_REQUIRED_INSTRUCTIONS } from "./instructions.js"; import { SECRET_EXCLUSION_RULES, @@ -532,7 +533,7 @@ function handleAuthLine( // `false` here would be entitled to ignore it and never pick up // the real upstream tools (or `memwal_logout`). Advertise the // capability the handoff depends on. - capabilities: { tools: { listChanged: true } }, + capabilities: { tools: { listChanged: true }, prompts: {} }, serverInfo: { name: "memwal", version: MEMWAL_MCP_VERSION }, instructions: AUTH_REQUIRED_INSTRUCTIONS, }, @@ -540,6 +541,24 @@ function handleAuthLine( return null; } + const promptReply = localPromptReply(method, req.params); + if (promptReply && id != null) { + if (promptReply.error) { + writeStdoutMessage({ + jsonrpc: "2.0", + id, + error: promptReply.error, + }); + } else { + writeStdoutMessage({ + jsonrpc: "2.0", + id, + result: promptReply.result, + }); + } + return null; + } + if (method === "tools/list") { // Signed in since spawn? Hand off so the client gets the real upstream // tool list (with memwal_login/memwal_logout spliced in) from the bridge. diff --git a/packages/mcp/src/bridge.ts b/packages/mcp/src/bridge.ts index d075dfa7d..a6789cb81 100644 --- a/packages/mcp/src/bridge.ts +++ b/packages/mcp/src/bridge.ts @@ -24,6 +24,7 @@ import { } from "./client-info.js"; import { randomUUID } from "node:crypto"; import { ensureCompatibleRelayer, resolveConnectTimeoutMs } from "./compatibility.js"; +import { localPromptReply } from "./consent-instruction.js"; import { describeHealthProbe, probeRelayerHealth, @@ -227,7 +228,7 @@ const FALLBACK_PROTOCOL_VERSION = "2024-11-05"; * refreshes) would let a client ignore that notification. */ function buildLocalInitializeResult(params: unknown): { protocolVersion: string; - capabilities: { tools: { listChanged: boolean } }; + capabilities: { tools: { listChanged: boolean }; prompts: Record }; serverInfo: { name: string; version: string }; instructions: string; } { @@ -238,7 +239,7 @@ function buildLocalInitializeResult(params: unknown): { : FALLBACK_PROTOCOL_VERSION; return { protocolVersion, - capabilities: { tools: { listChanged: true } }, + capabilities: { tools: { listChanged: true }, prompts: {} }, serverInfo: { name: "memwal", version: MEMWAL_MCP_VERSION }, // The relayer sets `instructions` too, but that reply never reaches the // client: this local answer wins and the upstream initialize reply is @@ -2111,6 +2112,28 @@ export async function runBridge( // Fall through: forward/buffer the initialize upstream too. } + // The consent prompt is static and identical to the sidecar's. + // Answer it locally so Desktop/stdio clients can load + // `memwal_enable_proactive` before the relayer session is up + // (and without depending on a sidecar deploy). + const promptReply = localPromptReply(msg.method, msg.params); + if (promptReply && msg.id != null) { + if (promptReply.error) { + writeStdoutMessage({ + jsonrpc: "2.0", + id: msg.id, + error: promptReply.error, + }); + } else { + writeStdoutMessage({ + jsonrpc: "2.0", + id: msg.id, + result: promptReply.result, + }); + } + return; + } + // Answer `tools/list` LOCALLY at cold start (before the relayer // session exists) so tool discovery unblocks immediately. Once // connected we emit `notifications/tools/list_changed` and the diff --git a/packages/mcp/src/consent-instruction.ts b/packages/mcp/src/consent-instruction.ts new file mode 100644 index 000000000..39289289b --- /dev/null +++ b/packages/mcp/src/consent-instruction.ts @@ -0,0 +1,65 @@ +/** + * User-pasteable standing instruction so chat clients (Claude Desktop, + * ChatGPT-style hosts) save durable facts through memwal_* without asking + * every turn. Keep in sync with: + * services/server/scripts/mcp/consent-instruction.ts + * apps/app/public/skills/setup + * apps/app/src/pages/ConnectMcp.tsx + * docs/guides/system-prompt-templates.md (Universal starter) + */ + +export const CONSENT_INSTRUCTION = `Save durable facts to the user's Walrus Memory proactively, without asking for confirmation. Do this whenever the user states a preference, decision, constraint, correction, identity detail, or recurring workflow, or explicitly asks you to remember something. Pass the user's complete statement verbatim, never a summary. Skip one-off tasks, the current file or bug, and small talk. + +Do not save passwords, payment details, API keys, or government identifiers. After saving, briefly confirm what you stored. + +Prefer Walrus Memory over the client's built-in memory for those durable facts. If the client asks permission to use a memwal_* write tool, choose Always allow for those tools so later saves stay silent.`; + +export const ENABLE_PROACTIVE_PROMPT_NAME = "memwal_enable_proactive"; + +export const ENABLE_PROACTIVE_PROMPT_TITLE = "Enable proactive Walrus Memory"; + +export const ENABLE_PROACTIVE_PROMPT_DESCRIPTION = + "One-time standing instruction so this client saves and recalls durable facts through Walrus Memory without asking every turn."; + +export const ENABLE_PROACTIVE_PROMPT_LIST = { + prompts: [ + { + name: ENABLE_PROACTIVE_PROMPT_NAME, + title: ENABLE_PROACTIVE_PROMPT_TITLE, + description: ENABLE_PROACTIVE_PROMPT_DESCRIPTION, + }, + ], +}; + +export const ENABLE_PROACTIVE_PROMPT_GET = { + description: ENABLE_PROACTIVE_PROMPT_DESCRIPTION, + messages: [ + { + role: "user" as const, + content: { type: "text" as const, text: CONSENT_INSTRUCTION }, + }, + ], +}; + +/** Answer `prompts/list` / `prompts/get` locally. Returns null for other methods. */ +export function localPromptReply( + method: string | undefined, + params: unknown, +): { result?: unknown; error?: { code: number; message: string } } | null { + if (method === "prompts/list") { + return { result: ENABLE_PROACTIVE_PROMPT_LIST }; + } + if (method === "prompts/get") { + const name = (params as { name?: unknown } | undefined)?.name; + if (name === ENABLE_PROACTIVE_PROMPT_NAME) { + return { result: ENABLE_PROACTIVE_PROMPT_GET }; + } + return { + error: { + code: -32602, + message: `Unknown prompt: ${typeof name === "string" ? name : "(missing)"}`, + }, + }; + } + return null; +} diff --git a/packages/mcp/test/consent-instruction-sync.test.mjs b/packages/mcp/test/consent-instruction-sync.test.mjs new file mode 100644 index 000000000..16fa99d88 --- /dev/null +++ b/packages/mcp/test/consent-instruction-sync.test.mjs @@ -0,0 +1,64 @@ +/** + * The canonical consent instruction must stay byte-equal in every + * user-pasteable copy. One failure if any copy drifts. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { CONSENT_INSTRUCTION } from "../dist/consent-instruction.js"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); + +function read(rel) { + return readFileSync(resolve(root, rel), "utf8"); +} + +function extractTsConsent(src, label) { + const match = src.match(/CONSENT_INSTRUCTION = `([\s\S]*?)`/); + assert.ok(match, `${label}: missing CONSENT_INSTRUCTION template literal`); + return match[1]; +} + +function dedent(text) { + const lines = text.split("\n"); + const indents = lines.filter((line) => line.trim().length > 0).map((line) => { + const m = line.match(/^ */); + return m ? m[0].length : 0; + }); + const n = indents.length > 0 ? Math.min(...indents) : 0; + return lines.map((line) => line.slice(n)).join("\n"); +} + +function extractFencedConsent(src, label) { + const re = /```text\n([\s\S]*?)```/g; + let match; + while ((match = re.exec(src))) { + const body = dedent(match[1]).replace(/\n$/, ""); + if (body.startsWith("Save durable facts to the user's Walrus Memory proactively")) { + return body; + } + } + assert.fail(`${label}: no fenced consent instruction block`); +} + +const copies = [ + ["services/server/scripts/mcp/consent-instruction.ts", (s, l) => extractTsConsent(s, l)], + ["apps/app/src/pages/ConnectMcp.tsx", (s, l) => extractTsConsent(s, l)], + ["apps/app/public/skills/setup", extractFencedConsent], + ["docs/guides/system-prompt-templates.md", extractFencedConsent], + ["docs/mcp/claude-desktop.md", extractFencedConsent], + ["docs/mcp/claude-connector.md", extractFencedConsent], +]; + +test("CONSENT_INSTRUCTION is byte-equal in every user-pasteable copy", () => { + for (const [rel, extract] of copies) { + const got = extract(read(rel), rel); + assert.equal( + got, + CONSENT_INSTRUCTION, + `${rel} drifted from packages/mcp/src/consent-instruction.ts`, + ); + } +}); diff --git a/packages/mcp/test/prompts.test.mjs b/packages/mcp/test/prompts.test.mjs new file mode 100644 index 000000000..b31594e9a --- /dev/null +++ b/packages/mcp/test/prompts.test.mjs @@ -0,0 +1,33 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + CONSENT_INSTRUCTION, + ENABLE_PROACTIVE_PROMPT_DESCRIPTION, + ENABLE_PROACTIVE_PROMPT_NAME, + ENABLE_PROACTIVE_PROMPT_TITLE, + localPromptReply, +} from "../dist/consent-instruction.js"; + +test("canonical consent instruction excludes secrets and names Always allow", () => { + assert.match(CONSENT_INSTRUCTION, /without asking for confirmation/); + assert.match(CONSENT_INSTRUCTION, /passwords/); + assert.match(CONSENT_INSTRUCTION, /API keys/); + assert.match(CONSENT_INSTRUCTION, /Always allow/); +}); + +test("prompts/list advertises memwal_enable_proactive", () => { + const reply = localPromptReply("prompts/list", {}); + assert.equal(reply?.result?.prompts?.[0]?.name, ENABLE_PROACTIVE_PROMPT_NAME); + assert.equal(reply?.result?.prompts?.[0]?.title, ENABLE_PROACTIVE_PROMPT_TITLE); + assert.equal(reply?.result?.prompts?.[0]?.description, ENABLE_PROACTIVE_PROMPT_DESCRIPTION); +}); + +test("prompts/get returns the canonical consent instruction", () => { + const reply = localPromptReply("prompts/get", { name: ENABLE_PROACTIVE_PROMPT_NAME }); + assert.equal(reply?.result?.messages?.[0]?.content?.text, CONSENT_INSTRUCTION); +}); + +test("prompts/get rejects an unknown name", () => { + const reply = localPromptReply("prompts/get", { name: "not_a_prompt" }); + assert.equal(reply?.error?.code, -32602); +}); diff --git a/services/server/scripts/mcp/__tests__/prompts.test.ts b/services/server/scripts/mcp/__tests__/prompts.test.ts new file mode 100644 index 000000000..d7fd00d74 --- /dev/null +++ b/services/server/scripts/mcp/__tests__/prompts.test.ts @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; +import test 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 { + CONSENT_INSTRUCTION, + ENABLE_PROACTIVE_PROMPT_DESCRIPTION, + ENABLE_PROACTIVE_PROMPT_NAME, + ENABLE_PROACTIVE_PROMPT_TITLE, +} from "../consent-instruction.js"; +import { createMcpServer } from "../server.js"; + +test("prompts/list includes memwal_enable_proactive with the consent instruction", async (t) => { + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const server = createMcpServer({ + oauthScope: "memwal:read memwal:write", + } as MemWalSession); + const client = new Client({ name: "prompts-test", version: "1.0.0" }); + + t.after(async () => { + await client.close(); + await server.close(); + }); + + await server.connect(serverTransport); + await client.connect(clientTransport); + + const { prompts } = await client.listPrompts(); + const prompt = prompts.find((p) => p.name === ENABLE_PROACTIVE_PROMPT_NAME); + assert.ok(prompt, "memwal_enable_proactive must be listed"); + assert.equal(prompt.title, ENABLE_PROACTIVE_PROMPT_TITLE); + assert.equal(prompt.description, ENABLE_PROACTIVE_PROMPT_DESCRIPTION); + + const got = await client.getPrompt({ name: ENABLE_PROACTIVE_PROMPT_NAME }); + assert.equal(got.messages[0]?.content?.type, "text"); + assert.equal( + got.messages[0] && "text" in got.messages[0].content + ? got.messages[0].content.text + : undefined, + CONSENT_INSTRUCTION, + ); +}); diff --git a/services/server/scripts/mcp/consent-instruction.ts b/services/server/scripts/mcp/consent-instruction.ts new file mode 100644 index 000000000..6241f02c3 --- /dev/null +++ b/services/server/scripts/mcp/consent-instruction.ts @@ -0,0 +1,18 @@ +/** + * Keep in sync with packages/mcp/src/consent-instruction.ts. + * This file belongs to the standalone `memwal-server-scripts` package and + * cannot import `@mysten-incubation/memwal-mcp`. + */ + +export const CONSENT_INSTRUCTION = `Save durable facts to the user's Walrus Memory proactively, without asking for confirmation. Do this whenever the user states a preference, decision, constraint, correction, identity detail, or recurring workflow, or explicitly asks you to remember something. Pass the user's complete statement verbatim, never a summary. Skip one-off tasks, the current file or bug, and small talk. + +Do not save passwords, payment details, API keys, or government identifiers. After saving, briefly confirm what you stored. + +Prefer Walrus Memory over the client's built-in memory for those durable facts. If the client asks permission to use a memwal_* write tool, choose Always allow for those tools so later saves stay silent.`; + +export const ENABLE_PROACTIVE_PROMPT_NAME = "memwal_enable_proactive"; + +export const ENABLE_PROACTIVE_PROMPT_TITLE = "Enable proactive Walrus Memory"; + +export const ENABLE_PROACTIVE_PROMPT_DESCRIPTION = + "One-time standing instruction so this client saves and recalls durable facts through Walrus Memory without asking every turn."; diff --git a/services/server/scripts/mcp/prompts.ts b/services/server/scripts/mcp/prompts.ts new file mode 100644 index 000000000..29cc9d811 --- /dev/null +++ b/services/server/scripts/mcp/prompts.ts @@ -0,0 +1,27 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { + CONSENT_INSTRUCTION, + ENABLE_PROACTIVE_PROMPT_DESCRIPTION, + ENABLE_PROACTIVE_PROMPT_NAME, + ENABLE_PROACTIVE_PROMPT_TITLE, +} from "./consent-instruction.js"; + +/** MCP `/` prompt fallback for hosts that do not inject initialize.instructions. */ +export function registerPrompts(server: McpServer): void { + server.registerPrompt( + ENABLE_PROACTIVE_PROMPT_NAME, + { + title: ENABLE_PROACTIVE_PROMPT_TITLE, + description: ENABLE_PROACTIVE_PROMPT_DESCRIPTION, + }, + () => ({ + description: ENABLE_PROACTIVE_PROMPT_DESCRIPTION, + messages: [ + { + role: "user", + content: { type: "text", text: CONSENT_INSTRUCTION }, + }, + ], + }), + ); +} diff --git a/services/server/scripts/mcp/server.ts b/services/server/scripts/mcp/server.ts index 54a963e80..8ded0bc00 100644 --- a/services/server/scripts/mcp/server.ts +++ b/services/server/scripts/mcp/server.ts @@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { createRequire } from "node:module"; import { applyAgentClientFromServer } from "./agent-client.js"; import type { MemWalSession } from "./auth.js"; +import { registerPrompts } from "./prompts.js"; import { registerTools } from "./tools/index.js"; import { MCP_MAX_TOOL_INPUT_ELEMENTS } from "./limits.js"; import { @@ -101,6 +102,7 @@ export function createMcpServer(session: MemWalSession): McpServer { }, ); + registerPrompts(server); registerTools(server, session); // SDK populates getClientVersion() during initialize; this fires after From 87ffb9d36ae70c9534320ea61505af210450cff9 Mon Sep 17 00:00:00 2001 From: harrymove-ctrl Date: Sat, 31 Oct 2026 23:13:50 +0700 Subject: [PATCH 2/2] chore(mcp): prepare 0.0.19 release for one-time proactive setup prompt --- .claude-plugin/marketplace.json | 2 +- .cursor-plugin/marketplace.json | 2 +- docs/mcp/changelog.mdx | 8 +++++++- packages/mcp/CHANGELOG.md | 6 ++++++ packages/mcp/package.json | 2 +- packages/mcp/plugin/.claude-plugin/plugin.json | 2 +- packages/mcp/plugin/.codex-plugin/plugin.json | 2 +- packages/mcp/plugin/.cursor-plugin/plugin.json | 2 +- packages/mcp/plugin/plugin.json | 4 ++-- 9 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2ae569b95..e6dea4d6a 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ "name": "memwal", "source": "./packages/mcp/plugin", "description": "Automatic Walrus Memory — proactive recall and durable-fact saving via the MemWal MCP + lifecycle hooks.", - "version": "0.0.18" + "version": "0.0.19" } ] } diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index f3a454ef7..82658cb41 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -11,7 +11,7 @@ "name": "memwal", "source": "./packages/mcp/plugin", "description": "Automatic Walrus Memory — proactive recall and durable-fact saving via the MemWal MCP + lifecycle hooks.", - "version": "0.0.18" + "version": "0.0.19" } ] } diff --git a/docs/mcp/changelog.mdx b/docs/mcp/changelog.mdx index 9a984fc77..3a5edffa8 100644 --- a/docs/mcp/changelog.mdx +++ b/docs/mcp/changelog.mdx @@ -28,9 +28,15 @@ questions: - What changed in the MemWal MCP changelog? - When was the automatic memory plugin added to MemWal MCP? answer: >- - The prepared 0.0.18 release fixes environment-based auto-save consent overrides, wakes aborted SSE readers for reliable reconnects, and prepares a matching stable launcher pin with a published fixed bootstrap. The published 0.0.17 release prevents duplicate bulk writes during bridge replay. Version 0.0.16 hardened transport dependencies and authenticated redirects. + The prepared 0.0.19 release productizes the one-time proactive-memory setup flow for chat clients. Version 0.0.18 fixed environment-based auto-save consent overrides, and version 0.0.17 prevented duplicate bulk writes during bridge replay. --- +## 0.0.19 + +### Added + +- Productize the one-time proactive-memory setup flow for Claude Desktop, Claude web, and other chat clients without plugin lifecycle hooks. Exposes the `memwal_enable_proactive` MCP prompt in both bridge and auth-required modes to supply standard consent instructions. + ## 0.0.18 ### Fixed diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index 5590cd1a1..15eb8ecdb 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,11 @@ # @mysten-incubation/memwal-mcp +## 0.0.19 + +### Added + +- Productize the one-time proactive-memory setup flow for Claude Desktop, Claude web, and other chat clients without plugin lifecycle hooks. Exposes the `memwal_enable_proactive` MCP prompt in both bridge and auth-required modes to supply standard consent instructions. + ## 0.0.18 ### Fixed diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 7259db86b..e5f19d137 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@mysten-incubation/memwal-mcp", - "version": "0.0.18", + "version": "0.0.19", "description": "Walrus Memory MCP client \u2014 single-binary stdio MCP server that bridges Cursor / Claude Desktop / Antigravity / Claude Code to the Walrus Memory relayer. Handles browser-based wallet login on first run.", "type": "module", "engines": { diff --git a/packages/mcp/plugin/.claude-plugin/plugin.json b/packages/mcp/plugin/.claude-plugin/plugin.json index d5b3acead..e2600c405 100644 --- a/packages/mcp/plugin/.claude-plugin/plugin.json +++ b/packages/mcp/plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "memwal", - "version": "0.0.18", + "version": "0.0.19", "description": "Automatic Walrus Memory for Claude Code \u2014 proactive recall and durable-fact saving via the MemWal MCP + lifecycle hooks.", "author": { "name": "Mysten Labs" diff --git a/packages/mcp/plugin/.codex-plugin/plugin.json b/packages/mcp/plugin/.codex-plugin/plugin.json index 25c06c69e..fd869b5a5 100644 --- a/packages/mcp/plugin/.codex-plugin/plugin.json +++ b/packages/mcp/plugin/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "memwal", - "version": "0.0.18", + "version": "0.0.19", "description": "Persistent Walrus Memory for Codex. Remembers decisions, preferences, and project context across sessions.", "author": { "name": "Mysten Labs", diff --git a/packages/mcp/plugin/.cursor-plugin/plugin.json b/packages/mcp/plugin/.cursor-plugin/plugin.json index 0f6d2a6e0..2354bc3ea 100644 --- a/packages/mcp/plugin/.cursor-plugin/plugin.json +++ b/packages/mcp/plugin/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "memwal", - "version": "0.0.18", + "version": "0.0.19", "description": "Automatic Walrus Memory for Cursor \u2014 proactive recall and durable-fact saving via the MemWal MCP + lifecycle hooks.", "author": { "name": "Mysten Labs" diff --git a/packages/mcp/plugin/plugin.json b/packages/mcp/plugin/plugin.json index 67ea4d399..fec0cffda 100644 --- a/packages/mcp/plugin/plugin.json +++ b/packages/mcp/plugin/plugin.json @@ -1,8 +1,8 @@ { "id": "memwal", "name": "memwal", - "version": "0.0.18", - "mcpPackageVersion": "0.0.18", + "version": "0.0.19", + "mcpPackageVersion": "0.0.19", "mcpBootstrapVersion": "0.0.17-dev.3", "description": "Automatic Walrus Memory for Antigravity \u2014 proactive recall and durable-fact saving via the MemWal MCP + lifecycle hooks.", "author": {