[FEATURE] Support `envFile` in `.mcp.json` for loading secrets before env var expansion

Open 💬 6 comments Opened Feb 26, 2026 by deepank-atlas

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 runs claude)
  • 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:

  1. Team commits .mcp.json with ${VAR} references — no secrets, fully shareable
  2. Each developer creates their own path/to/.env with their tokens
  3. Claude Code loads the env file synchronously at startup, before resolving ${VAR} in URLs, headers, args, and env fields
  4. 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:

  1. We define both MCP servers in .mcp.json committed to the repo
  2. The HTTP servers need Authorization: Bearer <token> headers
  3. Each developer has their own API token
  4. 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
  5. 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:

  1. .mcp.json is committed with ${BACKEND_1_TOKEN} and ${BACKEND_2_TOKEN} in headers — clean, no secrets
  2. path/to/.env is in .gitignore
  3. Onboarding doc says: "Generate your API tokens, add them to path/to/.env" using path/to/.env.example
  4. Developer creates path/to/.env:

``
BACKEND_1_TOKEN=<their-token>
BACKEND_2_TOKEN=<their-token>
``

  1. Claude Code starts, loads the env file, resolves the headers, connects to both MCP servers
  2. Changes to .mcp.json merge cleanly since there are no local edits
  3. Same pattern works for our stdio servers — one consistent approach instead of two

Additional Context

_No response_

View original on GitHub ↗

This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