Post

Half of All MCP Servers Store Your Secrets in Plaintext

The credential storage problem nobody's talking about in the AI tool ecosystem. A look at MCP server credential storage and security

Half of All MCP Servers Store Your Secrets in Plaintext

If you’ve connected your AI coding assistant to GitHub, Azure DevOps, Slack, or a database through MCP, there’s a good chance your tokens are sitting in a plaintext JSON file right now. A JSON file in your home directory, readable by any process running as your user.

What Is MCP?

Model Context Protocol (MCP) is the standard that lets AI assistants like Claude, Cursor, and Cline connect to external tools like your GitHub repos, Slack workspace, and databases. MCP servers act as bridges between the AI and these services. Each bridge needs credentials. So where and how are those credentials stored?

Where Your MCP Tokens Live

Almost all AI tools that support MCP store its server configurations in a JSON file. The locations vary by tool, but the format is the same:

{ "mcpServers": { "github": { "command": "npx", "args": ["-y", "@anthropic/github-mcp"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_a1b2c3d4e5f6..." } } } }

That ghp_a1b2c3d4e5f6... is your real GitHub PAT, in plaintext, in a JSON file.

Here’s where those files sit across tools:

Tool Config File Scope
Claude Desktop (macOS) ~/Library/Application Support/Claude/claude_desktop_config.json App-wide
Claude Desktop (Windows) %APPDATA%\Claude\claude_desktop_config.json App-wide
Claude Code ~/.claude.json User-global
Claude Code (project) .mcp.json at the repo root Per-repo — may be committed to git
Cursor ~/.cursor/mcp.json User-global
VS Code .vscode/mcp.json Per-workspace
Cline cline_mcp_settings.json in VS Code’s globalStorage Extension

The 48% Problem

Trend Micro analyzed 19,402 MCP server source repositories. Their finding: 48% recommend or implement storing credentials in plaintext either hardcoded in JSON config files or in .env files on disk.

This isn’t due to developer laziness. The MCP ecosystem’s own documentation and getting-started guides point people toward this pattern. The Azure DevOps MCP docs say to set ADO_MCP_AUTH_TOKEN as an environment variable for example. Users follow these instructions and hardcode it in their config file because that’s where env vars go in the MCP JSON format.

Trail of Bits found that claude_desktop_config.json on macOS is created with 0644 permissions (world-readable). Any process on the system can read your MCP tokens without elevated privileges.

Why This Is Worse Than It Looks

The Aggregation Problem

A single MCP config file doesn’t just contain one token. It contains tokens for every service you’ve connected:

{ "mcpServers": { "github": { "env": { "GITHUB_TOKEN": "ghp_..." } }, "azure-devops": { "env": { "ADO_MCP_AUTH_TOKEN": "eyJ..." } }, "slack": { "env": { "SLACK_BOT_TOKEN": "xoxb-..." } }, "postgres": { "env": { "DB_PASSWORD": "prod_secret_123" } }, "jira": { "env": { "JIRA_API_TOKEN": "ATATT3x..." } } } }

One file read can allow an attacker into your GitHub repos, Azure DevOps projects, etc. That doesnt take take hacking super skills. That’s one cat command.

The Three-Tier Trap

Claude Code uses a three-level scope system for MCP configs:

  1. Local (~/.claude.json under the project path) — private to you
  2. Project (.mcp.json at the repo root) — designed to be checked into git
  3. User (~/.claude.json globally) — applies across all your projects

The project scope is the trap. .mcp.json exists specifically so teams can share MCP configurations through version control. If you or anyone on your team hardcodes a token instead of using the ${ENV_VAR} reference syntax, that token is now in:

  • Your git history (permanent, even if you delete the file later)
  • Every developer’s machine who clones the repo
  • Your CI/CD environment
  • GitHub/GitLab’s web UI

Run git log -p .mcp.json on your repos. You might not like what you find.

The Cline Cloud Sync Problem

This one surprised me. Cline stores MCP credentials in cline_mcp_settings.json inside VS Code’s globalStorage. That file is:

  • Unencrypted JSON
  • Automatically included in VS Code Settings Sync
  • Uploaded to GitHub if you have Settings Sync enabled

Tokens you intended to keep local are silently synced to the cloud. Nobody opted into this. It’s just how VS Code’s sync mechanism works. It syncs extension globalStorage, and Cline puts credentials there.

Token Passthrough Attacks

MCP servers receive tokens at startup via environment variables. What stops a malicious MCP server from capturing those tokens and replaying them elsewhere? Not much, it turns out.

OAuth 2.1 added resource indicators (RFC 8707) that bind tokens to specific servers, preventing replay. But most deployed MCP servers don’t use OAuth 2.1. They use static tokens, and a static token works wherever you present it. A compromised MCP server npm package could harvest every token passed to it by every user who installed it. Given that 48% of setups hand credentials directly to the server process on launch, the blast radius is enormous.

The Permission Problem

Even when tokens aren’t hardcoded, the config files themselves are often misconfigured.

On macOS, claude_desktop_config.json is created with 0644 permissions — readable by any user or process on the system.

On WSL, Windows-mounted files default to 0777. Your MCP config at C:\Users\you\AppData\Roaming\Claude\claude_desktop_config.json is world-readable and world-writable from the Linux side.

On Linux, Claude Code creates ~/.claude.json with 0600 — but that’s the exception. Many tools don’t enforce this.

Run ls -la on your MCP config files. If you see anything other than 0600, fix it.

How MCP Servers Handle Auth (The Good, The Bad, The Common)

Not all MCP servers handle credentials the same way. Here’s how the major ones work:

Azure DevOps MCP expects ADO_MCP_AUTH_TOKEN as a Personal Access Token. PATs can be scoped, but users commonly create them with broad permissions. The docs recommend setting it as an env var, which users interpret as “put it in the config file.”

GitHub MCP supports GITHUB_PERSONAL_ACCESS_TOKEN and GITHUB_TOKEN. A better pattern exists: the ${input:github_mcp_pat} prompt variable asks for the token interactively each time. Few users know about it.

Slack MCP actually does this right. It uses a proper OAuth flow where users authorize via the Slack app. Permissions are explicit and revocable. This is what all MCP servers should do.

Google Cloud MCP supports Application Default Credentials, which auto-discover credentials from gcloud auth application-default login. No hardcoded tokens needed. But the alternative — API key auth — falls right back into the plaintext problem.

What You Should Do Right Now

1. Check your configs

Run AIHound to scan your system for exposed MCP credentials:

python3 -m aihound -v --html-file mcp-audit.html

Or check manually:

# macOS cat ~/Library/Application\ Support/Claude/claude_desktop_config.json cat ~/.claude.json # Linux cat ~/.claude.json cat ~/.cursor/mcp.json # Check permissions ls -la ~/.claude.json ls -la ~/Library/Application\ Support/Claude/claude_desktop_config.json

2. Replace inline tokens with env var references

Before (bad):

"env": { "GITHUB_TOKEN": "ghp_a1b2c3d4e5f6..." }

After (better):

"env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }

Then set the real value in your shell profile or a .env file that’s in .gitignore.

3. Fix file permissions

chmod 600 ~/.claude.json chmod 600 ~/.cursor/mcp.json chmod 600 ~/Library/Application\ Support/Claude/claude_desktop_config.json

4. Check your git history

git log -p -- .mcp.json git log -p -- .vscode/mcp.json

If you find tokens in the history, rotate them immediately. The tokens are compromised even if the file has been deleted — git history is permanent unless you rewrite it.

5. Disable VS Code Settings Sync for Cline

If you use Cline with MCP servers, either disable Settings Sync entirely or exclude Cline’s extension storage. Your MCP tokens should not be in GitHub’s cloud.

6. Add .mcp.json to .gitignore

If your .mcp.json contains inline secrets (or might in the future), add it to .gitignore now:

# MCP server configs may contain secrets .mcp.json

If you need to share MCP server configuration (commands, args) without secrets, use env var references and document the required env vars in your README.

What Tool Vendors Need to Do

  1. Store MCP credentials in OS keychains. macOS Keychain, Windows Credential Manager, Linux libsecret. The APIs exist. Use them.

  2. Create config files with 0600 permissions. Not 0644. Not “whatever the OS defaults to.” Explicitly set 0600 on creation.

  3. Warn users when inline secrets are detected. If a config file contains what looks like a hardcoded token in an env block, show a warning.

  4. Support credential helpers. Git solved this problem years ago with git credential-helper. MCP needs the equivalent. A mechanism where the config file specifies how to get a credential, not the credential itself.

  5. Adopt OAuth 2.1 with audience validation For remote MCP servers, static tokens should be deprecated in favor of short-lived OAuth tokens bound to specific servers (RFC 8707). This prevents token passthrough and limits the window of exposure.

The Bigger Picture

OWASP already has this on their radar. “Token Mismanagement and Secret Exposure” is MCP01-2025 in the OWASP MCP Top 10. But awareness doesn’t fix config files. We know how to store credentials securely: OS keychains, credential helpers, short-lived tokens with proper scoping, etc. We need to make sure we apply the same security practices to MCP


I built AIHound to scan for exactly these issues. It checks 13 AI tools across Windows, macOS, and Linux. Credentials are redacted by default so the output is safe for reports and screenshots.

This post is licensed under CC BY 4.0 by the author.