# MCP Security Checklist: 33 Controls for Discovery and the Gateway

> A scored checklist for Model Context Protocol: find the MCP servers already running on laptops, then gate what AI agents can reach. Weighted, vendor neutral, free.

Source: https://www.lutril.com/mcp-checklist
Controls: 33
Total weight: 770

---

MCP turned every SaaS tool into something an AI agent can call, and most of it arrived without a ticket. Two surfaces decide how that goes: the servers already running on laptops, which no identity provider or network log will ever name, and the gateway between an agent and the systems you care about.

Work down the list. Each control you confirm takes its weight off the exposure score. Nothing is sent anywhere, and your progress stays in this browser.

## 01. Discover what is already running

Most MCP servers are local processes a desktop AI client starts from a config file in a home directory. They hold long lived credentials, they are not sandboxed, and nothing in an identity provider, a CASB or a network log will ever name one.

- **Endpoints are scanned, not only the network** (critical, 35) A local server started over stdio never makes an outbound connection you can see. If discovery stops at traffic and sign-in logs, the entire local surface is invisible, including every server on a laptop nobody has signed into for months.
- **No credential is written in cleartext into a client config** (critical, 30) An API key typed into a JSON file is a credential with no rotation, no expiry and no owner. Replace the literal with an environment reference and move the value into the operating system keychain.
- **Any config holding a credential is owner only** (high, 20) On macOS every account shares the staff group, so a group readable config is a shared secret. Set it to owner only, then rotate, because another local account could already have read it.
- **No credential is passed as a command line argument** (high, 15) Process lists are readable by every account on the machine. A token in the arguments is a token everyone local already has, and it reaches process creation auditing and shell transcripts as well.
- **No config file can be rewritten by another account** (critical, 30) A writable config, or a writable folder above it, lets a local attacker add a server the client loads without a new prompt. Documented client flaws in 2026 worked exactly this way.
- **No MCP container mounts the Docker socket or runs privileged** (critical, 35) The host Docker socket is root on the machine, and privileged mode removes most of what a container isolates. Either one makes the container boundary decorative.
- **Filesystem servers are scoped to a project, never to a drive root or a home directory** (critical, 30) A filesystem server rooted at home turns every file the account can read into model context, reachable by anything that can influence what the model reads.
- **Packages are pinned, and nothing is fetched and run at launch** (high, 20) An npx or uvx launcher pulls from the registry and executes on every start, so a compromised release lands without a single config change. Install once at an exact version, or pin a digest.
- **Remote servers use https and an authentication method you can name** (high, 25) Plain http sends the credential across the network in the clear. A remote server with no token in its config is either using an OAuth flow that stores it elsewhere, or it is protected by nothing but its URL.
- **A definition that changes after approval raises an alert** (critical, 35) The command, the arguments, the variable names or the host can all change on an entry you already approved. That drift is how a reviewed server becomes a hostile one, and it is the half a one-time inventory cannot do.

## 02. Gate what the agent may actually do

A gateway earns its place by what it refuses. These are the gates that hold when an agent is confused, when a token is replayed, and when a tool somebody disabled this morning is still registered on an open session.

- **Tenant identity comes from the verified token and nowhere else** (critical, 35) Never from a header, a query parameter or a request body. One accepted hint from the caller is a cross-tenant read, and the database should be the authority on isolation rather than application code.
- **Authorization is OAuth 2.1 with PKCE and rotating refresh tokens** (high, 25) Consent is remembered per user, per client and per server, so five servers do not mean five browser round trips. Cross-tenant is checked when authorization starts and again at consent, because a server can move while a ticket is still live.
- **Tokens are audience bound to one server** (critical, 30) The audience is the server URL and a second claim names its id, both derived at issue time so the refresh path cannot drift. A token minted for one server is refused on every other, and a legacy token with no server claim is refused everywhere.
- **Each integration is addressable in its own right** (high, 20) One server per integration, each with its own protected resource, state, tool inventory and audit lineage. A single catch-all endpoint makes every grant a blanket grant and every log line ambiguous.
- **Writes require an explicit grant and no role bypasses it** (critical, 30) An owner is not an exception. A stored write grant that does not name its approver was auto granted under an older policy and is void. Where several gates read the same grant, the weakest one decides.
- **The tool list is the permission surface** (high, 25) A tool the caller may not run is never registered, so the model gets each tool's real parameters instead of one loose object, and it never spends a turn on a call it will be refused on.
- **A tool's risk class is read on every call, never at session registration** (critical, 30) Sessions outlive decisions. If the gate reads the value captured when the client connected, an admin disabling or reclassifying a tool changes nothing until that client disconnects.
- **The call carries the caller's own vendor credential** (high, 20) Then the vendor's own audit names the human, and their vendor permissions bound the call. A missing credential is a structured answer naming the connect URL, with no silent fallback to a shared connection.
- **Third-party servers are revalidated on every call, redirects included** (critical, 30) A host that resolves publicly today can resolve to a link-local address tomorrow, and a default HTTP client follows redirects without rechecking the destination. Validate, resolve and pin every hop, and require https, because the call carries the credential.
- **Revocation covers tokens already issued and tokens not yet minted** (high, 25) A cutoff invalidates what exists, a block refuses to mint more, and both are checked on verification and on both issuance paths. Otherwise a revoked user simply refreshes.
- **Nothing upstream leaks back** (high, 20) Never return vendor tokens, never echo the authorization header, and never reflect a vendor error body. A reflected response is what turns a blind request forgery into a readable one.
- **A failed tool sync changes nothing** (medium, 15) If a sync error is written as an empty inventory, an expired vendor token reads as the vendor having deleted every tool, and the gateway quietly disables a working integration.

