From 4282a2aba5eb4c8691f3c6e3caa6fd96dc405499 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:46:19 +0000 Subject: [PATCH 1/6] fix: MCP proxy uses the repo space instead of activeSpace Hosted MCP defaults a missing containerTag to the user's durable activeSpace, so plugin search/save could miss the repo container the hooks already write to. Inject the repo tag on space-scoped tool calls unless one is already set. --- plugin/hooks/mcp-proxy.js | 52 +++++++++++++++++++- test/unit.mjs | 101 +++++++++++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/plugin/hooks/mcp-proxy.js b/plugin/hooks/mcp-proxy.js index 5add792..89e65b5 100644 --- a/plugin/hooks/mcp-proxy.js +++ b/plugin/hooks/mcp-proxy.js @@ -4,14 +4,56 @@ // browser login covers both. Messages are forwarded sequentially to preserve // JSON-RPC ordering; SSE responses are unwrapped back into stdout lines. const readline = require('node:readline'); +const { getContainerTag } = require('./lib/container-tag'); const { getApiKey } = require('./lib/settings'); const MCP_URL = process.env.SUPERMEMORY_MCP_URL || 'https://mcp.supermemory.ai/mcp'; const REQUEST_TIMEOUT_MS = 30000; +// Hosted MCP treats a missing containerTag as the user's durable activeSpace, +// which is shared across every MCP client and is not this repo. Hooks already +// read/write the repo tag; inject it on space-scoped tools so MCP hits the +// same container. Leave an explicit containerTag and set-active-tag alone. +const REPO_SCOPED_TOOLS = new Set([ + 'search_memory', + 'add_memory', + 'listDocuments', + 'listMemories', + 'memory-graph', + 'fetch-graph-data', + 'save-memory', +]); + let sessionId = null; +function injectRepoContainerTag(message, containerTag) { + if (!containerTag || message.method !== 'tools/call') return; + const params = message.params; + if (!params || typeof params !== 'object') return; + if (!REPO_SCOPED_TOOLS.has(params.name)) return; + + let args = params.arguments; + let encoded = false; + if (args == null) { + params.arguments = { containerTag }; + return; + } + if (typeof args === 'string') { + try { + args = JSON.parse(args); + encoded = true; + } catch { + return; + } + } + if (!args || typeof args !== 'object' || Array.isArray(args)) return; + if (typeof args.containerTag === 'string' && args.containerTag.trim()) return; + + args.containerTag = containerTag; + params.arguments = encoded ? JSON.stringify(args) : args; +} + function send(message) { process.stdout.write(`${JSON.stringify(message)}\n`); } @@ -73,13 +115,20 @@ async function forward(message, apiKey) { } async function main() { + const cwd = process.cwd(); let apiKey = null; let keyError = null; + let repoContainerTag = null; try { - apiKey = getApiKey(process.cwd()); + apiKey = getApiKey(cwd); } catch (err) { keyError = err; } + try { + repoContainerTag = getContainerTag(cwd); + } catch { + repoContainerTag = null; + } let queue = Promise.resolve(); const rl = readline.createInterface({ input: process.stdin }); @@ -103,6 +152,7 @@ async function main() { return; } try { + injectRepoContainerTag(message, repoContainerTag); await forward(message, apiKey); } catch (err) { sendError(message.id, -32000, `Supermemory MCP proxy error: ${err.message}`); diff --git a/test/unit.mjs b/test/unit.mjs index c883f95..f627497 100644 --- a/test/unit.mjs +++ b/test/unit.mjs @@ -497,10 +497,11 @@ describe('capture hook', () => { }); describe('mcp proxy', () => { - function runProxy(t, env, lines) { + function runProxy(t, env, lines, options = {}) { return new Promise((resolve, reject) => { const child = spawn('node', [join(HOOKS_DIR, 'mcp-proxy.js')], { env: { ...process.env, ...env }, + cwd: options.cwd, stdio: ['pipe', 'pipe', 'pipe'], }); let stdout = ''; @@ -570,6 +571,104 @@ describe('mcp proxy', () => { assert.equal(messages[0].error.code, -32001); assert.match(messages[0].error.message, /not authenticated/); }); + + test('injects the repo container tag when MCP tools omit it', async (t) => { + const { repo, home } = makeRepo(t); + const expected = readTags(repo, home).tag; + const stub = await startStubServer(t, (record, res) => { + res.setHeader('Content-Type', 'application/json'); + const { id } = JSON.parse(record.body); + res.end(JSON.stringify({ jsonrpc: '2.0', id, result: { ok: true } })); + }); + + await runProxy( + t, + { + HOME: home, + USERPROFILE: home, + SUPERMEMORY_CC_API_KEY: 'sm_test_key_0123456789abcdef', + SUPERMEMORY_MCP_URL: `${stub.url}/mcp`, + }, + [ + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'search_memory', arguments: { query: 'auth' } }, + }, + { + jsonrpc: '2.0', + id: 2, + method: 'tools/call', + params: { name: 'add_memory', arguments: { content: 'remember this' } }, + }, + { + jsonrpc: '2.0', + id: 3, + method: 'tools/call', + params: { name: 'listDocuments' }, + }, + ], + { cwd: repo }, + ); + + const forwarded = stub.requests.map((r) => JSON.parse(r.body)); + assert.equal(forwarded[0].params.arguments.containerTag, expected); + assert.equal(forwarded[0].params.arguments.query, 'auth'); + assert.equal(forwarded[1].params.arguments.containerTag, expected); + assert.equal(forwarded[2].params.arguments.containerTag, expected); + }); + + test('keeps an explicit containerTag and does not rewrite unrelated tools', async (t) => { + const { repo, home } = makeRepo(t); + const stub = await startStubServer(t, (record, res) => { + res.setHeader('Content-Type', 'application/json'); + const { id } = JSON.parse(record.body); + res.end(JSON.stringify({ jsonrpc: '2.0', id, result: { ok: true } })); + }); + + await runProxy( + t, + { + HOME: home, + USERPROFILE: home, + SUPERMEMORY_CC_API_KEY: 'sm_test_key_0123456789abcdef', + SUPERMEMORY_MCP_URL: `${stub.url}/mcp`, + }, + [ + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { + name: 'search_memory', + arguments: { query: 'auth', containerTag: 'other_space' }, + }, + }, + { + jsonrpc: '2.0', + id: 2, + method: 'tools/call', + params: { name: 'set-active-tag', arguments: { containerTag: 'picked' } }, + }, + { + jsonrpc: '2.0', + id: 3, + method: 'tools/call', + params: { name: 'whoAmI' }, + }, + { jsonrpc: '2.0', id: 4, method: 'tools/list' }, + ], + { cwd: repo }, + ); + + const forwarded = stub.requests.map((r) => JSON.parse(r.body)); + assert.equal(forwarded[0].params.arguments.containerTag, 'other_space'); + assert.equal(forwarded[1].params.arguments.containerTag, 'picked'); + assert.equal(forwarded[2].params.arguments, undefined); + assert.equal(forwarded[3].method, 'tools/list'); + assert.equal(forwarded[3].params, undefined); + }); }); describe('statusline state', () => { From 5d6c501cb9a9c6a4d9d164ee8bc1fc18c2e6fcac Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:59:39 +0000 Subject: [PATCH 2/6] fix: advertise that search_memory defaults to the repo space Proxy injection means omitting containerTag no longer hits activeSpace. Tell the model search_memory is already scoped to this project, and to pass containerTag only when searching somewhere else. --- plugin/agents/context-gatherer.md | 2 +- plugin/hooks/recall-directive.js | 2 +- test/unit.mjs | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/agents/context-gatherer.md b/plugin/agents/context-gatherer.md index 2331cf4..54addee 100644 --- a/plugin/agents/context-gatherer.md +++ b/plugin/agents/context-gatherer.md @@ -10,7 +10,7 @@ You are the Supermemory context gatherer. Your job: assemble the background a co ## Process -1. Identify the project's memory container from the task prompt (the caller passes the active containerTag; if not, call `listSpaces` and pick the container matching the repo name). +1. Search this project's container by default (`search_memory` with no `containerTag` is already scoped to the repo). If the caller names a different space, resolve it with `listSpaces` and pass that `containerTag`. 2. Run several `search_memory` calls from different angles, not one broad query: - the specific task or files named in the prompt - recent decisions and conventions in this repo diff --git a/plugin/hooks/recall-directive.js b/plugin/hooks/recall-directive.js index 89d8cb6..be453ec 100644 --- a/plugin/hooks/recall-directive.js +++ b/plugin/hooks/recall-directive.js @@ -85,7 +85,7 @@ function formatRecall(results, containerTag) { ◪ Recalled from supermemory for this prompt (relevance-ranked): ${lines.join('\n')} -When one of these shapes your answer, credit it naturally with the ◪ prefix (e.g. "◪ earlier you decided X"); if you name the source, say "from supermemory" — never "from memory". For deeper history, call the supermemory search_memory tool (containerTag: "${containerTag}") or launch the context-gatherer agent. +When one of these shapes your answer, credit it naturally with the ◪ prefix (e.g. "◪ earlier you decided X"); if you name the source, say "from supermemory" — never "from memory". For deeper history, call the supermemory search_memory tool — it defaults to this project's container (${containerTag}). Pass containerTag only to search a different space. Or launch the context-gatherer agent. `; } diff --git a/test/unit.mjs b/test/unit.mjs index f627497..035040d 100644 --- a/test/unit.mjs +++ b/test/unit.mjs @@ -206,6 +206,8 @@ describe('recall-directive hook', () => { assert.match(context, /- ◪ Migration plan — Use expand-contract migrations/); assert.doesNotMatch(context, /irrelevant low-similarity hit/); assert.match(context, /repo_example_project__/); + assert.match(context, /defaults to this project's container/); + assert.doesNotMatch(context, /omit containerTag to search the account/i); assert.match(plain(output.systemMessage), /^◪ supermemory · recalled \d+ memories \(\d+ tok\)$/); assert.equal(stub.requests[0].url, '/v4/profile'); assert.equal( From 490b27adce3ec736c907d2401b657c7e0b56cdfd Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:04:34 +0000 Subject: [PATCH 3/6] chore: bump plugin to 0.1.7 CI does not bump the marketplace version. Ship the repo-space MCP routing fix as 0.1.7 so plugin update notices fire. --- latest.json | 2 +- package.json | 2 +- plugin/.claude-plugin/plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/latest.json b/latest.json index 839aa84..c084fbe 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "version": "0.1.6", + "version": "0.1.7", "updateCommand": "claude plugin update supermemory@supermemory-plugins" } diff --git a/package.json b/package.json index 8a88868..55efcbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-supermemory-dev", - "version": "0.1.6", + "version": "0.1.7", "description": "Claude code plugin by Supermemory AI", "private": true, "type": "commonjs", diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index b6de6ea..679927a 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "supermemory", "displayName": "Supermemory", - "version": "0.1.6", + "version": "0.1.7", "description": "Persistent memory across Claude Code sessions using Supermemory", "author": { "name": "Supermemory", From d595260024de56b17dc2c27f7605830de64de2f9 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:08:13 +0000 Subject: [PATCH 4/6] chore: trim mcp-proxy comments to one function-level line --- plugin/hooks/mcp-proxy.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/plugin/hooks/mcp-proxy.js b/plugin/hooks/mcp-proxy.js index 89e65b5..0511bc0 100644 --- a/plugin/hooks/mcp-proxy.js +++ b/plugin/hooks/mcp-proxy.js @@ -1,8 +1,4 @@ #!/usr/bin/env node -// Bridges Claude Code's stdio MCP transport to the hosted Supermemory MCP -// server, authenticating with the same credentials file the hooks use — one -// browser login covers both. Messages are forwarded sequentially to preserve -// JSON-RPC ordering; SSE responses are unwrapped back into stdout lines. const readline = require('node:readline'); const { getContainerTag } = require('./lib/container-tag'); const { getApiKey } = require('./lib/settings'); @@ -11,10 +7,6 @@ const MCP_URL = process.env.SUPERMEMORY_MCP_URL || 'https://mcp.supermemory.ai/mcp'; const REQUEST_TIMEOUT_MS = 30000; -// Hosted MCP treats a missing containerTag as the user's durable activeSpace, -// which is shared across every MCP client and is not this repo. Hooks already -// read/write the repo tag; inject it on space-scoped tools so MCP hits the -// same container. Leave an explicit containerTag and set-active-tag alone. const REPO_SCOPED_TOOLS = new Set([ 'search_memory', 'add_memory', @@ -27,6 +19,7 @@ const REPO_SCOPED_TOOLS = new Set([ let sessionId = null; +// Hosted MCP omits to activeSpace; default space-scoped calls to this repo instead. function injectRepoContainerTag(message, containerTag) { if (!containerTag || message.method !== 'tools/call') return; const params = message.params; From f55b6ae38b66c9657c72f1e4397429bc494d17e2 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:12:31 +0000 Subject: [PATCH 5/6] Revert "chore: bump plugin to 0.1.7" This reverts commit 490b27adce3ec736c907d2401b657c7e0b56cdfd. --- latest.json | 2 +- package.json | 2 +- plugin/.claude-plugin/plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/latest.json b/latest.json index c084fbe..839aa84 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "version": "0.1.7", + "version": "0.1.6", "updateCommand": "claude plugin update supermemory@supermemory-plugins" } diff --git a/package.json b/package.json index 55efcbf..8a88868 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-supermemory-dev", - "version": "0.1.7", + "version": "0.1.6", "description": "Claude code plugin by Supermemory AI", "private": true, "type": "commonjs", diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 679927a..b6de6ea 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "supermemory", "displayName": "Supermemory", - "version": "0.1.7", + "version": "0.1.6", "description": "Persistent memory across Claude Code sessions using Supermemory", "author": { "name": "Supermemory", From ae2bac0096b2087fb30e3cdec2e6185a1be32037 Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:12:49 +0000 Subject: [PATCH 6/6] ci: auto-bump plugin version on merge to main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same mechanism as codex-supermemory: patch-bump on main, skip the follow-up chore(release) commit. Syncs package.json, plugin.json, and latest.json. No npm publish — this is a Claude Code marketplace plugin. --- .github/workflows/release.yml | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1bc0cd1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Release + +on: + push: + branches: [main] + +permissions: + contents: write + +jobs: + bump: + if: ${{ !startsWith(github.event.head_commit.message, 'chore(release):') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: "20" + + - name: Bump patch version + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + npm version patch --no-git-tag-version + VERSION="$(node -p "require('./package.json').version")" + node -e ' + const fs = require("fs"); + const version = require("./package.json").version; + for (const file of ["plugin/.claude-plugin/plugin.json", "latest.json"]) { + const next = fs.readFileSync(file, "utf8").replace( + /"version":\s*"[^"]+"/, + `"version": "${version}"`, + ); + fs.writeFileSync(file, next); + } + ' + git add package.json package-lock.json plugin/.claude-plugin/plugin.json latest.json + git commit -m "chore(release): ${VERSION}" + + - name: Push version bump + run: git push origin HEAD:main