The short answer
For each server in the file, answer six questions: Is the package still maintained? Is a secret written into the file? Which of its tools can write, delete or move money? What can it reach (folders, projects, repositories)? Is it local or remote? Does it combine private data, untrusted content and a way to send data out? Fix the answers you don’t like, then rerun the check. npx deedglass scan or the in-browser /check answers the first four for the servers it knows.
Deedglass
A sealed record you can check offline
Record 1043
- What ran
- Medication order, Bed 12
- Approved by
- Dr. N. Shah
- Check
- Matches
- Time
- 09:12
- The request
- Not keptA fingerprint only: 77d2…c41a
Each record is linked to the one before
- Record 1041
- Record 1042
- Record 1043
Find every config first
You can’t audit a file you don’t know about. Each client keeps its own:
| Client | Project file | User file |
|---|---|---|
| Claude Code | .mcp.json5 |
~/.claude.json (local and user scopes)5 |
| Cursor | .cursor/mcp.json6 |
~/.cursor/mcp.json6 |
| VS Code | .mcp.json (portable) or .vscode/mcp.json7 |
~/.copilot/mcp-config.json or the profile mcp.json7 |
Run git ls-files | grep -i mcp in each repository, and check your home directory too. A user-level server applies to every project you open.
The checklist, per server
Copy this and fill it in for each entry under mcpServers (or servers in VS Code).
| # | Question | Bad sign | Fix |
|---|---|---|---|
| 1 | Is it maintained? | @modelcontextprotocol/server-postgres |
Use a maintained server, or a read-only database role |
| 2 | Is a secret in the file? | postgresql://user:pass@…, sk_live_…, ghp_… in args or env |
Read it from an environment variable |
| 3 | Which tools write? | execute_sql, write_file, create_refund |
Turn on the server’s read-only mode, or limit its tools |
| 4 | What can it reach? | a filesystem server given / |
Give it one project folder |
| 5 | Local or remote? | a url entry you haven’t reviewed |
Check its sign-in scopes by hand |
| 6 | Trifecta? | private data + untrusted content + a way out | Split it across agents, or drop one leg |
On row 1: Anthropic archived its reference Postgres server on 28 May 2025.3 Datadog Security Labs showed that its read-only mode runs each query in a read-only transaction that a stacked COMMIT; ends, after which later statements run with the role’s full rights.2 It’s still in plenty of configs.
On row 4: the reference filesystem server restricts every operation to its allowed directories, and the directories you pass on the command line are the method that works on every protocol version.4 Pass / and the agent can edit anything you can.
On row 6: Simon Willison’s “lethal trifecta” is access to private data, exposure to untrusted content, and the ability to communicate externally.8 An agent with a GitHub server (issues are untrusted content), a database server (private data) and Slack (a way out) has all three.
A worked example: D to B
We wrote a deliberately careless .mcp.json with nine servers: the archived Postgres server with a password in its URL, the filesystem server on /, GitHub with a token in env, Stripe with --tools=all and a live-style key in the arguments, Supabase with an access token, Slack, and remote Notion, Linear and Sentry. Then we ran deedglass 0.13.0’s scan on it, with the home directory isolated so only this file counted.1
Code What the screen or file shows, copied as is.
$ npx deedglass@0.13.0 scan
Blast radius: D · 8 write paths with no check · fix this first: Archived
Postgres MCP server · database not checked
A means a small blast radius, not a safe agent.
−8 medium finding Archived Postgres MCP server
−8 medium finding A database URL sits in a client config
−4 unchecked write path filesystem.edit_file · write · .mcp.json
−4 unchecked write path filesystem.move_file · destructive · .mcp.json
−4 unchecked write path filesystem.write_file · write · .mcp.json
−4 unchecked write path postgres.query · write · .mcp.json
−4 unchecked write path stripe.create_payment_link · money · .mcp.json
0 unchecked write path stripe.create_refund · money · .mcp.json (over the cap)
0 unchecked write path supabase.apply_migration · destructive · .mcp.json (over the cap)
0 unchecked write path supabase.execute_sql · write · .mcp.json (over the cap)
64 points
Found
1 MCP client config · 9 servers · 8 tools that can write
Then we applied the checklist:
- Removed the archived Postgres server (rows 1 and 2).
- Limited the filesystem server to
./docs(row 4). - Ran GitHub in read-only mode with
GITHUB_READ_ONLY=1, which offers only read-only tools, and passed the token from the environment instead of the file (rows 2 and 3).9 - Replaced the Supabase entry with its hosted URL scoped by
project_ref, withread_only=true, which runs all queries as a read-only Postgres user.10 - Dropped Stripe, Slack, Notion and Linear, which this project didn’t need.
The rescan:1
Code What the screen or file shows, copied as is.
Blast radius: B · 3 write paths with no check · fix this first:
filesystem.edit_file · database not checked
−4 unchecked write path filesystem.edit_file · write · .mcp.json
−4 unchecked write path filesystem.move_file · destructive · .mcp.json
−4 unchecked write path filesystem.write_file · write · .mcp.json
88 points
From 64 points to 88. The three that remain are honest: the filesystem server can still write inside ./docs. That’s its job. The scan’s own line puts it well: A means a small blast radius, not a safe agent.
What the automated check misses
The scan and /check are useful and narrow, and the gaps are worth knowing before you trust a grade. From the same two runs:11
- It didn’t flag the tokens. The GitHub token in
env, the Slack token and the Stripe key in the arguments got no finding. Only the database URL did. Review row 2 by eye, or run a secret scanner over the file. - It counted write tools for four servers only: filesystem, Postgres, Stripe and Supabase’s local package. GitHub and Slack contributed no write paths, though both can write.
- Remote servers are opaque to it. Notion, Linear, Sentry and the hosted Supabase URL added no write paths in either run. A
urlentry can be anything its sign-in allows; check row 5 by hand. - The database part needs a connection. “Database not checked” means it hasn’t looked at what the database role can change.
--postgres-dsn-env DATABASE_URLadds that.
/check runs deedglass’s own scan, compiled to WebAssembly, in your browser: nothing you paste is uploaded, and a config gets the grade deedglass scan gives a folder holding only that file.11 When we wrote this it ran the 0.12.0 build, which gave our first sample the same D.1
Make it a habit
A config file changes whenever someone adds a server for a demo and forgets it. Three habits keep it honest:
- Review it in pull requests. Scanning MCP configs in CI shows the GitHub Action that comments when a pull request changes the grade.
- Prefer project files to user files, so a server’s reach is visible in the repository. Where each client keeps its MCP config has the paths.
- Re-read the basics every few months: our MCP security checklist and the filesystem server post go deeper on single servers, and blast radius explains what the grade measures.
To try it, run npx deedglass scan in a repository, or paste a single config into /check, which runs in the browser and uploads nothing. What the grade means explains the scan’s letter. To watch the checks without installing anything, open the playground.
Written with AI assistance and checked against our claims register.