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
+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