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:
@@ -4,7 +4,7 @@
|
|||||||
"type": "http",
|
"type": "http",
|
||||||
"url": "https://mcp.mem0.ai/mcp/",
|
"url": "https://mcp.mem0.ai/mcp/",
|
||||||
"headers": {
|
"headers": {
|
||||||
"Authorization": "Token ${MEM0_API_KEY}"
|
"Authorization": "Token ${user_config.MEM0_API_KEY}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
1. MEM0_USER_ID env var (explicit override)
|
||||||
2. $USER, else "default"
|
2. $USER, else "default"
|
||||||
"""
|
"""
|
||||||
@@ -10,6 +14,13 @@ from __future__ import annotations
|
|||||||
import os
|
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:
|
def resolve_user_id() -> str:
|
||||||
explicit = os.environ.get("MEM0_USER_ID", "").strip()
|
explicit = os.environ.get("MEM0_USER_ID", "").strip()
|
||||||
if explicit:
|
if explicit:
|
||||||
|
|||||||
@@ -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)
|
# 1. MEM0_USER_ID env var (explicit override)
|
||||||
# 2. $USER, else "default"
|
# 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() {
|
_mem0_resolve_identity() {
|
||||||
if [ -n "${MEM0_USER_ID:-}" ]; then
|
if [ -n "${MEM0_USER_ID:-}" ]; then
|
||||||
printf '%s' "$MEM0_USER_ID"
|
printf '%s' "$MEM0_USER_ID"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import urllib.error
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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
|
from _project import resolve_branch, resolve_project_id
|
||||||
|
|
||||||
log = logging.getLogger("mem0-auto-import")
|
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:
|
def main() -> None:
|
||||||
api_key = os.environ.get("MEM0_API_KEY", "")
|
api_key = resolve_api_key()
|
||||||
if not api_key:
|
if not api_key:
|
||||||
log.debug("MEM0_API_KEY not set, skipping auto-import")
|
log.debug("MEM0_API_KEY not set, skipping auto-import")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import urllib.request
|
|||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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
|
from _project import resolve_branch, resolve_project_id
|
||||||
|
|
||||||
log = logging.getLogger("mem0-compact-summary")
|
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():
|
def main():
|
||||||
api_key = os.environ.get("MEM0_API_KEY", "")
|
api_key = resolve_api_key()
|
||||||
if not api_key:
|
if not api_key:
|
||||||
log.debug("MEM0_API_KEY not set, skipping capture")
|
log.debug("MEM0_API_KEY not set, skipping capture")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import urllib.request
|
|||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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
|
from _project import resolve_branch, resolve_project_id
|
||||||
|
|
||||||
log = logging.getLogger("mem0-capture")
|
log = logging.getLogger("mem0-capture")
|
||||||
@@ -217,7 +217,7 @@ def main():
|
|||||||
if arg.startswith("--source="):
|
if arg.startswith("--source="):
|
||||||
source = arg.split("=", 1)[1]
|
source = arg.split("=", 1)[1]
|
||||||
|
|
||||||
api_key = os.environ.get("MEM0_API_KEY", "")
|
api_key = resolve_api_key()
|
||||||
if not api_key:
|
if not api_key:
|
||||||
log.debug("MEM0_API_KEY not set, skipping capture")
|
log.debug("MEM0_API_KEY not set, skipping capture")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ if [ -n "${MEM0_DEBUG:-}" ]; then
|
|||||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||||
fi
|
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
|
# 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.
|
# would otherwise be told to call mem0 MCP tools that will all fail.
|
||||||
if [ -z "${MEM0_API_KEY:-}" ]; then
|
if [ -z "${MEM0_API_KEY:-}" ]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
# shellcheck source=_identity.sh
|
|
||||||
. "$SCRIPT_DIR/_identity.sh"
|
|
||||||
|
|
||||||
# Initialize session stats tracker
|
# Initialize session stats tracker
|
||||||
python3 "$SCRIPT_DIR/session_stats.py" init 2>/dev/null || true
|
python3 "$SCRIPT_DIR/session_stats.py" init 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ if [ ${#PROMPT} -lt 20 ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
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
|
# No API key means the agent can't search anyway
|
||||||
if [ -z "${MEM0_API_KEY:-}" ]; then
|
if [ -z "${MEM0_API_KEY:-}" ]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
# shellcheck source=_identity.sh
|
|
||||||
. "$SCRIPT_DIR/_identity.sh"
|
|
||||||
USER_ID="$MEM0_RESOLVED_USER_ID"
|
USER_ID="$MEM0_RESOLVED_USER_ID"
|
||||||
|
|
||||||
cat <<EOF
|
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 # dry-run: show current vs proposed, no changes
|
||||||
python setup_coding_categories.py --apply # actually call project.update()
|
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
|
from __future__ import annotations
|
||||||
@@ -23,6 +23,9 @@ import json
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from _identity import resolve_api_key
|
||||||
|
|
||||||
CODING_CATEGORIES = [
|
CODING_CATEGORIES = [
|
||||||
{
|
{
|
||||||
"architecture_decisions": (
|
"architecture_decisions": (
|
||||||
@@ -87,9 +90,11 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
if not os.environ.get("MEM0_API_KEY"):
|
api_key = resolve_api_key()
|
||||||
print("ERROR: MEM0_API_KEY is not set. Export it and try again.", file=sys.stderr)
|
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
|
return 1
|
||||||
|
os.environ["MEM0_API_KEY"] = api_key
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from mem0 import MemoryClient
|
from mem0 import MemoryClient
|
||||||
|
|||||||
@@ -13,20 +13,20 @@ Run this wizard to set up the mem0 plugin for the current project. Complete in ~
|
|||||||
|
|
||||||
## Step 1: Verify API key
|
## 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
|
```bash
|
||||||
echo "${MEM0_API_KEY:+SET}" || echo "NOT_SET"
|
echo "${MEM0_API_KEY:-${CLAUDE_PLUGIN_OPTION_MEM0_API_KEY:-NOT_SET}}"
|
||||||
```
|
```
|
||||||
|
|
||||||
- If **NOT set**:
|
- If **NOT_SET** (neither variable is set):
|
||||||
1. Ask the user: "No MEM0_API_KEY found. Do you have one, or need to create one?"
|
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:
|
2. If they need one, provide two options:
|
||||||
- **Browser**: Go to https://app.mem0.ai/dashboard/api-keys and copy the key
|
- **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
|
- **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.
|
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
|
## Step 2: Show identity
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
Verifies that all scripts writing to the Mem0 API:
|
||||||
1. Pass app_id as a top-level parameter (not in metadata)
|
1. Pass app_id as a top-level parameter (not in metadata)
|
||||||
2. Do NOT include project_id in metadata
|
2. Do NOT include project_id in metadata
|
||||||
3. Include branch in metadata when available
|
3. Include branch in metadata when available
|
||||||
|
4. Use resolve_api_key() for key resolution with userConfig fallback
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -164,3 +165,30 @@ def test_no_metadata_project_id_anywhere():
|
|||||||
metadata = body.get("metadata", {})
|
metadata = body.get("metadata", {})
|
||||||
assert "project_id" not in metadata, f"Write function #{i} still has metadata.project_id"
|
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"
|
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() == ""
|
||||||
|
|||||||
Reference in New Issue
Block a user