Which MCP servers have the best native controls?
The argument comes up constantly now: you don’t need a separate control layer — the MCP servers already have controls. It’s half true, and the half that’s true is worth documenting precisely. First-party servers grew real control surfaces in the last year: read-only flags, toolsets, scoped keys, admin allowlists. They differ wildly, the docs rarely say who can change them or what they can’t express, and two of the most-used servers went the other direction entirely.
This page compares the native control surfaces of the fifteen most-used MCP servers as of August 2026, judged on seven questions:
- Auth — how does the server know who’s calling, and is scoping enforced server-side or client-side?
- Read-only — is there a real one, and at what layer does it hold?
- Scoping — can you bound the blast radius (one project, one directory, one org)?
- Tool filtering — can you remove tools from the surface entirely?
- Approvals — does anything natively require a human before a call fires?
- Audit — what record exists of what the agent actually did?
- Who holds the keys — where does the control live, and who can rewrite it?
This is the server-side companion to our harness controls comparison (what the agent side ships) and the coverage matrix (what our layer intercepts). Deep pages per server are linked in each row as we publish them. Corrections welcome — every claim links to its source, and this page changes when the servers do.
The short version
- Richest control surface: the GitHub MCP server. Read-only via flag, env var, header, or URL path; 23 toolsets plus per-tool selection; a lockdown mode for untrusted-contributor content. Also the clearest disclaimers about what those controls are not — lockdown is explicitly “not an authorization boundary”, and a June 2026 advisory found a cross-user bug in the lockdown control itself.
- Best-documented security model: Sentry. Dual-token OAuth so the client never holds the raw credential, a skills system that gates which tools exist per grant, path constraints enforced per request, fail-closed on stale grants. The page where we mostly grade up.
- Only server-side enforcement in the set: Stripe. Restricted API keys are checked by Stripe, not by the local process — a read-only key genuinely cannot issue a refund no matter what the agent or its config says. What’s structurally absent: amount thresholds and approval rules. Stripe’s own docs punt human confirmation to the client.
- Strongest database story: Supabase —
--read-onlyenforces as a read-only Postgres role (not just a hidden tool),--project-refbounds the blast radius to one project,--featuresprunes tool groups. All three are launch flags in the client config. The lethal-trifecta incident started from a config where none were set. - The vendor that tells you the truth: Playwright. The top server by usage deleted its origin filters in v0.0.47 as not-a-real-boundary, restored them a week later under community pushback, and still labels them “not a security boundary” (redirects walk through). The README’s security section opens with “Playwright MCP is not a security boundary,” and there is no native way to say “browse, but never submit a form.”
- The zombie: the archived Postgres reference server. Deprecated July 2025, still pulling roughly 312k npm downloads a month, with a SQL injection fixed in git but never published to npm or Docker. The maintained community successor defaults to unrestricted.
- The honest zero: Notion’s hosted server. No read-only mode, no tool filtering, no scoping — tools act with your full Notion permissions, and the read-only-capable open-source server is maintenance-only. The only native control on the hosted path is which human logs in.
- Admin plane ≠ operator policy: Atlassian and Slack. Genuinely good enterprise controls — domain and IP allowlists, connection approval, and per-call audit logs of tool invocations. But the controls decide which clients may connect, and the per-call artifacts are records, not rules: nothing decides which calls may fire.
- The one native ask — already being retired: AWS.
REQUIRE_MUTATION_CONSENTon the AWS API server is the single native per-call human approval in the set, verified fail-closed at both layers — and AWS’s managed successor deprecates it back to IAM (“Not needed — use IAM policies”). - Posture as a URL: Linear. The control Notion lacks, shipped as a dedicated endpoint —
/mcp/readonlyonly ever serves read tools, server-enforced, legible at a glance in any config. The best read-only UX in the set; also the only lever it has. - The aggregator: Zapier. One endpoint fronting 9,000+ apps — the lethal trifecta can assemble inside a single server. The finest tool selection in the set (per-action, per-field locking) is no longer the default; dynamic mode lets the agent enable its own actions. Also the fullest per-call audit record anywhere in the table — forensic only.
- The inverse page: Context7. Read-only by design — the risk flows into the agent, not out: open library submission, no review step, and a disclosed injection-via-docs channel (patched; the submission model is the product).
- The rawest surface: Chrome DevTools MCP. 57 tools over the DevTools protocol,
--autoConnectattaches to your real Chrome behind one Allow dialog,evaluate_scriptsurvives every subtraction — and the vendor gets default-on telemetry while the operator gets no record.
The comparison
| Server | Auth | Read-only | Scoping | Tool filtering | Approvals | Audit | Who holds the keys |
|---|---|---|---|---|---|---|---|
| GitHub (deep page) | PAT or OAuth (remote) | --read-only / GITHUB_READ_ONLY=1 / X-MCP-Readonly header — strict filter, takes precedence |
Token scopes + toolsets | 23 toolsets, per-tool --tools, dynamic toolsets (the model can enable more); lockdown mode for untrusted content |
None | None at the server | Launch flags + headers in client config; token scopes at GitHub |
| Playwright (deep page) | None (local browser) — --isolated / --storage-state control which sessions it holds |
None | --allowed-origins / --blocked-origins — removed in v0.0.47, restored a week later; docs: “not a security boundary,” redirects bypass |
--caps gates extras only (vision, pdf, devtools) — core act tools not subtractable |
None | --save-trace (off by default; forensics, not control) |
Launch flags in client config |
| Supabase (deep page) | PAT or OAuth | --read-only — enforced as a read-only Postgres role and hides mutating tools |
--project-ref (unscoped = every project in the account) |
--features groups |
None | None | Launch flags in client config |
| Filesystem (deep page) | None | None (Docker ro mount as a workaround) |
Allowed directories — but client-supplied MCP roots silently replace them | None | None | None | The connected client — the boundary is client-writable at runtime |
| Stripe (deep page) | OAuth (remote, sessions revocable in Dashboard) or restricted API keys | Read-only restricted key — enforced by Stripe server-side | Per-resource key permissions (binary read/write — no amounts, no velocity) | --tools (local); remote writes concentrate in one stripe_api_write meta-tool, so name-level filtering collapses |
“Recommended” — delegated entirely to the client | Stripe Dashboard API logs (not agent-attributed) | Key permissions at Stripe; tool selection in client config |
| Postgres (archived + Pro) (deep page) | Connection string | Archived: bypassable read-only transaction (unpatched SQLi in published build); Pro: --access-mode=restricted — default is unrestricted |
Whatever the DB role allows | None | None | None | Client config + DB role |
| Notion (hosted) (deep page) | OAuth | None — “MCP tools act with your full Notion permissions” | None below the logged-in identity | None | None | Enterprise-only: connection auditing, approved-app list | The vendor; operators get nothing below identity |
| Atlassian (deep page) | OAuth 2.1 / API tokens, user-permission mirroring | None per connection (revoking write permission-groups = org-wide coarse version) | Org-admin domain allowlist; IP allowlist evaluated per tool call; API-token path bypasses the domain check (separate toggle) | Permission groups (read/write/search per product) — grain stops at the group | None | Yes — per-call (tool name, action, user) in Atlassian admin audit logs, on any plan | Org admins, at connection altitude — the record is per-call, the rules aren’t |
| Sentry (deep page) | Dual-token OAuth (hosted) — client never holds the raw Sentry token | Default posture: write skills (triage, project-management) disabled by default |
Org/project path constraints, enforced per request | Skills gate which tools exist per grant (--skills / --disable-skills local) |
Grant-time consent only | None operator-facing | Grant-time skills at the vendor; fail-closed on stale grants |
| Slack (deep page) | User-token OAuth behind workspace-admin approval; Marketplace/internal apps only | Scope-level only (app without chat:write etc. exposes no write tools) |
Workspace + granted scopes — chat:write is one bit, no per-channel grain |
None | Admin approves the connection, then nothing | Yes — per-call (*_tool_called events with acting user) — Enterprise Grid only, no arguments |
Workspace admins, at connection level |
| AWS API (deep page) | IAM credential chain — the richest vendor-side floor in the set | READ_OPERATIONS_ONLY=true (default false) — fail-closed if its index can’t load |
IAM policies + condition keys (the real boundary) | denyList / elicitList security-policy JSON (exact-match, no wildcards) |
Yes — the only native ask: REQUIRE_MUTATION_CONSENT elicitation, fail-closed without client support — deprecated by AWS’s managed successor |
Local rotating log (→ CloudWatch); CloudTrail = account-plane, principal-attributed, not agent-attributed | Env vars + JSON on the agent’s machine; IAM at AWS |
| Linear (deep page) | OAuth 2.1 + DCR (hosted); Okta-managed access on Enterprise | Dedicated endpoint /mcp/readonly — server-side tool exposure; read scope blocks at the token |
None below the user — no team/project grain | None (read vs read-write is the only selector) | None | None documented | Endpoint choice in client config; enforcement at Linear |
| Zapier (deep page) | OAuth (listed clients) or one-time connection token — header recommended, token-in-URL fallback survives | No — action selection is the lever | Account-wide app restrictions (can’t be MCP-only); enterprise per-user quotas + vendor kill switch | Manual mode: per-action + per-field locking (finest in the set); default dynamic mode: agent enables its own actions, two execute_* meta-tools |
None | Yes — fullest per-call record in the set (instructions, field values, output) — forensic, deletable with its subject | Surface chosen at mcp.zapier.com; calls converge at Zapier with no policy at the point |
| Context7 (deep page) | None required; API key = rate tier, not scope | Read-only by design — two tools, no writes exist | — | — | — | None | Open library submission, no review step — the input is the attack surface |
| Chrome DevTools (deep page) | None — profile is the credential; dedicated persistent profile by default, --autoConnect reaches the real Chrome behind one Allow dialog |
None — evaluate_script survives even --slim |
--blockedUrlPattern / --allowedUrlPattern — in-process guardrails |
Category flags (network/perf/emulation removable; input core + evaluate_script welded on) |
None (Chrome’s dialog approves the connection, once) | None for the operator — usage telemetry to Google on by default | Launch flags in client config; the strongest controls are Chrome’s, not the server’s |
Sources: each server’s official docs and repos, linked from its deep page. Adoption context: the GitHub server is at ~30k stars with 23 toolsets; Playwright MCP ranks first in community usage surveys at ~36k stars; the filesystem reference server pulls ~233k npm downloads a week and Context7 ~554k. Firebase, Figma, Cloudflare, and Composio are tracked for the next pass. This table is the per-server sequel to our March 2026 registry-wide scans, where the ecosystem-level numbers live: 74.9% of 8,216 servers document no audit capability, 85% of paid-API proxies ship no rate limits.
One note on where our read comes from: we hold both ends of this wire in production. ACP runs its own MCP server (OAuth 2.1 with dynamic client registration) and an MCP client that mounts external servers behind one chokepoint — namespaced tools, SSRF checks at registration and again at tool-build time, a per-call record. The first security review of our own client wrote this page’s thesis down before the page existed: absent an explicit rule, an attached server with a delete_database tool is callable by a background agent on day one. The gaps below aren’t hypothetical to us — we filed them against ourselves first.
What the table actually says
The best controls in the ecosystem are launch-time flags in a file the agent can often edit. GitHub’s read-only mode, Supabase’s project scoping, Playwright’s capability gates — all real, all set in the client’s MCP config at connection time. That’s the wrong side of the trust boundary twice over: it’s static (a flag, not a policy that can distinguish calls), and it’s frequently agent-writable (the config lives in the workspace the agent edits; the filesystem server goes further — a connected client can replace the allowed-directories boundary at runtime, by design). A control the governed process can loosen is a preference.
Server-side enforcement exists and it’s the standard to hold everything else to. Stripe’s restricted keys and Supabase’s Postgres-role read-only hold no matter what the client config says. GitHub token scopes, similarly. Where the vendor enforces, the control is real; where the local process enforces, it’s best-effort — Playwright’s docs label its origin filters “not a security boundary” (redirects walk straight through them), and GitHub says the same about lockdown mode.
Approvals are missing from the category — with exactly one exception, and it’s being retired. Fifteen servers, one native per-call approval among them: the AWS API server’s REQUIRE_MUTATION_CONSENT, which holds every mutation behind an MCP elicitation prompt and fails closed when the client can’t render one — genuinely well built, off by default, and deprecated by AWS’s own managed successor, whose migration guide says to use IAM policies instead. Stripe — the server moving actual money — explicitly recommends human confirmation and delegates it to the client. Everything else assumes the harness or something standing between will ask. Which means: if the harness is in yolo mode and nothing stands between, nothing asks — and the one vendor that built the ask is walking it back.
Audit exists in three rows, in three grains, all vendor silos. Atlassian records tool, action, and user on any plan; Slack records *_tool_called events behind Enterprise Grid; Zapier records the most — instructions, field values, and output — in a History tab that’s deleted along with the server it describes. Real credit to all three, and the shape holds everywhere: each record is written after the call, consulted by nothing before one, and lives in one vendor’s console. Ask the question customers actually ask — prove exactly what every agent did across every connector last Tuesday, and why it was allowed — and no combination of the rows above can answer it.
Where every native model ends
Grade every row generously and the same four gaps remain — structural, not missing features:
- The controls are owned by the wrong party. Server-side controls belong to the service’s admins; client-side controls live in config the agent can reach. The operator who carries the risk — the person who answers for what the agent did — often holds neither.
- Every server is its own dialect. Read-only is a flag here, a Postgres role there, a key permission somewhere else, absent entirely on two of the ten. Eight connectors means eight vocabularies, and a rule you wrote for one doesn’t exist for the rest.
- No server can see across servers. The GitHub private-repo leak and the Supabase lethal trifecta were each a sequence — read untrusted content on one surface, act on another. Every individual call looked fine to the server that received it. The dangerous object was the sequence, and no per-server control can even express a rule about it.
- Almost nothing asks, and almost nothing writes it down. One approval mechanism in fifteen — deprecated by its own vendor’s successor. Three vendor-console records in fifteen, none consulted before a call, and no record an operator can query across surfaces.
Those four are the job of a layer that stands where the calls converge. The native controls are genuinely improving — this page exists because configuring them well matters, and every deep page below starts with exactly how. What none of them try to be is one policy, one approval path, and one record across every server your agents touch. That’s the layer we build; each deep page says precisely where the native surface ends and the handoff happens.