PREDICTIVE TREND INSIGHT
Securing API keys in client-side Model Context Protocol plugins Illustration

Securing API keys in client-side Model Context Protocol plugins

Direct Summary:

Hardcoding API keys directly into an MCP server's config file is the single most common client-side security mistake — that file often ends up committed to git, synced across machines, or read by anything with local process access. The fix supported natively by VS Code's mcp.json is an inputs block: define a password-type input variable, reference it with ${input:variable-id} in the server's env, and the editor prompts for the value once and stores it securely instead of writing it to the file in plain text.

"Well done is better than well said."

— Benjamin Franklin

Key Insights

  • Never hardcode secrets in mcp.json: That file is routinely committed to git, synced to teammates, or left in a workspace others can read.
  • Use the inputs mechanism: VS Code's config format has a built-in prompt-and-store pattern purpose-built for API keys and passwords.
  • Stdio servers get credentials from the environment: The MCP spec itself says local, stdio-transport servers should retrieve credentials from the environment rather than any protocol-level auth flow.

The most common security mistake in client-side MCP setups isn't exotic — it's a plaintext API key sitting in a config file that later gets pushed to a public GitHub repo, synced via a dotfiles manager, or copy-pasted into a shared workspace. Because mcp.json is just JSON on disk, anything with read access to it — a teammate, a CI log, a misconfigured backup — can read your key. This lesson covers the actual mechanism VS Code provides to avoid that, and the security model MCP itself expects for local servers.

The inputs pattern

VS Code's mcp.json supports an inputs array specifically so you don't have to hardcode sensitive values. You declare a placeholder with a type, an id, and a human-readable prompt; if you mark it "password": true, VS Code masks the input field. Then you reference that placeholder from a server's env block with ${input:your-id} instead of the literal secret:

.vscode/mcp.json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "weather-api-key",
      "description": "API key for the weather service",
      "password": true
    }
  ],
  "servers": {
    "weather": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "weather-mcp-server"],
      "env": {
        "WEATHER_API_KEY": "${input:weather-api-key}"
      }
    }
  }
}

The first time this server starts, VS Code prompts you for the value in a masked input box and stores it securely for later runs — the raw key never has to appear inside the JSON file that gets committed to source control.

Why stdio servers don't use OAuth

It's worth being precise here about what "securing API keys" actually means for a local, stdio-transport server, versus a remote one. The MCP authorization spec explicitly says implementations using STDIO transport should not follow its OAuth-based authorization flow, and should instead retrieve credentials from the environment — because a stdio server is a local child process you already trust enough to spawn, not a remote service being accessed over a network. That's exactly the gap the inputs mechanism fills: a secure way to get a credential from you into that local process's environment without writing it to disk in the clear. (A remote, HTTP-transport MCP server is a different case, with its own real authorization flow — see the next lesson.)

Approach Where the key lives Risk
Hardcoded in mcp.json Plaintext in a file likely tracked by git High — leaks on commit, sync, or screen share
inputs + ${input:id} VS Code's secure storage, prompted once Low — file itself never contains the secret
Shell env var set outside the file (e.g. .zshrc/.bashrc) Your shell environment, referenced by name Low, but shared with every process you launch from that shell

Practical Challenge

Take any MCP server config where you currently hardcode a key in env, and convert it to use an inputs entry with "password": true instead. Confirm the raw key no longer appears anywhere in the committed file.

Concept Check

According to the MCP specification, how should a local, stdio-transport server obtain credentials?
Correct! The MCP authorization spec reserves OAuth 2.1 for HTTP-based transports and says stdio implementations should retrieve credentials from the environment instead.
Incorrect. Try again! Hint: OAuth in MCP is scoped to HTTP-transport servers; local stdio servers are expected to get credentials from their environment.

Sources & Further Reading

  • MCP configuration reference — official VS Code docs for the inputs array and ${input:id} syntax used to avoid hardcoding secrets.
  • MCP Specification — Authorization — the official spec section stating that STDIO-transport implementations should retrieve credentials from the environment rather than use the OAuth-based flow defined for HTTP transports.
Previous Guide Dashboard Next Guide