fix(mem0-plugin): resolve API key from userConfig, fix #4876

Root cause: .mcp.json required ${MEM0_API_KEY} shell env var which
OAuth flow never populates — MCP server never started, only auth
stubs were exposed.

Fix: switch .mcp.json to ${user_config.MEM0_API_KEY} so Claude Code
reads the key from plugin userConfig (system keychain). Add
resolve_api_key() fallback chain (MEM0_API_KEY -> CLAUDE_PLUGIN_OPTION_MEM0_API_KEY)
to all hook scripts. Update onboard skill and setup_coding_categories.py
to handle keychain-only users. Add 3 tests for key resolution.
This commit is contained in:
kartik-mem0
2026-05-21 17:10:17 +05:30
parent 66600565ed
commit ff2f6c281a
11 changed files with 83 additions and 29 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "http",
"url": "https://mcp.mem0.ai/mcp/",
"headers": {
"Authorization": "Token ${MEM0_API_KEY}"
"Authorization": "Token ${user_config.MEM0_API_KEY}"
}
}
}
+13 -2
View File
@@ -1,6 +1,10 @@
"""Resolve mem0 user_id.
"""Resolve mem0 identity: API key and user_id.
Resolution priority:
API key resolution (first non-empty wins):
1. MEM0_API_KEY env var (explicit / shell profile)
2. CLAUDE_PLUGIN_OPTION_MEM0_API_KEY (set by Claude Code userConfig)
User ID resolution:
1. MEM0_USER_ID env var (explicit override)
2. $USER, else "default"
"""
@@ -10,6 +14,13 @@ from __future__ import annotations
import os
def resolve_api_key() -> str:
key = os.environ.get("MEM0_API_KEY", "").strip()
if key:
return key
return os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", "").strip()
def resolve_user_id() -> str:
explicit = os.environ.get("MEM0_USER_ID", "").strip()
if explicit:
+12 -2
View File
@@ -1,9 +1,19 @@
# Source this file. Sets MEM0_RESOLVED_USER_ID.
# Source this file. Sets MEM0_API_KEY and MEM0_RESOLVED_USER_ID.
#
# Resolution priority:
# API key resolution (first non-empty wins):
# 1. MEM0_API_KEY env var (explicit / shell profile)
# 2. CLAUDE_PLUGIN_OPTION_MEM0_API_KEY (set by Claude Code userConfig)
#
# User ID resolution:
# 1. MEM0_USER_ID env var (explicit override)
# 2. $USER, else "default"
# Resolve API key from userConfig fallback
if [ -z "${MEM0_API_KEY:-}" ] && [ -n "${CLAUDE_PLUGIN_OPTION_MEM0_API_KEY:-}" ]; then
MEM0_API_KEY="$CLAUDE_PLUGIN_OPTION_MEM0_API_KEY"
export MEM0_API_KEY
fi
_mem0_resolve_identity() {
if [ -n "${MEM0_USER_ID:-}" ]; then
printf '%s' "$MEM0_USER_ID"
+2 -2
View File
@@ -21,7 +21,7 @@ import urllib.error
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_user_id
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
log = logging.getLogger("mem0-auto-import")
@@ -123,7 +123,7 @@ def post_memory(api_key: str, content: str, user_id: str, filename: str, project
def main() -> None:
api_key = os.environ.get("MEM0_API_KEY", "")
api_key = resolve_api_key()
if not api_key:
log.debug("MEM0_API_KEY not set, skipping auto-import")
return
@@ -25,7 +25,7 @@ import urllib.request
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_user_id
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
log = logging.getLogger("mem0-compact-summary")
@@ -136,7 +136,7 @@ def store_summary(api_key: str, summary: str, user_id: str, session_id: str, pro
def main():
api_key = os.environ.get("MEM0_API_KEY", "")
api_key = resolve_api_key()
if not api_key:
log.debug("MEM0_API_KEY not set, skipping capture")
return
+2 -2
View File
@@ -23,7 +23,7 @@ import urllib.request
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_user_id
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
log = logging.getLogger("mem0-capture")
@@ -217,7 +217,7 @@ def main():
if arg.startswith("--source="):
source = arg.split("=", 1)[1]
api_key = os.environ.get("MEM0_API_KEY", "")
api_key = resolve_api_key()
if not api_key:
log.debug("MEM0_API_KEY not set, skipping capture")
return
+4 -4
View File
@@ -15,16 +15,16 @@ if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=_identity.sh
. "$SCRIPT_DIR/_identity.sh"
# Skip the bootstrap entirely if no API key is configured -- the agent
# would otherwise be told to call mem0 MCP tools that will all fail.
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=_identity.sh
. "$SCRIPT_DIR/_identity.sh"
# Initialize session stats tracker
python3 "$SCRIPT_DIR/session_stats.py" init 2>/dev/null || true
+4 -4
View File
@@ -25,14 +25,14 @@ if [ ${#PROMPT} -lt 20 ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=_identity.sh
. "$SCRIPT_DIR/_identity.sh"
# No API key means the agent can't search anyway
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=_identity.sh
. "$SCRIPT_DIR/_identity.sh"
USER_ID="$MEM0_RESOLVED_USER_ID"
cat <<EOF
@@ -13,7 +13,7 @@ Usage:
python setup_coding_categories.py # dry-run: show current vs proposed, no changes
python setup_coding_categories.py --apply # actually call project.update()
Requires the mem0ai Python SDK and MEM0_API_KEY to be set.
Requires the mem0ai Python SDK and MEM0_API_KEY (or CLAUDE_PLUGIN_OPTION_MEM0_API_KEY) to be set.
"""
from __future__ import annotations
@@ -23,6 +23,9 @@ import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_api_key
CODING_CATEGORIES = [
{
"architecture_decisions": (
@@ -87,9 +90,11 @@ def main() -> int:
)
args = ap.parse_args()
if not os.environ.get("MEM0_API_KEY"):
print("ERROR: MEM0_API_KEY is not set. Export it and try again.", file=sys.stderr)
api_key = resolve_api_key()
if not api_key:
print("ERROR: MEM0_API_KEY is not set. Export it or configure it via plugin userConfig.", file=sys.stderr)
return 1
os.environ["MEM0_API_KEY"] = api_key
try:
from mem0 import MemoryClient
+6 -6
View File
@@ -13,20 +13,20 @@ Run this wizard to set up the mem0 plugin for the current project. Complete in ~
## Step 1: Verify API key
Check if `MEM0_API_KEY` is set in the current environment:
Check if the API key is available. Claude Code may provide it via `userConfig` (stored in system keychain, exposed as `CLAUDE_PLUGIN_OPTION_MEM0_API_KEY`) or via shell environment (`MEM0_API_KEY`):
```bash
echo "${MEM0_API_KEY:+SET}" || echo "NOT_SET"
echo "${MEM0_API_KEY:-${CLAUDE_PLUGIN_OPTION_MEM0_API_KEY:-NOT_SET}}"
```
- If **NOT set**:
1. Ask the user: "No MEM0_API_KEY found. Do you have one, or need to create one?"
- If **NOT_SET** (neither variable is set):
1. Ask the user: "No API key found. Do you have one, or need to create one?"
2. If they need one, provide two options:
- **Browser**: Go to https://app.mem0.ai/dashboard/api-keys and copy the key
- **CLI**: Run `pip install mem0-cli && mem0 init --agent --json` to mint a key without email
3. Once they have the key, tell them to run: `export MEM0_API_KEY="m0-..."` in their terminal, then restart this Claude Code session (the env var must be set before Claude Code starts).
3. Once they have the key, tell them to run: `export MEM0_API_KEY="m0-..."` in their terminal, then restart this Claude Code session.
4. **STOP here.** Do not proceed until the key is confirmed set.
- If **SET**: Proceed to Step 2.
- If **SET** (either variable has a value): Proceed to Step 2.
## Step 2: Show identity
+29 -1
View File
@@ -1,9 +1,10 @@
"""Tests for write-path app_id migration.
"""Tests for write-path app_id migration and API key resolution.
Verifies that all scripts writing to the Mem0 API:
1. Pass app_id as a top-level parameter (not in metadata)
2. Do NOT include project_id in metadata
3. Include branch in metadata when available
4. Use resolve_api_key() for key resolution with userConfig fallback
"""
from __future__ import annotations
@@ -164,3 +165,30 @@ def test_no_metadata_project_id_anywhere():
metadata = body.get("metadata", {})
assert "project_id" not in metadata, f"Write function #{i} still has metadata.project_id"
assert body.get("app_id") == "proj", f"Write function #{i} missing app_id top-level"
def test_resolve_api_key_prefers_env_var(monkeypatch):
"""resolve_api_key returns MEM0_API_KEY when both are set."""
from _identity import resolve_api_key
monkeypatch.setenv("MEM0_API_KEY", "direct-key")
monkeypatch.setenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", "fallback-key")
assert resolve_api_key() == "direct-key"
def test_resolve_api_key_falls_back_to_plugin_option(monkeypatch):
"""resolve_api_key falls back to CLAUDE_PLUGIN_OPTION_MEM0_API_KEY."""
from _identity import resolve_api_key
monkeypatch.delenv("MEM0_API_KEY", raising=False)
monkeypatch.setenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", "fallback-key")
assert resolve_api_key() == "fallback-key"
def test_resolve_api_key_returns_empty_when_neither_set(monkeypatch):
"""resolve_api_key returns empty string when no key is available."""
from _identity import resolve_api_key
monkeypatch.delenv("MEM0_API_KEY", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False)
assert resolve_api_key() == ""