From ff2f6c281a2c685db81000b1cbcf964cc36b27ba Mon Sep 17 00:00:00 2001 From: kartik-mem0 Date: Thu, 21 May 2026 17:10:17 +0530 Subject: [PATCH] fix(mem0-plugin): resolve API key from userConfig, fix #4876 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- mem0-plugin/.mcp.json | 2 +- mem0-plugin/scripts/_identity.py | 15 ++++++++-- mem0-plugin/scripts/_identity.sh | 14 +++++++-- mem0-plugin/scripts/auto_import.py | 4 +-- .../scripts/capture_compact_summary.py | 4 +-- mem0-plugin/scripts/on_pre_compact.py | 4 +-- mem0-plugin/scripts/on_session_start.sh | 8 ++--- mem0-plugin/scripts/on_user_prompt.sh | 8 ++--- .../scripts/setup_coding_categories.py | 11 +++++-- mem0-plugin/skills/mem0-onboard/SKILL.md | 12 ++++---- mem0-plugin/tests/test_write_path.py | 30 ++++++++++++++++++- 11 files changed, 83 insertions(+), 29 deletions(-) diff --git a/mem0-plugin/.mcp.json b/mem0-plugin/.mcp.json index dd091bc0c..8fe95bf1f 100644 --- a/mem0-plugin/.mcp.json +++ b/mem0-plugin/.mcp.json @@ -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}" } } } diff --git a/mem0-plugin/scripts/_identity.py b/mem0-plugin/scripts/_identity.py index e2e88fe42..5a0f971ec 100644 --- a/mem0-plugin/scripts/_identity.py +++ b/mem0-plugin/scripts/_identity.py @@ -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: diff --git a/mem0-plugin/scripts/_identity.sh b/mem0-plugin/scripts/_identity.sh index e032d2bec..ac69caa52 100644 --- a/mem0-plugin/scripts/_identity.sh +++ b/mem0-plugin/scripts/_identity.sh @@ -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" diff --git a/mem0-plugin/scripts/auto_import.py b/mem0-plugin/scripts/auto_import.py index 3c3cecb6f..ae74ca1bb 100644 --- a/mem0-plugin/scripts/auto_import.py +++ b/mem0-plugin/scripts/auto_import.py @@ -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 diff --git a/mem0-plugin/scripts/capture_compact_summary.py b/mem0-plugin/scripts/capture_compact_summary.py index 9cbb825da..f9218eec9 100644 --- a/mem0-plugin/scripts/capture_compact_summary.py +++ b/mem0-plugin/scripts/capture_compact_summary.py @@ -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 diff --git a/mem0-plugin/scripts/on_pre_compact.py b/mem0-plugin/scripts/on_pre_compact.py index 6b614e210..3160ef3ed 100755 --- a/mem0-plugin/scripts/on_pre_compact.py +++ b/mem0-plugin/scripts/on_pre_compact.py @@ -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 diff --git a/mem0-plugin/scripts/on_session_start.sh b/mem0-plugin/scripts/on_session_start.sh index e51edf14f..74a5953d2 100755 --- a/mem0-plugin/scripts/on_session_start.sh +++ b/mem0-plugin/scripts/on_session_start.sh @@ -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 diff --git a/mem0-plugin/scripts/on_user_prompt.sh b/mem0-plugin/scripts/on_user_prompt.sh index ddf38011e..f34d48994 100755 --- a/mem0-plugin/scripts/on_user_prompt.sh +++ b/mem0-plugin/scripts/on_user_prompt.sh @@ -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 < 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 diff --git a/mem0-plugin/skills/mem0-onboard/SKILL.md b/mem0-plugin/skills/mem0-onboard/SKILL.md index 7ea69fb17..e00b51f38 100644 --- a/mem0-plugin/skills/mem0-onboard/SKILL.md +++ b/mem0-plugin/skills/mem0-onboard/SKILL.md @@ -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 diff --git a/mem0-plugin/tests/test_write_path.py b/mem0-plugin/tests/test_write_path.py index 42404b24b..fa5d3d2d7 100644 --- a/mem0-plugin/tests/test_write_path.py +++ b/mem0-plugin/tests/test_write_path.py @@ -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() == ""