Skip to content

Commit 74b0f3d

Browse files
Dumbrisclaude
andcommitted
Support ANTHROPIC_API_KEY for Docker, fix auth hierarchy
Auth methods (priority order): 1. ANTHROPIC_API_KEY env var - works everywhere including Docker 2. System claude CLI - shares auth with Claude Code (local dev) 3. Mounted .credentials.json - OAuth tokens from K8s host Detect .claude.json without system CLI and give clear error message. Update mcpproxy registry entry with ANTHROPIC_API_KEY as optional env. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 690b079 commit 74b0f3d

3 files changed

Lines changed: 77 additions & 19 deletions

File tree

‎src/mcp_scanner/auth.py‎

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
"""Claude OAuth token handling for Agent SDK."""
1+
"""Claude authentication for Agent SDK.
2+
3+
Supports three auth methods (in priority order):
4+
1. ANTHROPIC_API_KEY env var - works everywhere (API billing)
5+
2. System `claude` CLI - shares auth with Claude Code (local dev)
6+
3. Mounted .credentials.json - OAuth tokens from K8s/Docker host
7+
"""
28

39
from __future__ import annotations
410

@@ -30,25 +36,61 @@ def _find_credentials(config_dir: str) -> str | None:
3036
return None
3137

3238

39+
def has_api_key() -> bool:
40+
"""Check if ANTHROPIC_API_KEY is set."""
41+
return bool(os.environ.get("ANTHROPIC_API_KEY"))
42+
43+
44+
def has_system_claude() -> bool:
45+
"""Check if system claude CLI is available."""
46+
return shutil.which("claude") is not None
47+
48+
3349
def ensure_writable_config() -> str:
3450
"""Ensure a writable config dir with Claude credentials exists.
3551
36-
Two deployment scenarios:
37-
1. **Local (dev)**: ~/.claude/ is writable and has .claude.json from Claude Code.
38-
System `claude` CLI is available and handles auth natively.
39-
2. **Docker (mcpproxy)**: ~/.claude/ is mounted read-only with .credentials.json.
40-
Bundled SDK CLI is used. We copy credentials to a writable tmpdir.
52+
Auth priority:
53+
1. ANTHROPIC_API_KEY env var - SDK uses it directly, no config dir needed
54+
2. System claude CLI - handles auth natively from ~/.claude
55+
3. Mounted .credentials.json - copy to writable tmpdir for bundled CLI
4156
4257
Returns the path to the (writable) config directory.
58+
Raises RuntimeError if no auth method is available.
4359
"""
60+
# If API key is set, the SDK handles auth directly
61+
if has_api_key():
62+
logger.info("Using ANTHROPIC_API_KEY for authentication")
63+
# Still need a writable config dir for SDK session files
64+
tmp_dir = tempfile.mkdtemp(prefix="mcp_scanner_claude_")
65+
return tmp_dir
66+
4467
src_dir = get_claude_config_dir()
4568
creds_path = _find_credentials(src_dir)
4669

70+
# System claude CLI handles its own auth (reads ~/.claude internally)
71+
if has_system_claude():
72+
if creds_path:
73+
logger.info("Found credentials at %s (system claude CLI will use natively)", creds_path)
74+
else:
75+
logger.info("System claude CLI found, will use its own auth")
76+
return src_dir
77+
78+
# Docker/bundled CLI: need actual .credentials.json with OAuth tokens
4779
if not creds_path:
4880
raise RuntimeError(
49-
f"No credentials found in {src_dir}. "
50-
f"Looked for: {', '.join(CREDENTIAL_FILES)}. "
51-
"Run 'claude login' to authenticate."
81+
"No authentication available. Options:\n"
82+
" 1. Set ANTHROPIC_API_KEY environment variable\n"
83+
" 2. Install claude CLI and run 'claude login'\n"
84+
" 3. Mount ~/.claude with .credentials.json (Docker/K8s)"
85+
)
86+
87+
# .claude.json from desktop app doesn't have raw tokens - bundled CLI can't use it
88+
if creds_path.endswith(".claude.json") and not has_system_claude():
89+
raise RuntimeError(
90+
f"Found {creds_path} but bundled CLI needs .credentials.json with OAuth tokens.\n"
91+
"Options:\n"
92+
" 1. Set ANTHROPIC_API_KEY environment variable\n"
93+
" 2. Mount .credentials.json from a host with 'claude login' (K8s pattern)"
5294
)
5395

