Policy
MCP permissions: how to decide what agents may read, write, or delete
Connect one MCP server and you inherit twenty or thirty tools you didn't write. Some are obviously reads, some obviously writes, and a few, like anything that takes a SQL string, are whatever the caller makes of them. 'Reads run, writes wait' is where nearly everyone starts, and it's a decent start. It's also where most teams stop, which is how an agent ends up with an Allow on a tool that can dump a customer table.
This is a method for going one step further: an inventory you can trust, four questions per tool, and a starter matrix with real tools from Linear, GitHub and Postgres that you can copy and then argue with.
The short answer
Work from the tool list the server actually returns, not from its docs. For each tool, decide what it can expose, what it can change, whether that change is reversible, and how far the credential behind it reaches. Assign one mode per tool, narrow by resource where the tool carries one, and write the reason down. Reads Allow and writes Ask is a starting posture, never a verdict.
Start from tools/list, not the documentation
The list a server returns from tools/list is the only inventory that counts, because it's the one the model sees. Docs lag: GitHub's official server replaced its create_issue tool with issue_write, which creates or updates issues and pull requests, and a policy written against the old name governs nothing. Servers also change shape by configuration. GitHub's remote server exposes toolsets you can turn on and off, with a read-only variant of each, and PostHog's server hands most clients a single exec tool that can do anything PostHog can, unless you connect with a mode parameter that splits it into individual tools. One meta-tool can't be split into reads and writes by any policy; the split has to happen at the server.
So pin the list. Tier Two reads a connection's tools once and stores them, and a server that adds a tool tomorrow doesn't silently widen what agents can do; you see the difference when you refresh, and new tools arrive with the connection's preset mode while existing tools keep the mode you set. If your gate doesn't pin, refresh the inventory on a schedule and diff it, because a new tool with a harmless name is exactly how an allow-everything preset gets a delete verb.
Three levels of control, and what each can't say
Permission decisions land at different levels, and confusion between them is the source of most overreach. A rule at the wrong level feels like control and isn't.
| Level | Example | What it can enforce | What it cannot express |
|---|---|---|---|
| Credential | A read-only endpoint, an OAuth read scope, a SELECT-only database role | What the account can reach at all, regardless of tool | Which of the reachable things this task should touch |
| Tool | Allow, Ask or Block on linear__save_issue | Whether a named operation runs, waits, or is hidden | Anything about the arguments |
| Resource | A different mode for database id 3 than for database id 7 | One declared argument, when the tool carries it | Filters on arbitrary arguments or on the data returned |
| Destination | Linear project membership, a repository's branch protection | Row and object level access, with the system's own rules | Whether a person should have said yes to this call |
Notice the gap in the third row. Neither Tier Two nor the host rule grammars in Claude Code and Cursor offer a general allow-list of arguments, and that's not an oversight; matching free-form arguments is how you end up with a regex as a security boundary. If a tool is safe with some inputs and dangerous with others, and a single resource argument can't separate the two, the honest choices are Ask, so a person sees the arguments, or Block. The argument-level control that does exist belongs to the destination, and you get it by narrowing the credential, which is the read-only access article's subject.
Four questions for every tool
Run each tool through the same four questions and record the answers. They take a minute apiece once you've done a few, and the record is what makes the policy reviewable later.
- What's the broadest result it can return? Not the typical result; the worst one. A list_issues call returns whatever the credential can read, including the private project nobody meant to include.
- What does it change, and for whom is the change visible? A comment is seen by the whole team within seconds; a branch push is seen by CI.
- Can the change be undone, and by whom? An edited issue can be edited back. A deleted comment is gone unless the system keeps history. A merged pull request is a different conversation.
- How far does the credential reach on its own? A personal sign-in inherits that person's limits. A shared workspace key inherits nothing, so the tool's mode becomes the only limit.
The fourth question is the one people skip, and it changes the answer to the other three. The same save_issue tool is a modest risk behind Dana's own Linear login and a large one behind a workspace admin token.
A starter policy matrix
These are real tool names from the official Linear and GitHub servers and a generic Postgres server, as they appear in tools/list at the time of writing. Modes assume a personal sign-in; with a shared key, move each write one notch stricter and reconsider the reads. Suggested modes are opinions to argue with, not a compliance table.
| System | Tool | What it can touch | Side effect and reversibility | Suggested mode | Why |
|---|---|---|---|---|---|
| Linear | list_issues, get_issue | Every issue, description and comment the account can read | None | Allow, or Ask on workspaces with confidential projects | A read, but one that returns private discussion; decide by what it can return |
| Linear | save_issue | Creates or updates issues | Notifies watchers; edits are reversible by another edit | Ask, scoped by project where the tool carries a project id | The arguments (title, project, assignee) are the decision |
| Linear | save_comment | Posts a comment | Visible to the team immediately; can be edited or deleted but not unseen | Ask | A wrong comment is a small incident with a wide audience |
| Linear | delete_comment | Removes a comment | Destructive | Block until a workflow needs it | No agent workflow in most teams needs deletion |
| GitHub | get_file_contents, search_code | Source of every repository the token can read, including secrets committed by mistake | None | Allow for public repositories; Ask or a read-only toolset for private ones | The same tool name spans very different data |
| GitHub | create_or_update_file, push_files | Commits to a branch | Reversible through git, but CI and reviewers react instantly | Ask | The branch name is the argument that matters, and it isn't a declared scope |
| GitHub | create_pull_request | Opens a PR | Triggers CI, notifies reviewers; closable | Ask | Cheap to undo, expensive in attention |
| GitHub | delete_file | Removes a file in a commit | Revertible by another commit | Block, or Ask on a sandbox repository | Rarely what the task needs |
| Postgres | execute_sql, query, or any tool taking a SQL string | Anything the database role can SELECT; writes if the role allows them | Depends entirely on the role and the statement | Ask. Allow only behind a SELECT-only role on a non-production replica | The name says nothing; the role and the statement say everything |
| PostHog | exec (the single meta-tool most clients receive) | All of PostHog | Mixed reads and writes in one tool | Block until the server is connected in the mode that exposes individual tools | One tool can't be split by policy |
Two rows deserve a second look. The GitHub read row shows why 'read' isn't a mode by itself: the identical tool is harmless on an open-source repository and a leak on the monorepo, and only the credential or the resource tells them apart. The Postgres row is the general case of a tool you can't classify from its name, and any tool whose input is a query language belongs in it, including the ones with friendly names.
How Tier Two's Recommended preset guesses, exactly
When you add a connection, Tier Two applies the Recommended preset, and it's worth knowing the rule rather than trusting the label. The classifier checks, in order: does the tool name end in execute_sql, run_sql, execute_query or run_query? Then it's SQL, and gets Ask. Does the server set readOnlyHint to true? Then it's a read, and gets Allow. Does the name start with get, list, search, read, describe, fetch, find, show, view, query, construct, count, check, lookup or browse? Then it's a read. Otherwise it's a write, and gets Ask. Open sets everything to Allow; Locked sets everything to Block until you allow a tool by hand.
The name check sits after the hint check on purpose, and the hint is not trusted on its own. The MCP spec says clients must treat annotations as untrusted unless the server is trusted, and in practice servers get them wrong in both directions; Metabase marks one of its plain reads as not read-only, and the name rule catches it. The guess is the starting point and the mode is the enforcement. Nothing about a tool being classified as a read makes it safe to Allow if its broadest result is a customer list.
Presets are per connection, and changing one resets every tool on that connection, including the modes you set by hand. The dashboard asks you to confirm, and the change lands in the governance ledger with your name and a before-and-after diff, so a preset switch is a fine way to establish a baseline and a poor way to make a small adjustment later. The policy documentation has the current preset wording.
Scope by resource, and know where it stops
Some tools carry the resource in their arguments: a database id, a project id, a repository. Tier Two lets a tool declare one such argument as its scope, using a dotted path into the call, and the mode can then differ per resource. Remembered approvals key on the same scope, so 'always for Dana' can mean always for Dana on database id 3 and still Ask on database id 7. A call whose declared scope can't be resolved from the arguments is refused rather than guessed, and the refusal shows in Activity as 'couldn't scope the call', which is the behavior you want when the alternative is falling through to Allow.
Modes are workspace-wide. The only per-person difference is a remembered approval, and there is no 'always for everyone'; if everyone should run a tool, set it to Allow so the decision is visible. That constraint sounds limiting and is mostly a relief, because two policy layers that can disagree is how nobody can answer 'why did this run'.
Test the policy before an agent does
A written matrix is a hypothesis until a call goes through it. Before turning agents loose on a connection, run one deliberate call per mode from a real host and check the record.
- Call an Allow tool and confirm Activity shows it as Allowed, with the person, agent and host attached.
- Call an Ask tool, watch the request appear, deny it, and confirm the agent reports the denial rather than trying another path.
- List tools from the host and confirm a Blocked tool is absent, then call it by name anyway and confirm the refusal is recorded.
- For a scoped tool, send a call with the scope argument missing and confirm it's refused rather than allowed.
- Grant a one-hour approval, revoke it, and confirm the next call asks again.
Then put the matrix somewhere people can edit it, with a column for the last date each row was checked. Policies that nobody re-reads decay in the same direction: toward Allow.
Fewer Allows than you started with
Done properly this review ends with a shorter Allow list, a handful of Asks on the tools whose arguments are the decision, and a Block or two on verbs no workflow needs. That's a policy you can explain to the person whose data it protects, which is a better test than whether it feels convenient during the demo.
Tier Two is built around exactly this matrix: one mode per tool, resource scoping where the tool carries one, and a pinned tool list so a server can't widen your policy overnight. Connect one system, keep the Recommended guess or override it row by row, and see every decision in Activity.
Sign up for Tier Two