# MCP restriction resolver precedence, as a decision tree

> How the mcp restriction resolver precedence works across Elaichi's enforcement points, traced through one real tool call from browse to outbound request.

**TL;DR** Elaichi resolves restrictions in two layers: the rules on a member's role and the rules aimed at that member personally. Both layers must admit a connector or tool, so a personal rule narrows a role and never widens it, except for members still flagged as a legacy override. The mcp restriction resolver precedence runs at every point a member can reach a tool, from browsing the catalog to the final outbound request.

## The situation

An admin blocks a Jira tool for the Support role on Tuesday. On Wednesday a support agent's Claude connection still runs it. The admin re-reads the rule, finds nothing wrong with it, and starts to wonder whether restrictions work at all.

Usually the rule is fine. What went wrong is a misreading of where and when the rule is evaluated. Elaichi checks restrictions at more places than most people assume, in a fixed order, against two layers of rules rather than one. The mcp restriction resolver precedence is worth drawing out as a decision tree, because once you can trace one call through it, almost every surprising outcome explains itself.

A restriction, in Elaichi, is a rule saying which connectors and tools a role or a member may reach. A connector is an app Elaichi can run tools in. A tool is one operation inside that app.

## What does mcp restriction resolver precedence actually mean?

It means two layers are consulted on every check, and both must say yes. The rules written on the member's role form one layer. The rules aimed at that member personally form the other. A connector or tool is reachable only when both layers admit it.

That has a consequence people get backwards. A personal rule can only narrow what the role allows. If the Support role is blocked from Jira, a personal allow rule naming Jira does not open it. The way to lift a role rule for one person is an access request an admin approves, which creates an access grant for that requester alone.

There is one exception. A member whose personal rules predate layering, and could not be converted with identical effect, is flagged as a legacy override. For that member, the role's restrictions do not apply while the flag stands, and access grants do not apply either. The rule's summary says so: "Legacy override: this member's role restrictions do not apply to them while it stands." An admin clears the flag with a PATCH that sets `legacy_override` to false, which can only narrow the member.

So the decision tree starts with one question: is this member flagged as a legacy override? If yes, read their personal rules alone. If no, read both layers and intersect them.

## How do allow and block rules combine inside one layer?

Within each layer, all allow rules merge into one allowlist and all block rules merge into one blocklist. A block always beats an allow.

The trap sits in the allowlist. An allow rule turns on the allowlist for its layer just by existing, whatever it names. Once a role carries any allow rule, every connector and tool that none of its allow rules names is blocked for that role, across the whole catalog and not only the apps a rule mentions. An allow rule that names nothing blocks everything for its target; its summary reads "Allows nothing. Every connector and tool is blocked."

That is why holding a role to some tools of one app takes more than one rule. If you write an allow rule naming three Jira read tools, the role also needs whole-connector allow rules for every other app it uses. Those can be separate rules, because a layer's allow rules merge into one allowlist.

A layer with only block rules behaves differently. It has no allowlist, so everything it does not name passes. Blocking some of an app's write tools leaves every write tool you did not name reachable. On Microsoft Intune, `msintune_managed_devices_execute_action` stays reachable unless a rule names it.

Lineage adds blocks and never allows. A block on a connector also blocks every fork that declares descent from it, walking the lineage toward its root for up to eight hops. An allow rule naming a connector does not admit its forks; each fork has to be named on the allowlist itself.

