[FEATURE] Support `envFile` in `.mcp.json` for loading secrets before env var expansion
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
Teams using HTTP-based MCP servers in .mcp.json have no clean way to manage per-developer secrets. The config file needs to be committed to the repo so the whole team shares the same MCP server setup, but HTTP servers require auth tokens in headers — putting developers in an impossible position: commit secrets or don't share the config.
For stdio servers, the community workaround is wrapping commands with dotenv-cli to load secrets from a gitignored env file. This works because there's a command to wrap. HTTP servers have no command — just a URL and headers — so there's no place to inject the env file loading.
The current alternatives are all fragile or awkward:
- Export vars in
~/.zshrc(not project-scoped, leaks across projects) - Launch with
dotenv -e path/to/.env -- claude(easy to forget, breaks if someone just runsclaude) - SessionStart hook writing to
$CLAUDE_ENV_FILE(timing issues — MCP servers may init before the hook completes; also unreliable per #15840) - Hardcode tokens in
.mcp.json(security risk, different per developer, flagged in #23852)
This is a daily friction point for any team using HTTP MCP servers with auth.
[!Note] This applies specifically to HTTP MCP servers using static Bearer tokens (API keys) — the common pattern for internal and private MCP servers. Servers that support OAuth don't have this problem since Claude Code handles the auth flow interactively. For these internal servers, implementing a full OAuth flow is overkill; they just need a simple way to inject a token without hardcoding it.
Proposed Solution
Add an envFile field to .mcp.json that loads environment variables before any ${VAR} expansion happens:
{
"envFile": "path/to/.env",
"mcpServers": {
"amazing-software-api": {
"type": "http",
"url": "http://some-amazing-software.domain/mcp/",
"headers": {
"Authorization": "Bearer ${API_TOKEN}"
}
},
"amazing-software-service": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@some/mcp-server"],
"env": {
"SECRET_KEY": "${SECRET_KEY}"
}
}
}
}
The path/to/.env file (gitignored) contains the actual secrets in standard dotenv format:
API_TOKEN=sk-abc123
SECRET_KEY=my-secret
The developer experience becomes:
- Team commits
.mcp.jsonwith${VAR}references — no secrets, fully shareable - Each developer creates their own
path/to/.envwith their tokens - Claude Code loads the env file synchronously at startup, before resolving
${VAR}in URLs, headers, args, and env fields - MCP servers connect with the resolved values
This also eliminates the dotenv-cli wrapper hack for stdio servers, giving both transport types a single, consistent way to handle secrets.
Alternative Solutions
1. dotenv-cli wrapper (stdio only)
Works for stdio servers by wrapping the command: npx dotenv-cli -e path/to/.env -- npx @some/server. Not applicable to HTTP servers since there's no command to wrap. Also makes stdio configs verbose and hard to read.
2. Shell profile exports (~/.zshrc)
Each developer adds export API_TOKEN=... to their shell profile. Works reliably but isn't project-scoped — tokens leak across unrelated projects. New developers have to manually figure out which vars to set. Nothing ties the required vars to the project.
3. Launcher wrapper (dotenv -e path/to/.env -- claude)
Works for both server types since the vars are in the process environment before Claude Code starts. But it's fragile — if anyone runs claude directly, all HTTP MCP servers fail silently. Can't be enforced across a team.
4. SessionStart hook with $CLAUDE_ENV_FILE
A hook that appends path/to/.env contents to $CLAUDE_ENV_FILE at session start. Promising in theory, but has timing issues — MCP servers may initialize before the hook completes. Also $CLAUDE_ENV_FILE can be empty in some contexts (#15840), making this unreliable.
Priority
High - Significant impact on productivity
Feature Category
MCP server integration
Use Case Example
We build a product with two backend services, each exposing an MCP server over HTTP with Bearer token auth. The team has 10+ developers, all using Claude Code Max daily.
Today's painful workflow:
- We define both MCP servers in
.mcp.jsoncommitted to the repo - The HTTP servers need
Authorization: Bearer <token>headers - Each developer has their own API token
- We tell developers to export tokens in their shell before running Claude —
export BACKEND_1_TOKEN=... && export BACKEND_2_TOKEN=...— but people forget, tokens change, and managing 4-5 env vars across multiple MCP servers gets tedious fast - New developers joining the team have to figure out why MCP tools aren't working, discover they need a token, find out how to generate one, and hand-edit the config
With envFile:
.mcp.jsonis committed with${BACKEND_1_TOKEN}and${BACKEND_2_TOKEN}in headers — clean, no secretspath/to/.envis in.gitignore- Onboarding doc says: "Generate your API tokens, add them to
path/to/.env" usingpath/to/.env.example - Developer creates
path/to/.env:
````
BACKEND_1_TOKEN=<their-token>
BACKEND_2_TOKEN=<their-token>
- Claude Code starts, loads the env file, resolves the headers, connects to both MCP servers
- Changes to
.mcp.jsonmerge cleanly since there are no local edits - Same pattern works for our
stdioservers — one consistent approach instead of two
Additional Context
_No response_
This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