# MCP execute_tool indirection security

> MCP execute_tool indirection security rests on one rule: the wrapper unwraps to a name and arguments, then meets the gates a direct call would meet.

**TL;DR** In Elaichi, execute_tool is a naming indirection and nothing else: it unwraps to the tool name and arguments it was handed. The call then passes the same role, scope, restriction and toolbox checks a direct call passes. The cost is that a client control gating tool names sees every connected-app call as one execute_tool. Per-app control belongs in Elaichi's restrictions.

## The tool list a security reviewer sees first

When an AI client connects to Elaichi, its tool list holds Elaichi's own `elaichi__` operations. Beside them sit `search_tools` and `execute_tool`, plus `run_code` where Code Mode is available. There is no Salesforce tool in that list, and no Jira tool, because connected tools are never listed one by one, however few there are. The first question a reviewer asks is whether that wrapper is a way around the rules. MCP execute_tool indirection security is the answer. `execute_tool` resolves a string to a tool and decides nothing else, and the call it unwraps to meets the gates a direct call meets.

## What does execute_tool do with the name it is handed?

It unwraps it. `execute_tool` takes `name`, which is required, and `arguments`. There is no separate execution path behind it, and it carries no privilege of its own. A `connection` argument picks the connected account a tool runs against. It belongs inside `arguments`. One sent at the top level beside `name` is refused, with an error showing where to move it. Nothing else is guessed.

The name can also be one of Elaichi's own operations. `execute_tool` will unwrap to an `elaichi__` operation, which then has to clear that operation's gates rather than the wrapper's. Nothing about passing through `execute_tool` changes what the named thing is allowed to do.

## Why MCP execute_tool indirection security has to mean no extra privilege

Because the alternative is a bypass with a friendly name. Suppose the wrapper held authority of its own. Every role check, every restriction and every scope decision could then be sidestepped. A caller would route the call through the wrapper instead of naming the tool. A restriction, in Elaichi, is a [rule on a role or on one member](/blog/roles-restrictions-acls-three-mechanics/) saying which connectors and tools they may reach. A rule that holds for a direct call and not for a wrapped one is not a rule.

So the wrapper does one job. It turns a name into a resolved tool, then hands the call to the same checks. The honest test of any meta-tool is not what its description promises. It is whether both routes land in the same place.

## Which gates one connected-tool call passes, in order

Four gates, in this order. A connected-tool call passes them whether or not it was wrapped.

1. The caller's role must hold `tool:execute`. A role without it gets an empty tool list. The in-band refusal says re-authorizing cannot grant it, because it is a role and not a scope.
2. The name must resolve on this grant's surface. A grant is the OAuth authorization the person approved for that client. Without the `mcp:tools` scope the answer is a scope refusal. A tool a restriction withholds gets a restriction refusal. Anything else gets "Unknown tool".
3. The gate re-derives the tool's tier, read, write or delete, at call time. It charges no further scope for that tier. A custom tool edited from a GET to a DELETE is dispatched as a delete. If the tool no longer resolves, the call is refused rather than run unclassified.
4. Execution re-resolves the toolbox and requires use access to it. It then applies the caller's own restrictions, resolves the `connection` argument, and writes an audit row.

One detail matters for evidence. A call refused at the scope check writes no audit row, because the refusal happens before any executor runs. Rows exist for calls that reached execution and then ran, failed or were refused there.

## What the gates read at call time, not at list time

Every authorization is taken again when the call arrives. A grant's connected-tool surface is what `search_tools` ranks and `execute_tool` resolves. It is kept in worker memory for up to 60 seconds. That cache only affects what is advertised. Each tool call re-reads toolbox access, the caller's restrictions and the scope tier from a fresh resolution. A stored toolbox is re-resolved entry by entry. That includes whether the person who pinned each connection still holds access to it.

The timing differs by change. Revoking a grant fails the client's very next request. Revoking a share or disconnecting an account takes effect on the next call. Role, team and restriction changes reach every surface within about two minutes.

## What happens when the name is one of Elaichi's own operations

It meets that operation's own gates, which are stricter than a connected tool's. The call passes `tool:execute`, then the app-only check, then the step-up check. Next comes the requirement that the name exists, is not forbidden and is offered to this client. After that comes the organization's assistant-operations setting. The scope ladder runs last: a read needs `mcp:read`, a write needs `mcp:write`, a delete needs `mcp:destructive`. The executor then re-checks the role's permission, the plan feature and the operation's own access rules.

Some names cannot be reached at all. Six operations are listed when role and scope allow, then refused on every MCP call. They are creating and deleting invitations, changing a member's roles, deleting a member, deleting a role and deleting a team. The Elaichi app asks the person to confirm it is them first. The refusal names the console screen to use. Three operations classified forbidden, covering credential creation or rotation, security changes and authentication flows, never run on any surface. Wrapping a name does not conjure a path that does not exist.

## Can a client skip the wrapper and call the tool name directly?

Yes, and that is the point. The server accepts a connected tool's exact name in `tools/call`, even though it is never listed. The call meets the same gates it would meet through `execute_tool`. Over MCP a connected tool can also be run through `run_code`, where Code Mode is available, and through the `elaichi__toolbox__execute` and `elaichi__synthetic_tool__execute` operations. Each of those routes ends at the [same restriction check](/blog/restriction-resolver-four-enforcement-points/), evaluated for the person the call acts as.