There is also a detail about matching that catches people out. When a rule is saved, Elaichi looks each named connector and tool up in the organization's catalog and stores each tool's operation beside the name. An allow rule then matches only by that stored operation. A block rule matches by name or by operation. [Why blocks match tool names but allows don't](/blog/block-matches-name-allow-matches-operation/) works through the reasoning.

## Where does the resolver run?

Elaichi enforces restrictions at every place a member can reach a connector or tool, including listing, connecting, advertising, executing, the final outbound-request check and opening a stored tool file. Written out, the deployed enforcement points are:

- Listing connectors and their tools, in the console, the REST API and the AI catalog operations.
- Connecting or reconnecting an account.
- Forking a connector.
- Saving a toolbox entry or a synthetic tool step.
- Advertising tools to a client: MCP `tools/list`, `search_tools` and toolbox views.
- Executing any tool call.
- The final outbound-request guard.
- Opening a stored tool file that came out of a now-restricted tool.

Each of those is walked through in [the resolver's enforcement points](/blog/restriction-resolver-four-enforcement-points/).

The last guard deserves its own sentence. Before a call leaves, Elaichi compares the fully resolved outbound URL, and every redirect hop, with the HTTP operations named by tool-level block rules on the called connector and its declared ancestors, and refuses a match. It never refuses a request merely for reaching a host. A whole-connector block is enforced earlier, before the request is built.

One more point about the execution check: every connected-tool call passes a single restriction clamp, evaluated for the member the call acts as. MCP `tools/call` and `execute_tool`, `toolbox.execute`, the in-app assistant and each step of a synthetic tool all run through the same clamp. Restrictions therefore bind whatever client or automation sends the call, and they narrow what a member can do through a connection whatever the credential behind it could do upstream.

Client-side controls sit outside this resolver and are built for a different job. Claude sets connector tool permissions to Always allow, Needs approval or Blocked, applied to a whole connector, a category of its tools or a single tool ([Anthropic's connectors article](https://support.claude.com/en/articles/11176164-use-connectors-to-extend-claude-s-capabilities), checked October 2026). Cursor asks for approval before using MCP tools by default ([Cursor's MCP documentation](https://cursor.com/docs/mcp), checked October 2026). ChatGPT may ask for confirmation before a write or modify action, depending on app permissions and the action's context ([OpenAI's app permissions article](https://help.openai.com/en/articles/20001495-managing-app-permissions-in-chatgpt), checked October 2026). Those settings act on the tool names Elaichi advertises, and connected tools arrive as `search_tools` and `execute_tool`, so a per-tool setting in a client cannot single out one connected app. Per-app and per-tool shaping belongs in Elaichi, as restrictions.

## Tracing one call end to end

Take a support agent on a Support role. The role carries one block rule naming two Jira tools: the one that deletes an issue and the one that transitions an issue to Closed. The agent has no personal rules and is not flagged as a legacy override. Their Claude connection holds `mcp:tools`, and their role holds `tool:execute`.

The agent asks Claude to close a ticket. Here is what happens.

**Discovery.** Claude calls `search_tools` with the words the model chose. Elaichi searches the tools the agent may run, then ranks the agent's restricted tools separately against the same query and returns the matches by name, flagged restricted, with no description or schema and no way to call them, plus a `restricted_count`. So the transition tool appears in the result, named and marked, rather than vanishing. That is deliberate: the agent can see what to ask for.

**The call.** The model calls `execute_tool` with the transition tool's exact name. `execute_tool` is only a naming indirection. It unwraps to the tool name and arguments and goes through the same gates as a direct call, with no separate path and no extra privilege.

**The gates, in order.** First `tool:execute`, which the role holds. Then the name must resolve on this grant's surface. It resolves, and the restriction check fires: the role's block rule names this tool. The call is refused in-band, HTTP 200 with a tool error, telling the caller that the tool is restricted for their role by an organization policy and that an admin can lift it through an access request. The refusal does not name the rule, who wrote it, why, or what else it covers.

**What is not written.** A call refused at the restriction check writes no audit entry, because no executor ran. Audit rows are written for calls that reach execution and then run, fail or are refused there.

Now change one thing. Suppose instead the agent asks for a Jira comment tool that no rule names. The role has only block rules, so there is no allowlist, and the comment tool passes the layer. It passes the member layer too, which is empty. The call reaches the executor, which re-resolves the toolbox, requires use access to it, applies the caller's restrictions again, resolves the connection argument and writes an audit row. The outbound guard compares the resolved URL against tool-level block rules on Jira and its ancestors, finds no match, and the request leaves.

## Why the Tuesday rule looked broken on Wednesday

Because a restriction change is not effective the moment it is saved. A member's roles, teams and permissions, and their resolved restrictions, are cached for 60 seconds under keys that include a per-organization epoch. Every role, membership, team, restriction and grant change writes a new epoch. A change reaches every surface only once a point of presence sees the new epoch. Elaichi's own design note puts the worst case at a remote point of presence at about two minutes.

So write "about two minutes" on the change-management runbook, not "immediately". That is a worst case rather than a typical delay, and it is the number to plan around.

Two neighboring timings behave differently and are worth keeping straight. Revoking a share or disconnecting an account takes effect on the caller's next call, because toolboxes, shares and connections are read from the organization's data store on every request. Revoking a grant, removing a member or suspending one also stops the client on its next call, because every MCP request re-reads the grant.

There is also an advertising cache. A grant's connected-tool surface is held in worker memory for up to 60 seconds, keyed on epochs the data store moves on every write that changes who can reach which tool. A stale entry can only mis-advertise. Every authorization is taken again at call time, so a tool that should not run does not run, however it was listed.

## When the resolver is not your problem

If nobody has written a restriction, nothing is restricted. When no rule reaches a member, through their role or aimed at them, every connector and tool passes the check. A team of eight with one role and no rules does not need this decision tree, and reading it will not change anything for them. Start with [designing roles for AI agents](/blog/designing-roles-for-ai-agents/) instead, because a member holds exactly one role and the role is where most of the shaping belongs.

The resolver also will not solve a problem that is really about identity. If the question is which account a call acts as, the answer lives in connections and sharing, not restrictions: a shared connection runs every caller's call on its owner's sign-in to the third-party app. And if the question is whether a specific tool or the whole app should be blocked, [restrict one AI tool or the whole app](/blog/per-tool-vs-per-app-restrictions/) walks six concrete cases.

For the wider picture of who may do what in which app, [MCP governance](/blog/mcp-governance/) is the pillar. For the apps themselves, the [connector catalog](/connectors/) lists what is available to govern.

## The tree, written out

1. Is the member flagged as a legacy override? If yes, evaluate their personal rules alone and stop reading role rules. If no, continue.
2. Resolve the role layer. Combine its allow rules into one allowlist and its block rules into one blocklist. Any block wins. If any allow rule exists, anything unnamed is blocked.
3. Resolve the member layer the same way.
4. Admit the connector or tool only if both layers admit it.
5. Apply an access grant, if one exists, to lift the approved connector or tool out of the role layer only. A grant never lifts a rule written for the member personally.
6. Re-run the whole thing at each enforcement point, for the person making the call rather than for whoever pinned the connection.

Step five is the one admins most often misread. Approving an access request creates a grant for that one requester, not an exception rule, and the role's rules are never edited. Every other role rule keeps applying, including ones added later, and nobody else's access changes.

## FAQ

### Can a member-level restriction widen what that member's role allows in Elaichi?

No. In Elaichi, a member is governed by two layers at once: the rules on their role and the rules aimed at them personally. A connector or tool is reachable only when both layers admit it, so a personal rule can only narrow what the role allows. To lift a role rule for one person, an admin approves that person's access request, which creates an access grant for them alone. The one exception is a member flagged as a legacy override, whose personal rules predate layering: while that flag stands their role's restrictions do not apply to them, and access grants do not apply either. An admin clears the flag with a PATCH setting legacy_override to false, which can only narrow the member.

### How long does it take for an Elaichi restriction change to apply?

It takes about two minutes. A member's roles, teams, permissions and resolved restrictions are cached for 60 seconds under keys that include a per-organization epoch, and every role, membership, team, restriction and grant change writes a new epoch. The change reaches every surface only once a point of presence sees the new epoch, and Elaichi's design note puts the worst case at a remote point of presence at about two minutes. That is a worst case rather than a typical delay. Revoking a grant, removing a member, suspending one, revoking a share or disconnecting an account all take effect on the next call instead.

### Where does Elaichi check restrictions on a tool call?

At every point a member can reach a connector or tool. That includes listing connectors and their tools in the console, the REST API and the AI catalog operations; connecting or reconnecting an account; forking a connector; saving a toolbox entry or a synthetic tool step; advertising tools to a client through tools/list, search_tools and toolbox views; executing any tool call; a final outbound-request guard that compares the fully resolved URL and every redirect hop against tool-level block rules on the connector and its declared ancestors; and opening a stored tool file that came out of a now-restricted tool.

### Why does an empty allow rule block everything?

Because an allow rule turns on the allowlist for its layer just by existing, whatever it names. Once a role carries any allow rule, every connector and tool that none of the role's allow rules names is blocked for that role, across all connectors and not only the ones a rule mentions. An allow rule naming nothing therefore blocks every connector and tool for its target, and its summary reads "Allows nothing. Every connector and tool is blocked." To hold a role to some tools of one app, the role also needs whole-connector allow rules for every other app it uses.

### Is a restricted tool hidden from the person it is restricted for?

No. Elaichi's search_tools names tools a restriction blocks for the caller, flagged restricted and with no description or input schema, alongside a restricted_count, and a connector blocked whole appears as one row for the connector. Calling a restricted tool's name returns a restriction refusal explaining that the block comes from the caller's role or their own account and that an admin can lift it through an access request, rather than "Unknown tool". The refusal does not name the rule, who wrote it, why, or what else it covers.

## Read next

- [MCP governance: who may do what, in which app](/blog/mcp-governance/) — MCP governance decides who connects which app, which tools each role calls, whose account a call uses, what is logged and how access ends.
- [Where MCP restriction enforcement points actually sit](/blog/restriction-resolver-four-enforcement-points/) — The mcp restriction enforcement points in Elaichi run at eight places, not four, and a ninth check inside the AI client would prove nothing.
- [Restrict one AI tool or the whole app? Six cases](/blog/per-tool-vs-per-app-restrictions/) — Restrict one AI tool when a role needs part of an app, and block the whole app when it needs none of it. Six cases, and what new tools do to each rule.
