MCP tool surface
An MCP tool surface is the set of tools a connection is shown by tools/list, computed from that connection's scopes, so a capability the grant does not carry is never named to the model at all.
Most permission checks on an agent run late. The model reads the mail, decides to
call a send tool, and a policy layer refuses. Everything before the refusal already
happened, including the part where a paragraph of hostile text talked the model into
sending. A tool surface moves the line earlier: the grant decides what tools/list
returns, and tools/list is the only thing the model ever learns about.
Six tools, and the scope each one costs
The server exposes exactly six tools. There is no seventh, no dynamic registration and no administrative tool.
| Tool | Required scope |
|---|---|
list_inboxes | inboxes:read |
list_threads | threads:read |
list_messages | messages:read |
get_message | messages:read |
create_draft | drafts:write |
send_message | messages:send |
A tool appears only when the grant holds every scope it requires. The three
connection presets therefore produce three different surfaces: read sees four
tools, read_draft sees five, and only read_draft_send sees six. Scope breadth and
tool surface are not the same measurement, and the tool surface is the one the model
can act on.
Why an absent tool beats a denied call
A denied call is a conversation. An absent tool is not. A denial is a message the model receives, interprets and plans around: it can rephrase the arguments, try a different recipient, or ask the human for more access. Refusals are an input, and every input on an agent’s path is something an attacker gets to influence. Removing the tool removes the input.
Two details make this a boundary rather than a presentation choice.
The filter runs again on the way in. tools/call recomputes the visible set from the
grant’s scopes and compares the requested name against it before any argument is
parsed, so a filtered name is refused with permission_denied and never becomes a
tool invocation at all.
The scopes are re-read rather than remembered. The access token is a lookup handle,
not a claim set, so a grant narrowed in the console narrows the tool list on the next
tools/list and the tool set on the next call, with no cached-token window.
Two asymmetries worth knowing
Visibility and execution are checked against different scopes. create_draft becomes
visible on drafts:write, while the service behind it requires drafts:submit to
write anything. The presets always grant those together so the gap never shows in
normal use, and it remains a real property of the design: appearing in tools/list
is necessary to call a tool and not sufficient to succeed with it.
events:read sits in all three presets and no tool consumes it. A connected agent
cannot poll the event cursor over MCP however you scope it, so the inbound half of
the loop stays on the REST path. That is one of three capabilities the surface
deliberately lacks, alongside uploading attachment bytes and managing webhooks, and
the MCP tool surface as a permission
boundary works through what each absence
costs.
What the surface does not solve
It does not make the content safe. The model still reads untrusted text from strangers, and a message that persuades it to summarise a thread misleadingly needs no tool at all. The surface bounds what the model can do, not what it can be told.
It does not bound the result size in the way a paginating client would. A tool result over 131,072 characters is replaced with an error pointing at the scoped REST API, which is a cliff rather than a page boundary.
And it is not reachable with an API key. The /mcp endpoint rejects efa_ keys with
HTTP 401 and a WWW-Authenticate: Bearer realm="mcp" challenge, so the surface only
ever exists inside a connection a human authorised. How that fits the rest of the
integration story is on the integrations page.