feat(crm-agent): mcp_auth_proxy.py — let Claude Code call the auth-protected MCP#71
Merged
Conversation
… our MCP Claude Code's HTTP MCP support has two relevant limitations: - `claude mcp add --header "Authorization: Bearer X"` registers a STATIC token — expires in ~1h, no refresh - 401 responses are not retried (per docs) - OAuth 2.0 support requires server-side metadata; our MCP server doesn't publish that So a long-lived `claude mcp add` registration with a static token would work for an hour, then 401 silently and stay broken until the user manually re-registers. Painful and easy to miss. Solution: thin local proxy. Claude Code talks to the proxy on localhost (no auth needed for that hop); proxy fetches a fresh az JWT per request window (45-min eager refresh) and injects `Authorization: Bearer <jwt>` on every forwarded request. Token management lives in one place; Claude Code's `.mcp.json` config stays simple. Surface (~140 lines, single file): python scripts/mcp_auth_proxy.py # → MCP_SERVER_URL python scripts/mcp_auth_proxy.py --remote # → MCP_SERVER_URL_REMOTE python scripts/mcp_auth_proxy.py --port 9001 ... → Listen: http://127.0.0.1:8001/ → Wire Claude Code: claude mcp add --transport http crm http://127.0.0.1:8001/mcp/ Implementation notes: - Reuses one httpx.AsyncClient for the proxy lifetime so MCP Streamable HTTP keep-alive sessions persist across requests. - Pass-through everything except (host, content-length, our own auth). - Streaming response via Starlette StreamingResponse + httpx stream=True — MCP returns SSE for some flows; can't buffer to memory. - 502 if upstream unreachable; explicit error rather than silent fail. - Eager `_acquire_user_jwt()` at startup so missing `az login` fails before Claude Code tries to connect. Smoke (with both `run_mcp_server_local.py` on :8000 and proxy on :8001): - POST /mcp/ initialize → 200, serverInfo.name=crm-mcp - POST /mcp/ notifications/initialized → 202 Accepted - POST /mcp/ tools/list → 7 expected tools (list_opportunities, get_opportunity, create_opportunity, update_opportunity, delete_opportunity, search_accounts, search_contacts) Doc: - llm-foundry-zh.md "本地 REPL" gains a new H3 (§4) "让 Claude Code (或其他外部 MCP 客户端)调我们的工具" explaining the gap, the workaround, and the wiring command. - §5 (切换模型) numbering shifted from §4. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 10, 2026
carvychen
added a commit
that referenced
this pull request
May 10, 2026
…#72) Without this, Claude Code can't discover the CRM skill — its skill scanner walks `.claude/skills/<name>/SKILL.md`, not `integrations/.../skills/`. After PR #71 (mcp_auth_proxy.py), Claude Code can call our auth-protected MCP, but it had no SOP context to know when to do so. This closes that gap. Symlink (mode 120000) — single source of truth: edits to the host-portable bundle at `integrations/crm-agent/skills/crm-opportunity/` are immediately visible to Claude Code. No copy / no drift. Git stores symlinks cross-platform on Unix; Windows users (rare for this repo) may need `core.symlinks=true`. Verified: starting Claude Code in this repo lists `crm-opportunity` alongside the existing 5 SOP skills (domain-model, github-triage, tdd, to-issues, to-prd). Description matches the bundle's frontmatter. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
carvychen
added a commit
that referenced
this pull request
May 10, 2026
* docs(crm-agent): SKILL.md compatibility — distinguish verified vs designed-for The "Tested against" list claimed 4 hosts (Claude Desktop, VS Code Copilot MCP, in-repo agent, Copilot Studio). In reality only the in-repo agent has been verified end-to-end. The other 3 are aspirational MCP-spec compliance. Today we also empirically verified Claude Code works — but only via the auth-injecting proxy added in PR #71, not via `.mcp.json` directly, because Claude Code's `--header` Bearer support is static and our tokens are short-lived. Restructure to distinguish two categories: - "Verified end-to-end": the in-repo agent + Claude Code (via local proxy). Names the proxy by reference for hosts with the same token-lifecycle gap. - "Designed-for, not yet verified by us": Claude Desktop, VS Code Copilot MCP, Copilot Studio. All three accept static Bearer via `.mcp.json` directly; refresh discipline is on the customer. Also explicitly states the implicit precondition that was always true but never stated: hosts must be able to attach `Authorization: Bearer <user-jwt>` to every request. The bundle's only interface with the host is `.mcp.json`; how the host sources a JWT is host-specific (and is the gnarly part for hosts whose token model isn't a great fit). 32 tests pass. No code/config change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(crm-agent): SKILL.md — strip host/project-specific accretion (#73 expanded) The "Compatibility honest" change in this PR's first commit was the right direction but too narrow. SKILL.md had been accreting more than just one overclaim — multiple sections were host-specific or project-specific in a way that breaks the "host-portable bundle" promise from ADR 0009. Removed (project / host specific noise): - "Single-source note" callout about in-repo agent + SkillsProvider — external customers don't care that OUR agent consumes this bundle. - "Setup" 3-step section enumerating Claude Desktop / VS Code / Custom agent wiring — those belong in each host's own MCP docs. The 1-line "edit .mcp.json + wire per host docs" is enough. - "Reference deployments: Bicep / func start / run_mcp_server_local.py" paragraph — pure project-specific (already documented in docs/deployment/walkthrough-zh.md and llm-foundry-zh.md). - "Compatibility" section listing verified-by-us hosts (Claude Code via proxy, etc.) — the "in this repo" framing falls apart the moment the bundle is dropped into a non-this-repo context. - Frontmatter `compatibility:` field's enumeration of named hosts. - "Further reading" link to "main repo docs/" — bundle shouldn't reach back into specific project layout. Kept (host-agnostic core): - Frontmatter (with generalized `compatibility:` text). - 2-line intro: what the skill does + how to wire (.mcp.json + host docs). - Available tools table — the contract. - Rating values / dates / FIELD_REFERENCE pointer — tool data the LLM uses. - SOP § 1-4 (name resolution, destructive confirm, RLS, error fidelity). - 3 Example dialogues. - references/FIELD_REFERENCE.md link. Test casualty: removed `test_skill_md_references_adr_0009` since we deliberately removed the ADR 0009 reference (it was the project-specific "single-source note"). The host-neutrality test (`test_skill_md_setup_is_host_neutral`) remains — its docstring no longer mentions ADR 0009 either. Net: 119 → 95 lines (-20%). 31 affected tests pass. Project-specific content all moved (or already lives) in the right docs: - Setup mechanics → docs/deployment/walkthrough-zh.md, llm-foundry-zh.md - Claude Code via proxy → llm-foundry-zh.md "本地 REPL" §4 - ADR 0009 single-source rationale → docs/adr/0009-... Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Let Claude Code (and any other MCP client that doesn't natively manage Azure AD token refresh) call our auth-protected MCP server.
Why a proxy is needed: Claude Code's HTTP MCP support has three relevant limits:
So a single `claude mcp add` would work for an hour, silently 401 forever after, and the user wouldn't know why their CRM tools stopped working.
Solution: thin local proxy on `127.0.0.1:8001`. Claude Code talks to the proxy (no auth needed for the localhost hop); proxy fetches a fresh `az` JWT (45-min eager refresh) and injects `Authorization: Bearer ...` on every forwarded request.
```
Claude Code ──► localhost:8001/mcp/ (proxy)
│ injects Authorization: Bearer
▼
MCP_SERVER_URL[_REMOTE] (real MCP server)
```
Wire-up
```bash
terminal X (long-running)
python scripts/mcp_auth_proxy.py # → MCP_SERVER_URL (local)
python scripts/mcp_auth_proxy.py --remote # → MCP_SERVER_URL_REMOTE (deployed FA)
one-time, in Claude Code
claude mcp add --transport http crm http://127.0.0.1:8001/mcp/
```
Implementation
Test plan
Doc
`docs/deployment/llm-foundry-zh.md` "本地 REPL" gets a new H3 (§4) "让 Claude Code 调我们的工具" explaining the gap, the workaround, and the wiring command. §5 (切换模型) numbering shifted from §4.
🤖 Generated with Claude Code