How do you handle configuration and environment variables for an MCP server?

5 minintermediatemcpconfigurationenvironment-variablessecrets

Quick Answer

Pass configuration (API keys, base URLs, feature flags) to an MCP server the same way you would to any CLI tool or service — via environment variables, read at startup and validated before the server starts accepting connections (fail fast with a clear error if something required is missing, rather than failing confusingly on the first tool call). For stdio servers, the launching host typically supplies these through the env field of its server config; for remote/HTTP servers, they come from the deployment platform's standard secrets mechanism (a container orchestrator's secret store, a cloud provider's environment configuration). Avoid hardcoding secrets in source code or committing them to version control, and avoid accepting sensitive configuration (like an API key) as a tool argument the model could see or accidentally leak into its output — configuration should live outside the protocol's request/response payloads entirely.

Detailed Answer

Configuration for an MCP server isn't fundamentally different from configuring any backend service, but a couple of MCP-specific wrinkles are worth calling out.

Reading config at startup, failing fast

import os, sys

API_KEY = os.environ.get("ACME_API_KEY")
if not API_KEY:
    print("Missing required ACME_API_KEY environment variable", file=sys.stderr)
    sys.exit(1)

Failing immediately on startup with a clear message is much easier to diagnose. Compare that to a server that starts fine, but produces a confusing error the first time a tool actually needs the missing key.

How a host supplies these values (stdio case)

{
  "mcpServers": {
    "acme": {
      "command": "python",
      "args": ["acme_server.py"],
      "env": { "ACME_API_KEY": "sk-xxxxx" }
    }
  }
}

Why config shouldn't be a tool argument

# Wrong: api_key as a tool argument the model could see, log, or leak
@mcp.tool()
def get_account(api_key: str, account_id: str) -> str: ...

# Right: the server already has the key from its own environment;
# the tool only takes business-level parameters
@mcp.tool()
def get_account(account_id: str) -> str:
    return call_acme_api(API_KEY, account_id)

If a secret is exposed as a tool argument, it becomes something the model could echo back into its response, include in a log of tool calls, or even leak via a crafted prompt. None of that is a risk for a value the server reads once from its own process environment and never surfaces through the protocol at all.

Remote/HTTP servers

The same principle applies, but sourced from whatever your deployment platform provides — a container orchestrator's secret mounts, a cloud provider's managed secrets service — rather than a host's local config file. The server should never need the user's MCP client to hand it credentials as part of a request. Per-user authorization for a remote server is instead handled through the OAuth flow covered in the security section, not through tool arguments or hardcoded server-side secrets.

Related Resources