← All posts
·4 min read·GuardLayer

Your MCP Config Is Leaking Secrets

MCPSecretsAISupabase

MCP config files — .mcp.json, .cursor/mcp.json, claude_desktop_config.json — are ordinary files that get committed to your repo, so any credential pasted inline (a database URL with a password, an API key, a token) is exposed to everyone with repo access. Reference secrets from environment variables instead ("env": { "KEY": "${KEY}" }), keep the real value in an untracked .env, and rotate anything already committed.

The Model Context Protocol (MCP) is how editors and desktop agents — Claude Desktop, Cursor, Cline — connect to tools and data sources. You wire up a server (a Postgres connector, a GitHub tool, a Supabase MCP server) in a small JSON config. And because getting it working is a two-minute copy-paste, people routinely drop a live connection string or API key straight into that JSON — then commit it with everything else.

That's the leak. The config file isn't special; git treats it like any other source file. A password in it is a hardcoded secret with all the same consequences: it's in history forever, and anyone who clones the repo — or scrapes it, if it's public — has it.

What does a leaking MCP config look like?

The most common version is a database MCP server pointed at production with the password inline in its arguments:

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-postgres",
        "postgresql://admin:S3cretDbPass99@db.prod.example.com:5432/app"
      ]
    }
  }
}

That single line hands your production database credentials to anyone with repo access. Here's what GuardLayer reports on that exact config — live engine output:

guardlayer scan · .mcp.jsonLive engine output
Check failed
75/100 · B
  • Critical.mcp.json:8

    Hardcoded secret in an MCP / agent config

    Reference the value from an environment variable instead (e.g. "env": { "X": "${X}" }), keep the real secret in an untracked .env, and rotate anything already committed.

The same shape shows up with a SUPABASE_SERVICE_ROLE_KEY, a GITHUB_TOKEN, or a provider API key sitting as a literal value in the env block instead of a reference.

The fix: reference, don't embed

MCP config supports environment-variable references. Keep the real secret out of the file:

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres"],
      "env": {
        "DATABASE_URL": "${DATABASE_URL}"
      }
    }
  }
}

Then set DATABASE_URL in your shell or an untracked .env, and make sure the config file's directory is covered by .gitignore if it's personal (e.g. .cursor/). If a secret is already committed, rotate it and purge git history — deleting the line isn't enough.

Two separate risks — don't confuse them

There's a second, deeper MCP problem worth naming so you can tell it apart from this one:

  • Secret in the config (this post). A static, mechanical leak: a credential is sitting in a committed file. It's fully catchable by reading the file, and the fix is a text change.
  • The lethal trifecta. An agent that has private-data access, sees untrusted input, and can act — so it can be prompt-injected into exfiltrating data. Notably, handing an agent the service_role key (which bypasses RLS) turns a small injection into full database access.

These need different defenses. To be exact about what a scanner can and can't do here: GuardLayer reads the config files in your repo and flags committed secrets and an over-privileged service_role key. It is a static analyzer — it does not run your MCP servers, monitor the agent at runtime, or stop prompt injection. The trifecta is mitigated by architecture (scope the agent's access, never give it service_role), not by static analysis. Anyone claiming a scanner "secures your AI agent" is overselling; the honest win here is keeping secrets and over-broad keys out of the config in the first place.

How do I check my MCP configs for secrets?

# Find MCP config files, then look for inline passwords / literal keys.
find . -name "*mcp*.json" -o -name "claude_desktop_config.json" | while read f; do
  grep -nE "://[^:@/]+:[^@/]+@|service_role|_KEY\"\s*:\s*\"[^$]" "$f" && echo "  ^ in $f"
done

Anything that returns a real value (not a ${VAR} reference) needs to move to an env var and be rotated. GuardLayer scans these config files on every push alongside your Next.js and Supabase code, so a committed key gets caught within seconds of the push.

FAQ

Are MCP config files committed to git by default? A project-level .mcp.json usually is (that's how it's shared with a team). Editor-level configs like .cursor/mcp.json may be personal — but they still live in a repo folder, so it's easy to commit one by accident. Assume anything under your repo can be committed.

Can I just use ${VAR} references everywhere? Yes — that's the recommended pattern. The config names the variable; the real value lives in an untracked .env or your shell environment. The secret never enters the file.

Does GuardLayer scan the MCP server itself? No. It's a static scanner for the code and config in your repo — it flags secrets and over-privileged keys in MCP config files. It does not run servers, inspect agent behavior at runtime, or detect prompt injection.

Is a service_role key in an MCP config worse than another key? Yes. The Supabase service_role key bypasses Row Level Security entirely, so an agent (or anyone who reads the config) gets unrestricted database access. Give the agent a scoped key or a dedicated RLS-enforced role instead.

Catch this before it ships — free

GuardLayer scans every push for this and 19 other Next.js + Supabase issues, with the exact fix inline.

No signup, no card — your code is scanned in memory and never stored.

Keep reading