feat: add cross-project user preference memory MCP - #319
Conversation
Co-authored-by: Dung Huynh Duc <dunghd.it@gmail.com>
🦋 Changeset detectedLatest commit: def18fa The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
WalkthroughAdds the ChangesUser memory MCP server
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant MCPClient
participant MCPServer
participant PreferenceStore
participant Filesystem
MCPClient->>MCPServer: Call memory_preference_set
MCPServer->>PreferenceStore: Validate confirmed preference
PreferenceStore->>Filesystem: Lock and atomically persist preference
Filesystem-->>PreferenceStore: Preference and audit result
PreferenceStore-->>MCPServer: Return preference
MCPServer-->>MCPClient: JSON MCP response
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a new MCP server, @jellydn/user-memory-mcp, designed to manage explicit, durable user preferences across projects and coding agents. The changes include adding the package, updating configuration files for various AI tools to integrate the server, and documenting its usage. The review feedback provides valuable robustness and performance improvements, such as handling potential resolution errors in the entry point, optimizing preference lookups with findIndex, cleaning up temporary files on write failures, and removing redundant chmod calls.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) { | ||
| main().catch((error: unknown) => { | ||
| console.error("user-memory MCP server failed:", error); | ||
| process.exitCode = 1; | ||
| }); | ||
| } |
There was a problem hiding this comment.
If process.argv[1] is not a valid file path (which can happen in certain test runners, bundlers, or custom execution environments), realpathSync will throw an ENOENT error and crash the process immediately upon importing this module. Wrapping this check in a try-catch block makes the module safe to import in any environment.
| if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) { | |
| main().catch((error: unknown) => { | |
| console.error("user-memory MCP server failed:", error); | |
| process.exitCode = 1; | |
| }); | |
| } | |
| try { | |
| if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) { | |
| main().catch((error: unknown) => { | |
| console.error("user-memory MCP server failed:", error); | |
| process.exitCode = 1; | |
| }); | |
| } | |
| } catch { | |
| // Ignore errors from realpathSync if process.argv[1] cannot be resolved | |
| } |
| @@ -0,0 +1,198 @@ | |||
| import { randomUUID } from "node:crypto"; | |||
| import { appendFile, chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises"; | |||
There was a problem hiding this comment.
| const existing = memory.preferences.find((preference) => preference.key === key); | ||
| const preference: UserPreference = existing | ||
| ? { ...existing, value, updatedAt: timestamp } | ||
| : { key, value, source: "explicit", createdAt: timestamp, updatedAt: timestamp }; | ||
|
|
||
| if (existing) { | ||
| memory.preferences[memory.preferences.indexOf(existing)] = preference; | ||
| } else { | ||
| memory.preferences.push(preference); | ||
| } |
There was a problem hiding this comment.
Optimize the array search by using findIndex instead of calling both find and indexOf, which performs two full scans of the array in the worst case.
| const existing = memory.preferences.find((preference) => preference.key === key); | |
| const preference: UserPreference = existing | |
| ? { ...existing, value, updatedAt: timestamp } | |
| : { key, value, source: "explicit", createdAt: timestamp, updatedAt: timestamp }; | |
| if (existing) { | |
| memory.preferences[memory.preferences.indexOf(existing)] = preference; | |
| } else { | |
| memory.preferences.push(preference); | |
| } | |
| const existingIndex = memory.preferences.findIndex((preference) => preference.key === key); | |
| const existing = existingIndex !== -1 ? memory.preferences[existingIndex] : undefined; | |
| const preference: UserPreference = existing | |
| ? { ...existing, value, updatedAt: timestamp } | |
| : { key, value, source: "explicit", createdAt: timestamp, updatedAt: timestamp }; | |
| if (existingIndex !== -1) { | |
| memory.preferences[existingIndex] = preference; | |
| } else { | |
| memory.preferences.push(preference); | |
| } |
| private async writeMemory(memory: UserMemory): Promise<void> { | ||
| await this.ensureDirectory(); | ||
| const temporaryPath = `${this.memoryPath}.${process.pid}.${randomUUID()}.tmp`; | ||
| await writeFile(temporaryPath, `${JSON.stringify(memory, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); | ||
| await rename(temporaryPath, this.memoryPath); | ||
| await chmod(this.memoryPath, 0o600); | ||
| } |
There was a problem hiding this comment.
Ensure that the temporary file is cleaned up if writeFile or rename fails, preventing orphaned .tmp files from cluttering the user's directory. Additionally, calling chmod after rename is redundant because rename preserves the permissions of the source file, which was already created with 0o600 mode.
private async writeMemory(memory: UserMemory): Promise<void> {
await this.ensureDirectory();
const temporaryPath = `${this.memoryPath}.${process.pid}.${randomUUID()}.tmp`;
try {
await writeFile(temporaryPath, `${JSON.stringify(memory, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
await rename(temporaryPath, this.memoryPath);
} catch (error) {
try {
await unlink(temporaryPath);
} catch {
// Ignore cleanup errors to propagate the original error
}
throw error;
}
}| private async appendAuditBestEffort(entry: AuditEntry): Promise<void> { | ||
| try { | ||
| await this.ensureDirectory(); | ||
| await appendFile(this.auditPath, `${JSON.stringify(entry)}\n`, { encoding: "utf8", mode: 0o600 }); | ||
| await chmod(this.auditPath, 0o600); | ||
| } catch (error) { | ||
| console.error("user-memory: preference was saved, but its audit entry could not be written", error); | ||
| } | ||
| } |
There was a problem hiding this comment.
Calling chmod on every audit log append is redundant. The appendFile function's mode: 0o600 option already ensures that the file is created with the correct permissions if it does not exist. If the file already exists, its permissions are already set, making the extra chmod system call unnecessary and inefficient.
private async appendAuditBestEffort(entry: AuditEntry): Promise<void> {
try {
await this.ensureDirectory();
await appendFile(this.auditPath, `${JSON.stringify(entry)}\n`, { encoding: "utf8", mode: 0o600 });
} catch (error) {
console.error("user-memory: preference was saved, but its audit entry could not be written", error);
}
}There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@MEMORY.md`:
- Line 131: Update the sentence in MEMORY.md so “that is two separate stores”
uses the grammatically correct plural phrasing “those are two separate stores,”
while preserving the surrounding guidance.
In `@packages/user-memory-mcp/src/store.ts`:
- Around line 96-99: Update UserPreferenceStore.list() to sort keys using direct
string comparison instead of localeCompare(), ensuring deterministic code-point
ordering for mixed-case or punctuation-bearing keys. Add a focused test covering
one such key ordering case.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b17f1a92-0232-468a-802b-09482641cab6
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (36)
.changeset/user-memory-mcp.mdDockerfileMEMORY.mdREADME.mdconfigs/amp/settings.jsonconfigs/antigravity-cli/plugins/my-ai-tools-gemini-migration/mcp_config.jsonconfigs/claude/mcp-servers.jsonconfigs/cline/mcp-settings.jsonconfigs/codex/config.tomlconfigs/commandcode/mcp.jsonconfigs/copilot/mcp-config.jsonconfigs/cursor/mcp.jsonconfigs/devin/config.jsonconfigs/factory/mcp.jsonconfigs/gemini/settings.jsonconfigs/grok/config.tomlconfigs/kilo/config.jsonconfigs/kimi-code/mcp.jsonconfigs/kiro/mcp.jsonconfigs/kiro/settings.jsonconfigs/mcp-registry.jsonconfigs/mimo/mimocode.jsoncconfigs/opencode/opencode.jsonconfigs/pi/mcp.jsonconfigs/qodercli/settings.jsonpackage.jsonpackages/user-memory-mcp/README.mdpackages/user-memory-mcp/package.jsonpackages/user-memory-mcp/src/schema.tspackages/user-memory-mcp/src/server.tspackages/user-memory-mcp/src/store.tspackages/user-memory-mcp/src/tools.tspackages/user-memory-mcp/tests/mcp.test.tspackages/user-memory-mcp/tests/store.test.tspackages/user-memory-mcp/tsconfig.jsontests/pr_user_memory_mcp.bats
| | `agentmemory` | Today, same session/project | `mcp__agentmemory__memory_save` | | ||
| | `/handoffs` | Continue a task in a future session | `/handoffs` slash command → resume with `/pickup` | | ||
|
|
||
| If you find yourself wanting both "remember this for me next time" and "let me continue this tomorrow" — that is two separate stores; pick the lane that matches the actual horizon. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix grammar.
The phrase "that is two separate stores" contains a grammatical error. Consider using "those are two separate stores".
📝 Proposed fix
-If you find yourself wanting both "remember this for me next time" and "let me continue this tomorrow" — that is two separate stores; pick the lane that matches the actual horizon.
+If you find yourself wanting both "remember this for me next time" and "let me continue this tomorrow" — those are two separate stores; pick the lane that matches the actual horizon.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| If you find yourself wanting both "remember this for me next time" and "let me continue this tomorrow" — that is two separate stores; pick the lane that matches the actual horizon. | |
| If you find yourself wanting both "remember this for me next time" and "let me continue this tomorrow" — those are two separate stores; pick the lane that matches the actual horizon. |
🧰 Tools
🪛 LanguageTool
[grammar] ~131-~131: Ensure spelling is correct
Context: ...e this tomorrow" — that is two separate stores; pick the lane that matches the actual h...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@MEMORY.md` at line 131, Update the sentence in MEMORY.md so “that is two
separate stores” uses the grammatically correct plural phrasing “those are two
separate stores,” while preserving the surrounding guidance.
Source: Linters/SAST tools
| async list(): Promise<UserPreference[]> { | ||
| const memory = await this.readMemory(); | ||
| return [...memory.preferences].sort((left, right) => left.key.localeCompare(right.key)); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -C3 '\.localeCompare\s*\(' packages/user-memory-mcp
rg -n -C3 'deterministic order' packages/user-memory-mcp/testsRepository: jellydn/my-ai-tools
Length of output: 1289
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== store.ts =="
sed -n '1,180p' packages/user-memory-mcp/src/store.ts
echo
echo "== store.test.ts =="
sed -n '1,220p' packages/user-memory-mcp/tests/store.test.tsRepository: jellydn/my-ai-tools
Length of output: 10521
🏁 Script executed:
#!/bin/bash
set -euo pipefail
node - <<'JS'
const pairs = [
["a", "A"],
["a", "b"],
["a", "_"],
["A", "a"],
["testRunner", "packageManager"],
["a-b", "a_b"],
];
for (const locale of [undefined, "en", "sv", "de", "tr", "und"]) {
const label = locale ?? "(default)";
console.log(`\nLocale: ${label}`);
for (const [x, y] of pairs) {
const r = locale === undefined ? x.localeCompare(y) : x.localeCompare(y, locale);
console.log(`${JSON.stringify(x)} vs ${JSON.stringify(y)} => ${r}`);
}
}
JSRepository: jellydn/my-ai-tools
Length of output: 981
Use code-point ordering for list()
localeCompare() can produce locale-dependent ordering for mixed-case or punctuation-bearing keys. Switch to direct string comparison and add a test that covers one of those cases.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/user-memory-mcp/src/store.ts` around lines 96 - 99, Update
UserPreferenceStore.list() to sort keys using direct string comparison instead
of localeCompare(), ensuring deterministic code-point ordering for mixed-case or
punctuation-bearing keys. Add a focused test covering one such key ordering
case.
Model preferences as a keyed map instead of an array so get/set/delete are O(1) and the key is not duplicated. Drop the constant source field and the store-level explicit flag; MCP confirmed:true owns consent.
The package is not on npm yet, so configs and the registry now invoke the linked bin instead of npx @jellydn/user-memory-mcp@latest. cli.sh auto-links from the monorepo when the prerequisite is missing.
Run build/test for packages/* in CI so user-memory-mcp is not config-only coverage. Dockerfile copies packages/ generically instead of hardcoding one package path.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
packages/user-memory-mcp/src/store.ts (1)
102-107: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse code-point ordering for
list().
localeCompare()can produce locale-dependent ordering for mixed-case or punctuation-bearing keys. Switch to direct string comparison to ensure deterministic code-point ordering.🐛 Proposed fix
- .sort((left, right) => left.key.localeCompare(right.key)); + .sort((left, right) => (left.key < right.key ? -1 : left.key > right.key ? 1 : 0));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/user-memory-mcp/src/store.ts` around lines 102 - 107, Update the sorting comparator in UserPreferenceStore.list() to use direct string comparison between left.key and right.key instead of localeCompare(), ensuring deterministic code-point ordering for mixed-case and punctuation-bearing keys.
🧹 Nitpick comments (2)
configs/antigravity-cli/plugins/my-ai-tools-gemini-migration/mcp_config.json (1)
45-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFormat both added JSON blocks with Biome’s tab indentation.
The repository guideline requires tabs for JSON files; both changed blocks use spaces.
configs/antigravity-cli/plugins/my-ai-tools-gemini-migration/mcp_config.json#L45-L46: reformat the addedcommandandargslines with tabs.configs/commandcode/mcp.json#L65-L66: reformat the addedcommandandargslines with tabs.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@configs/antigravity-cli/plugins/my-ai-tools-gemini-migration/mcp_config.json` around lines 45 - 46, Reformat the added command and args entries using Biome’s tab indentation in configs/antigravity-cli/plugins/my-ai-tools-gemini-migration/mcp_config.json lines 45-46 and configs/commandcode/mcp.json lines 65-66; no content changes are needed.Source: Coding guidelines
.github/workflows/test.yml (1)
30-31: 🩺 Stability & Availability | 🔵 TrivialDisable persisted checkout credentials.
This checkout step only needs read access, so setpersist-credentials: falseto avoid leaving the token in the runner’s git config.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/test.yml around lines 30 - 31, Add persist-credentials: false to the actions/checkout@v7 step identified by the Checkout name, while preserving its existing read-only checkout behavior.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@packages/user-memory-mcp/src/store.ts`:
- Around line 102-107: Update the sorting comparator in
UserPreferenceStore.list() to use direct string comparison between left.key and
right.key instead of localeCompare(), ensuring deterministic code-point ordering
for mixed-case and punctuation-bearing keys.
---
Nitpick comments:
In @.github/workflows/test.yml:
- Around line 30-31: Add persist-credentials: false to the actions/checkout@v7
step identified by the Checkout name, while preserving its existing read-only
checkout behavior.
In
`@configs/antigravity-cli/plugins/my-ai-tools-gemini-migration/mcp_config.json`:
- Around line 45-46: Reformat the added command and args entries using Biome’s
tab indentation in
configs/antigravity-cli/plugins/my-ai-tools-gemini-migration/mcp_config.json
lines 45-46 and configs/commandcode/mcp.json lines 65-66; no content changes are
needed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 08b625ee-f156-42c8-89e6-ba09cdb2ef69
📒 Files selected for processing (35)
.github/workflows/test.ymlDockerfileREADME.mdcli.shconfigs/amp/settings.jsonconfigs/antigravity-cli/plugins/my-ai-tools-gemini-migration/mcp_config.jsonconfigs/claude/mcp-servers.jsonconfigs/cline/mcp-settings.jsonconfigs/codex/config.tomlconfigs/commandcode/mcp.jsonconfigs/copilot/mcp-config.jsonconfigs/cursor/mcp.jsonconfigs/devin/config.jsonconfigs/factory/mcp.jsonconfigs/gemini/settings.jsonconfigs/grok/config.tomlconfigs/kilo/config.jsonconfigs/kimi-code/mcp.jsonconfigs/kiro/mcp.jsonconfigs/kiro/settings.jsonconfigs/mcp-registry.jsonconfigs/mimo/mimocode.jsoncconfigs/opencode/opencode.jsonconfigs/pi/mcp.jsonconfigs/qodercli/settings.jsonlib/install.shpackage.jsonpackages/user-memory-mcp/README.mdpackages/user-memory-mcp/package.jsonpackages/user-memory-mcp/src/schema.tspackages/user-memory-mcp/src/store.tspackages/user-memory-mcp/src/tools.tspackages/user-memory-mcp/tests/mcp.test.tspackages/user-memory-mcp/tests/store.test.tstests/pr_user_memory_mcp.bats
🚧 Files skipped from review as they are similar to previous changes (9)
- configs/pi/mcp.json
- configs/kilo/config.json
- configs/mimo/mimocode.jsonc
- packages/user-memory-mcp/package.json
- packages/user-memory-mcp/README.md
- packages/user-memory-mcp/src/tools.ts
- configs/copilot/mcp-config.json
- tests/pr_user_memory_mcp.bats
- README.md
What
Add
@jellydn/user-memory-mcp, a structured MCP server for durable user preferences shared across projects and coding agents.The repository now has four distinct memory lanes:
agentmemoryqmd/handoffsand/pickupuser-memoryThe new server exposes five tools:
memory_preference_setmemory_preference_getmemory_preference_listmemory_preference_deletememory_preference_resetPreferences are stored under
~/.ai-tools/user-memory/in structured JSON with a value-free audit log. The server is registered alongsideagentmemoryacross all supported MCP client configurations.Why
The repository already supports session memory, durable project knowledge, and task handoffs. Building another generic memory service would duplicate those capabilities.
The missing lane is user-level preferences that should follow someone between projects and tools, such as:
Project-scoped
qmdcollections are not the right place for these preferences, whileagentmemoryis intentionally limited to short-lived session discoveries. A separate preference store gives users deterministic lookup, exact inspection, deletion/reset controls, and a clear rule that agents must not infer preferences.How
packages/user-memory-mcpusing the stable MCP TypeScript SDK v1.source: "explicit"and creation/update timestamps.confirmed: true; MCP schema validation rejects unconfirmed writes.user-memoryin the central registry and every existing agent configuration that includesagentmemory.MEMORY.md, README guidance, npm workspace metadata, Docker's dependency layer, and release changesets.Validation
jsonschemaskip)npm cisimulationgit diff --checkRelease requirement
The checked-in configurations invoke
npx -y @jellydn/user-memory-mcp@latest. Publish the package before marking this PR ready to merge so external clients can start the configured server.Summary by CodeRabbit
user-memoryMCP server for durable cross-project user preferences with set/get/list/delete/reset tools.user-memoryserver across supported MCP integrations and config presets.MEMORY.mdwith the newuser-memorylane, decision guidance, and tool descriptions.