How many MCP restriction enforcement points are there?
More than four. An admin writes one rule and wants to know where it bites. Elaichi checks a restriction 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.
The mcp restriction enforcement points in the deployed product are eight. 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. The final outbound-request guard. And opening a stored tool file that came out of a now-restricted tool.
A restriction is a rule naming which connectors and tools a role or one member may reach. In Elaichi, restriction targets are role or user only; the organization default is the absence of a rule, which means allow everything. To keep a team off a tool, the rule goes on their role or on each member.
What the resolver decides, and whose access it reads
Every one of those checks asks the resolver the same question. For this person, is this connector or this tool reachable? A member is governed by two layers at once: the rules on their role, and the rules aimed at them personally. Both layers have to admit the tool, and a personal rule can only narrow what the role allows. Inside each layer, all allow rules merge into one allowlist and all block rules into one blocklist. A block always beats an allow. How restrictions sit beside roles and sharing grants is laid out in three separate access layers.
An empty allow rule is the trap. An allow rule turns on the allowlist for its layer just by existing, whatever it names. So an allow rule naming nothing blocks every connector and tool for its target. Its summary says so: "Allows nothing. Every connector and tool is blocked."
The resolver reads the person the call acts as. Not the owner of the connection, and not the toolbox entry's delegator. A tool a restriction blocks for the caller is blocked inside every toolbox entry for that caller, frozen arguments or not. Allow rules match only the operation Elaichi pinned when the rule was saved. Block rules match by name or by operation. The reasoning behind that asymmetry is in why blocks match tool names and allows do not.
Browse and connect: the checks that run before any tool call
These two run before a connection exists. They are the reason a rule can stop something from ever being set up. When an AI client or the console lists a connector's tools, restricted tools are left out. The count of what was left out comes back as restricted_count, and a connector blocked whole is refused rather than listed as empty. In the console's own connector and connection tool lists, every tool comes back with restricted, restricted_by and restricted_scope flags for anyone who can read the connector. So confirming that one tool is restricted for you needs no permission to view restrictions.
Connecting or reconnecting an account is itself checked. That matters more than it sounds. Creating a personal connection is a default Member permission. Absent any restriction, a Member can connect any connector in the catalog with their own account. The connect-time check is what makes an approved-apps list stick when somebody tries to sign in to app number nine.
Authoring checks: forking, toolbox entries and synthetic steps
Three more checks run while somebody is building, before anything is callable. Forking is refused for a connector, or a fork of one, that a restriction blocks for the caller. Saving a toolbox entry or a synthetic tool step is refused when it names a tool restricted for the person saving it. The message reads "Tool () is restricted for your account."
These checks are convenience, not the boundary. An entry saved before the restriction existed is not refused again on later saves. It is withheld when the toolbox resolves, and refused when it is called. That is the point: execution is the layer that has to hold.
Fork lineage runs one way on purpose. A block on a connector also blocks every fork that declares descent from it, walking the lineage toward its root for up to 8 hops. A chain that is longer, loops or is broken is refused outright rather than checked partially. An allow rule does not inherit, so a fork has to be named on the allowlist itself. And a connector's identity is its slug plus its declared lineage, never the host it calls. A connector built from scratch against the same API as a blocked one is not caught by that block. That is why connector:create is the one permission Elaichi flags "High trust".
What an AI client sees when a tool is restricted
A restricted tool is withheld from tools/list and from a toolbox's runnable tools. The check runs for the person using the toolbox, against the catalog tool's own name and operation. An entry's rename, schema override or frozen values change nothing about the outcome.
It is not hidden, though. search_tools ranks the caller's restricted tools separately against the same query. It returns those matches by name, flagged restricted, with no description, no schema and no way to call them, plus a restricted_count. A connector blocked whole shows as one row for the connector. Calling a restricted tool's name gets a restriction refusal rather than "Unknown tool". The refusal tells the caller whether the block comes from their role or from their own account, and that an admin can lift it through an access request. It does not name the rule, who wrote it, or what else it covers.
The advertised surface is cached in worker memory for up to 60 seconds per grant. A stale entry can only mis-advertise. Every authorization is taken again at call time.
The execute clamp: one check for every surface
Every connected-tool call Elaichi runs passes one restriction clamp, evaluated for the member the call acts as. MCP tools/call and execute_tool go through it. So does each tool call inside a run_code program, elaichi__toolbox__execute, the in-app Agent, and every step of a synthetic tool. Restrictions therefore bind whatever client or automation sent the call. They narrow what a member can do through a connection, whatever the credential behind that connection could do upstream.
Two details make the clamp trustworthy. Each tool call through a stored toolbox re-resolves the toolbox at call time from the organization's store, including every entry's delegation standing. The advertised list is never used to authorize a call. And when the resolver gets an answer in a shape it does not recognize, it refuses the call rather than allowing it. That happens, for example, with an organization store still running older code during a deploy. It does not cache that answer either.
The outbound-request guard: the last check before the wire
As a last check before a call leaves, Elaichi resolves the outbound URL in full. It compares that 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 at all.
Within a declared fork chain, the guard compares the HTTP method, path and host the called tool really sends with those of each blocked upstream tool. Renaming a tool does not escape a block written against the original. Nor does editing the resource and method keys inside a fork's config. This is the check that makes a tool-level block about what a call does rather than what it is labeled.
Why a fifth check at the client would be theater
Because Elaichi's MCP tool list names no connected app. In that list, connected tools are never listed one by one, however few there are; a client finds them with search_tools. So a client control that picks or gates individual MCP tools can act only on execute_tool, run_code, elaichi__toolbox__execute and elaichi__synthetic_tool__execute, each as a whole. It cannot tell a Salesforce call from a payroll call. And a permission set on execute_tool does not cover the other three paths.
Tool annotations will not rescue it either. Elaichi treats annotations as untrusted hints for clients and keeps its own check as the gate. execute_tool and run_code are annotated destructive and open-world. The real tier of whatever they are handed is not knowable before the call. So a client setting that runs read-only tools without prompting still prompts on every connected-app call, reads included. It does not make connected apps read-only.
The client vendors say the same thing about their own controls. Anthropic warns that a serverName entry in Claude Code's allow and deny lists is not a security control, because the name is a label the user assigns (Claude Code managed MCP docs, checked October 2026). It also describes server-managed settings as a client-side control rather than a security boundary, which a user on an unmanaged device can bypass without admin access (Claude Code server-managed settings, checked October 2026). A check a user can edit is not an enforcement point.
What the client should carry instead
Per-call confirmation. Over MCP, the person's OAuth grant is the approval. Elaichi runs every call the grant's scopes cover as already approved, and sends no per-call prompt of its own. A prompt a person sees is their client's. Claude's connector tool permissions offer Always allow, Needs approval or Blocked, per connector, category or tool (Anthropic connectors help, checked October 2026). Cursor asks for approval before using MCP tools by default (Cursor MCP docs, checked October 2026). The layers are compared in human approval for AI agent actions.
Prompt-injection defense belongs there too. Nothing on Elaichi's /mcp path scans tool results for injection. The gate could not run there as built: the endpoint reads only a tool name and arguments from a tools/call. It never has the person's prompt to judge the call against.
And the honest case for skipping all of this. A company running one client against one app can get what it needs from that client's own connector-level policy. Restrictions earn their keep when the same tool is reachable from several clients, surfaces and scheduled jobs at once.
When a restriction change actually bites
Role, team-membership, restriction and access-grant changes reach every surface within about two minutes. A member's resolved restrictions are cached briefly behind a per-organization epoch. Revoking a share or disconnecting an account takes effect on the caller's next call. So does revoking a grant, removing a member or suspending one.
Restriction writes are recorded as restriction.created, restriction.updated and restriction.deleted. Approving an access request records an access grant for that one person rather than editing the role's rules. That new access arrives on the same schedule as a restriction edit.
For how rules map to roles across a whole company, start from MCP governance. For whether to write a rule against one tool or the whole app, read restrict one AI tool or the whole app. The governance use case walks the same model from the admin's side at /use-cases/governance/.