5496
logger.info("Found credentials at %s", creds_path)
@@ -63,7 +105,7 @@ def ensure_writable_config() -> str:
63105
except OSError:
64106
pass
65107

66-
# Source is read-only (Docker mount) - copy to writable tmpdir
108+
# Read-only mount - copy to writable tmpdir
67109
tmp_dir = tempfile.mkdtemp(prefix="mcp_scanner_claude_")
68110

69111
for name in CREDENTIAL_FILES:

‎src/mcp_scanner/source_analyzer.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,25 @@
2929

3030

3131
def _sdk_kwargs(config_dir: str) -> dict:
32-
"""Build SDK kwargs - local uses system CLI, Docker uses bundled with env."""
32+
"""Build ClaudeAgentOptions kwargs based on available auth method.
33+
34+
Local: system claude CLI, no env override needed.
35+
Docker with API key: bundled CLI + ANTHROPIC_API_KEY in env.
36+
Docker with OAuth: bundled CLI + CLAUDE_CONFIG_DIR in env.
37+
"""
3338
import shutil
3439
kwargs: dict = {}
3540
system_claude = shutil.which("claude")
3641
if system_claude:
42+
# Local: system CLI handles its own auth
3743
kwargs["cli_path"] = system_claude
3844
else:
39-
kwargs["env"] = {"CLAUDE_CONFIG_DIR": config_dir}
45+
# Docker: bundled CLI needs env vars
46+
env = {"CLAUDE_CONFIG_DIR": config_dir}
47+
api_key = os.environ.get("ANTHROPIC_API_KEY")
48+
if api_key:
49+
env["ANTHROPIC_API_KEY"] = api_key
50+
kwargs["env"] = env
4051
return kwargs
4152

4253

‎src/mcp_scanner/tool_analyzer.py‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,27 @@
2121

2222

2323
def _sdk_kwargs(config_dir: str) -> dict:
24-
"""Build ClaudeAgentOptions kwargs for SDK invocation.
24+
"""Build ClaudeAgentOptions kwargs based on available auth method.
2525
26-
Locally: use system claude CLI (shares auth with Claude Code), don't override env.
27-
Docker: use bundled CLI, set CLAUDE_CONFIG_DIR via env.
26+
Local: system claude CLI, no env override needed.
27+
Docker with API key: bundled CLI + ANTHROPIC_API_KEY in env.
28+
Docker with OAuth: bundled CLI + CLAUDE_CONFIG_DIR in env.
2829
"""
2930
import shutil
3031
kwargs: dict = {}
3132
system_claude = shutil.which("claude")
3233
if system_claude:
33-
# Local: system CLI handles its own auth, don't pass env to avoid breaking it
3434
logger.info("Using system Claude CLI: %s", system_claude)
3535
kwargs["cli_path"] = system_claude
3636
else:
37-
# Docker: bundled CLI needs CLAUDE_CONFIG_DIR to find mounted credentials
38-
logger.info("Using SDK bundled CLI with CLAUDE_CONFIG_DIR=%s", config_dir)
39-
kwargs["env"] = {"CLAUDE_CONFIG_DIR": config_dir}
37+
env = {"CLAUDE_CONFIG_DIR": config_dir}
38+
api_key = os.environ.get("ANTHROPIC_API_KEY")
39+
if api_key:
40+
env["ANTHROPIC_API_KEY"] = api_key
41+
logger.info("Using SDK bundled CLI with ANTHROPIC_API_KEY")
42+
else:
43+
logger.info("Using SDK bundled CLI with CLAUDE_CONFIG_DIR=%s", config_dir)
44+
kwargs["env"] = env
4045
return kwargs
4146

4247
TOOL_ANALYSIS_PROMPT = """You are an MCP (Model Context Protocol) security analyst. Analyze the following MCP tool definitions for security threats.

0 commit comments

Comments
 (0)