## 03. Prove it, and say what you cannot prove

The part that decides whether any of this survives an audit, a leaver, or a Monday morning. It includes the limits worth stating out loud, because a control that quietly overclaims is worse than one you know you are missing.

- **Every call is logged down to the vendor tool it reached** (high, 25) Actor, tenant, server, the tool called and the upstream tool it proxied to, with payload sizes. A log that stops at the gateway boundary cannot answer who moved the record.
- **Access is time boxed and taken back on the deadline** (high, 20) An expiry that depends on somebody remembering is not an expiry. The sweeper that removes access should write the same audit line a human removal would.
- **Requests and approvals work in Slack, Microsoft Teams and the app** (high, 20) One decision, one audit line, three front doors. A flow that exists in only one of them pushes half the company into email, and that half leaves no record.
- **There is a kill switch and somebody has tested it this quarter** (critical, 30) Per agent and per tenant, effective on tokens that are already out. An untested kill switch is a slide, not a control.
- **Every agent has a named human owner** (medium, 15) A non-human identity with no owner survives every offboarding, every access review and every reorganisation. Ownership is what makes the other controls actionable.
- **Approving a definition never clears a finding about one copy of it** (high, 20) This command with these variable names is fine. That is a different statement from this laptop writing the token in cleartext being fine. Keep definition findings and install findings in separate counts.
- **A scan that could not read everything retires nothing** (medium, 10) A locked file or a missing disk permission must not make servers look gone. An inventory that silently retires what it failed to read reports progress nobody made.
- **Attribution is shown as inferred** (medium, 10) An endpoint scan cannot prove who uses a machine. Render the signed-in address as a hint with its source, and let an admin override it, rather than printing it as an identity claim.
- **Rescoring the fleet is a server side change** (medium, 15) The endpoint should emit observations from a fixed vocabulary and let the server own the weights. Otherwise changing your mind about a risk means pushing a new binary to every laptop.
- **The agent on the endpoint never executes what it discovers** (medium, 15) It reads config files an attacker may have written. Running what they name is the exact vulnerability it exists to report, so the scanner should have no way to execute at all, enforced by a test rather than by review.
- **The scoring version is recorded on every row** (medium, 10) When a weight changes, you need to find exactly which findings an older engine judged. Without a version stamp, a rescore is a guess and a trend line is fiction.

## Where the weights come from

The discovery weights answer the three questions a local MCP server actually turns on: where the credential is stored, what the process can reach under the user's account since a local server is not sandboxed, and whether the package is pinned and attributable. The OWASP MCP Top 10 supplies the rest. It is an Incubator document, so it is treated as provisional rather than settled.

The gateway controls are taken from running one: audience bound tokens, consent per user and per server, write grants that name an approver, and a tool inventory that doubles as the permission surface. Several of them exist because the obvious version was wrong first.

Every control here is verifiable without Lutril. We automate the parts that do not survive a spreadsheet: a read-only agent that finds local servers across macOS, Linux and Windows, drift detection on definitions you already approved, and a gateway that holds these gates on every call.

## Questions about the checklist

### How is the exposure score calculated?

Each control carries a weight between 10 and 35 points, set by how much damage the gap behind it allows. The score is the share of unconfirmed weight, expressed from 0 to 100, so it starts at 100 and falls as you confirm controls. Confirming every critical control alone takes it to 51.

### Why does the checklist cover laptops and not just the gateway?

Because most MCP servers never reach a gateway. They are stdio processes launched by Cursor, Claude Desktop or VS Code from a config file in a home directory, holding a long lived token, with no outbound connection for a network tool to see. A gateway governs what you route through it, and nothing else.

### Do I need a product to work through it?

No. Every control is a statement you can verify by reading a config file, a token claim or a log line. A scored spreadsheet gets a team through it once. What a spreadsheet cannot do is notice when an approved definition changes next month, which is the single highest weighted control on the list.

### Is my progress sent anywhere?

No. Ticking a control writes to this browser's local storage and nowhere else. There is no account, no form and no tracking of which controls you confirmed. Copy open items puts the remaining controls on your clipboard as plain text so you can paste them into your own tracker.

### What counts as confirming a control?

That you have checked the current state and it holds, not that it is on a roadmap. Several controls are about honesty rather than configuration: attribution shown as inferred, a partial scan retiring nothing, a scoring version on every row. Those are worth confirming because the alternative is a report that quietly overclaims.
