Your MCP Config Is Leaking Secrets
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:
- 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_rolekey (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.