A name that resolves to nothing returns an in-band tool error rather than a protocol error. When the caller has connected tools, it suggests the three closest names, ranked as tool search ranks them. A name with one typo still finds its target. The error also says the gap may be a role, a scope or a restriction rather than a spelling mistake. The mechanics of that ranking are covered in [how tool search picks one tool from hundreds](/blog/search-tools-ranking-floor-idf/).

## What the indirection costs

One tool name carries every connected-tool call made through `execute_tool`, whatever the app. A client control that allows or gates individual MCP tool names therefore sees them as one tool. It cannot tell a Salesforce read from a Gusto write. A permission set on `execute_tool` also misses calls made through `run_code` or `elaichi__toolbox__execute`. Per-app and per-tool control has to live in Elaichi, as restrictions on a role or on one member. The case for each shape is in [restrict one AI tool or the whole app](/blog/per-tool-vs-per-app-restrictions/).

The annotations compound this. Elaichi marks `execute_tool` destructive, open-world and not idempotent. The real tier of whatever it is handed is not knowable before the call. Elaichi treats annotations as untrusted hints for clients, not as gates.

Client-side approval settings read those hints. Codex sets approval behavior per server with `default_tools_approval_mode`. Its `writes` setting prompts for any tool not marked read-only ([OpenAI's MCP docs](https://learn.chatgpt.com/docs/extend/mcp), checked October 2026). Cursor asks for approval before using MCP tools by default ([Cursor's MCP docs](https://cursor.com/docs/mcp), checked October 2026). Claude's connector tool permissions take one of three settings: Always allow, Needs approval or Blocked. They apply 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). A per-tool setting of that kind can only reach the tools Elaichi lists, so on Elaichi it lands on `execute_tool` rather than on one app. Because `execute_tool` is never marked read-only, a client that prompts for anything but reads asks before every connected-app call, reads included.

Scopes do not narrow this either. `mcp:tools` alone runs every tier of connected tool, deletes included. Leaving "Create and change data" unticked does not make a connected app read-only. To hold a client to reads, pick toolboxes of read tools at consent. Or block the app's write and delete tools with a restriction.

## When you do not need any of this

If one team uses one app's own MCP server, and that server lists its tools individually, the indirection question never arises. The client shows a dozen named tools. Per-tool approval there means what it looks like it means, and nothing sits between the client and the vendor. Adding a layer there buys governance you may not need yet, and [the honest case for waiting](/blog/when-you-dont-need-an-mcp-gateway/) is worth reading before you shop.

The wrapper earns its place when one address fronts many apps for many people. At that point a tool list long enough to be useful stops fitting in a model's context. Naming becomes indirection whether you choose it or not. The question left is whether the indirect path is checked as tightly as the direct one. That is what [an MCP control plane](/blog/what-is-an-mcp-control-plane/) is for. It is also the thing to verify in any product that offers one. To see what sits behind the wrapper on Elaichi's side, browse the [connector catalog](/connectors/).

## FAQ

### Does calling a tool through execute_tool need a different OAuth scope than calling it directly?

No. In Elaichi, execute_tool unwraps to the tool name and arguments it was given. The call then meets the same scope check a direct call meets. For connected tools that check is mcp:tools, which runs every tier, including tools that delete in the connected app. The wrapper never adds or removes a scope requirement.

### Can an AI client call a connected tool by its exact name even though Elaichi never lists it?

Yes. Elaichi's MCP endpoint accepts a connected tool's exact name in tools/call, even though connected tools are never listed one by one, however few there are. That call passes the same gates as a call made through execute_tool. Those are the role's tool:execute permission, the grant's scopes, the caller's restrictions, and use access to the toolbox. The model finds the name with search_tools.

### What happens when execute_tool names a tool a restriction blocks?

Elaichi refuses it in band. The response is HTTP 200 with a tool error. It says the tool is restricted for the caller's role or for their account. It adds that an admin can lift the block through an access request. The refusal does not name the rule, who wrote it, or what else it covers, and the call never reaches the connected app.

### Can execute_tool reach Elaichi's own management operations?

It can unwrap to an elaichi__ operation. That operation then clears its own gates. Those are the role permission it requires and the scope its classification needs. A plan feature and the operation's own access rules apply too. Some names are refused on every MCP call regardless. They include creating or deleting invitations, changing a member's roles, deleting a member, and deleting a role or a team. The Elaichi app asks the person to confirm their identity first.

### Why is execute_tool annotated as destructive even when it runs a read?

Because Elaichi cannot know the real tier of the tool it is handed before the call. execute_tool is annotated destructive, open-world and not idempotent for that reason, and run_code is annotated the same way. Elaichi treats annotations as untrusted hints for clients. The server's own scope and restriction checks are the gates, and the actual tier is re-derived at call time.

## Read next

- [How MCP tool restriction enforcement works, end to end](/blog/what-a-tool-restriction-looks-like-on-the-wire/) — Follow one rule through MCP tool restriction enforcement in Elaichi, from the admin who writes it to the final check on the resolved outbound URL.
- [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.
- [Roles, Restrictions and ACLs in MCP: Three Layers](/blog/roles-restrictions-acls-three-mechanics/) — Roles restrictions and ACLs in MCP are three layers: a role says what a member may do, a restriction which tools they reach, an ACL who may use one connection.
