Skip to content

feat(mcp): resolve secrets into project-scope configs for all tools - #258

Merged
jeff-r2026 merged 1 commit into
mainfrom
feat/mcp-project-plaintext-secrets
Jul 29, 2026
Merged

feat(mcp): resolve secrets into project-scope configs for all tools#258
jeff-r2026 merged 1 commit into
mainfrom
feat/mcp-project-plaintext-secrets

Conversation

@jeff-r2026

Copy link
Copy Markdown
Collaborator

Problem

When distributing team MCP servers at project scope, only Claude Code received secret-bearing servers. Cursor and CodeBuddy silently skipped any server whose headers/URL referenced a ${VAR} (e.g. Authorization: Bearer ${GPU_ANALYSIS_TOKEN}).

Root cause: reconcileMcpForConfig refused to resolve a placeholder for tools that cannot expand ${VAR} themselves when the target file is project-scoped (committed to the repo), to avoid leaking a plaintext secret into version control. Only Claude's project .mcp.json (expands ${VAR} itself) and Codex (bearer_token_env_var) kept the placeholder, so only they got the server.

Reproduced against the test repo https://git.woa.com/teamai/teamai-dev-repo (gpu-analysis server): claude/gpu-analysis added, but cursor/gpu-analysis skipped, codebuddy/gpu-analysis skipped.

Change

Drop the project-scope refusal. The value is now resolved and written verbatim for every tool, in every scope. The only remaining skip reason for secrets is a genuinely unresolved variable.

  • Claude project .mcp.json still passes the placeholder through (Claude expands it).
  • Codex still names the env var (bearer_token_env_var / env_http_headers).
  • Cursor / CodeBuddy now get the resolved value written into their config (new files created 0600).

Docs (both languages) gain a warning: gitignore .cursor/mcp.json / .codebuddy/mcp.json, or distribute secret-bearing servers at user scope, if the secret must stay out of version control.

Test Plan

  • npx tsc --noEmit — no errors
  • npx vitest run — 1814 passed (140 files). Rewrote the mcp-reconcile test that previously asserted the skip, to assert the resolved value is now written for Cursor.
  • npm run build — success
  • End-to-end against the real test repo (teamai-dev-repo, project scope, GPU_ANALYSIS_TOKEN set):
    • Before: cursor/gpu-analysis skipped, codebuddy/gpu-analysis skipped
    • After: cursor/gpu-analysis added, codebuddy/gpu-analysis added
    • Verified on disk — .cursor/mcp.json & .codebuddy/mcp.json contain Authorization: "Bearer <resolved-token>" at mode 0600; .mcp.json keeps Bearer ${GPU_ANALYSIS_TOKEN}.

🤖 Generated with Claude Code

Previously, project-scope MCP config files for tools that cannot expand
${VAR} themselves (Cursor, CodeBuddy) skipped any secret-bearing server,
to avoid writing a resolved secret into a committed file. Teams that
accept committing the secret (or gitignore the config) had no way to
distribute such a server at project scope.

Drop the project-scope refusal: the value is now resolved and written
verbatim for every tool, in every scope. Claude project .mcp.json still
passes the placeholder through, and Codex still names the env var, so
those keep secrets off disk as before. Docs gain a warning to gitignore
.cursor/mcp.json / .codebuddy/mcp.json or use user scope if the secret
must stay out of version control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jeff-r2026
jeff-r2026 merged commit bf468e8 into main Jul 29, 2026
7 checks passed
jeff-r2026 added a commit that referenced this pull request Jul 29, 2026
…rets off disk (#260)

CodeBuddy and Cursor were getting remote (http/sse) MCP servers written with
`transportType: "streamable-http"`, a field both tools ignore — so the
Authorization header never shipped and the server 401'd. Both key the transport
off `type`, exactly like the claude family. Emit `type` for them.

While fixing the format, restore placeholder passthrough for these two tools so
team secrets no longer land on disk (undoing that part of #258 for them):
- CodeBuddy interpolates the bare `${VAR}` in every scope — pass it through.
- Cursor interpolates `${env:NAME}` — rewrite `${VAR}` into that syntax.

`supportsEnvExpansion` now returns true for cursor/buddy; the renderers carry
the per-tool placeholder syntax. The resolve-to-plaintext path remains only as a
fallback (user-scope claude, codex placeholders it cannot name as a whole
header).

Verified end-to-end against the real test repo in a project-scope install:
claude keeps `${VAR}`, codebuddy `type:"http"` + `${VAR}`, cursor `type:"http"`
+ `${env:VAR}`; a POST to the backend with the resolved token returns HTTP 200,
confirming the header now ships. Docs (EN + zh-CN) updated.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant