A security reviewer asks two questions about any AI assistant you let near company systems. The first: who decided this person may run that tool. The second: where the record of that decision sits. MCP itself answers neither question. The specification covers tool discovery and execution, not identity, authorization, or logging. That gap is why mcp role-based access control usually ends up hand-written inside every server a team runs, duplicated with small variations each time.
This post keeps two registers separate. Claims about MCP the protocol are marked (spec). Everything unmarked describes how Elaichi implements access control and audit logging on top of it. The spec-level claims generalize to any MCP deployment. Read the Elaichi-specific claims as a reference design, not a standard.
MCP (Model Context Protocol) is the standard way an AI assistant calls tools in other apps (spec) MCP standardizes how a client finds a tool and runs it: tools/list and tools/call over a transport. It says nothing about who may run a given tool. It says nothing about how a caller's identity maps to permissions, or what gets written down afterward. Those gaps are what the rest of this post covers.
What does MCP role-based access control actually decide?
Three things, at three different layers, and most broken designs come from collapsing them into one:
- Role: what a member may do inside the control plane (console actions, not tool calls).
- Restriction: which connectors and tools that role, or that specific member, may reach over MCP.
- OAuth scope: what one connected client is allowed to do with the grant a person issued it. Scope can only narrow the role, never widen it.
Elaichi's permissions are grouped into roles. A member holds exactly one role per organization, and a write that asks for zero roles or two is rejected. A member's permissions are their role's permissions. To combine two personas, say a team lead who also reviews billing, an admin authors a custom role. System roles cannot be stacked on one person.
The six system roles form a strict chain, each tier inheriting everything below it:
Guest < Member < Team Admin < People Admin < Org Admin < Org Owner
Billing Admin and Auditor sit off that chain as narrow, single-purpose personas. Both are free seats. Adding a compliance reviewer as an Auditor adds no billable seat. That matters when you are scoping a review against SOC 2 or ISO 27001 and do not want headcount cost blocking it. Elaichi's certifications and reports are listed in the Trust Center.
One permission decides whether MCP works at all for a role: tool:execute. A role without it gets an empty tool list over MCP, not a partial list. Every call is refused in-band. The message states that reconnecting will not help, because the gap is a role property rather than a scope issue. Guest, Billing Admin and Auditor do not hold tool:execute.
Above both sits the OAuth scope. A person ticks scopes on Elaichi's consent screen when they connect a client. The grant the client then holds gates which tools surface to it, not which identity claims it receives. It is the outermost layer, and it can only subtract, never add.
No consent checkbox holds a client to read-only access. "Use your connected apps" covers reads, writes and deletes alike. "Delete things" covers only Elaichi's own deletes, not a connected app's data. To hold a client to reads only, pick read-only toolboxes at consent time. The other way is to block the write and delete tools with a restriction rule. The three access layers are separated here in more detail.
Why does native MCP leave this to each server?
Because the spec covers discovery and execution, not identity mapping (spec). A server author receives a bearer token and decides, unilaterally, how to turn it into a person, a role, and a yes-or-no.
That's tolerable for one server. It stops being tolerable across a fleet of ten or fifty. Each server carries its own token validation, its own role table and its own log format. A single rule change, like revoking a contractor's write access, means a separate deploy to each one. Nothing guarantees the logic is equivalent across them.
The harder half is the servers you don't own. Vendors increasingly run their own MCP servers directly, and a growing share of connectors in any catalog fall into that category. You cannot inject a permission check inside someone else's service. If a client calls a vendor's MCP server directly, that call never passes through anything you control. Your role table and your audit log have nothing to say about it.
How do roles and restrictions cover a mixed connector catalog?
By sitting in front of both kinds of connector rather than inside either. Elaichi's catalog holds 650+ connectors, split two ways:
- Elaichi-run connectors: Elaichi authors and operates the MCP server.
- Native MCP connectors: the vendor builds and runs its own MCP server. Datadog, Linear, GitLab, PostHog and Miro are examples. Elaichi handles sign-in, access and audit in front of it.
A team on Gold can also bring its own remote MCP server in as a connector, under the same rule engine.
A restriction rule has four parts. The mode is allow or block. The target is one role or one member, never the whole organization and never a team. To cover a team, write the rule against the role its members hold. The last two parts are lists of whole connectors and of single tools.
How the two kinds of rule combine, stated precisely because this is where most misconfigurations happen:
- All allow rules at a layer merge into one allowlist.
- All block rules at a layer merge into one blocklist.
- A block always beats an allow, at any layer.
- A member is governed by their role's rules plus any rules aimed at them personally.
- The personal layer can only narrow what the role allows: it cannot grant back something the role already blocks.
Two behaviors catch people out often enough to warrant their own writeups. First, an allow rule that names nothing blocks everything for its target: an empty allowlist means allow nothing, not allow all. Second, blocks match a tool's name while allows match its stored operation. The two list types aren't symmetric, so a rule that looks like the mirror image of another may not behave like one.
The same rule engine applies to a native MCP connector. A remote tool appears to a rule as the resource mcp, with the upstream tool name as its method. Restrictions and scopes are evaluated before the call leaves Elaichi's infrastructure, and only a tool on that connection's active list can run. Elaichi does not write or maintain the vendor's tools; it governs access to them. Running a vendor's MCP server through Elaichi walks through the setup.
Timing differs by change type. A role, team, restriction, or access-grant change reaches every surface about two minutes. By contrast, removing or suspending a member revokes every live grant in the same transaction as the membership change.
When a restriction refuses a call, the caller is told where the block comes from: their role, or a rule aimed at them personally. The refusal also says an admin can lift it. Tracing one call through the resolver shows how the two layers combine. Any member can file an access request, and filing itself requires no permission. Only a person in the Elaichi console can approve or deny one, under Governance, then Access requests. That takes member:manage plus a fresh step-up identity check. No AI surface, including Elaichi's own, can decide a request. Approving one records a per-person access grant rather than editing the role's rules. Grants and role definitions can drift apart over time if nobody reviews them. No screen or API lists or revokes an individual access grant. A grant approved in the console carries no expiry date. If you approve a time-boxed exception, walking it back is your responsibility; the system will not do it for you.
How does a client discover a tool it may not run?
Through search, with the refusal visible rather than silent. In Elaichi, connected tools are never listed one by one, however few there are. A model finds tools via search_tools and runs them via read_tool (for a read) or execute_tool (for anything else).
search_tools indexes only the tools the caller may run. It then ranks the caller's restricted tools against the same query, separately. Those matches come back by name, flagged restricted, with no description, no input schema and no way to invoke them, plus a count. A connector blocked in its entirety shows as a single row for the connector, not one row per hidden tool. The model learns what exists and what it cannot have. For an agent deciding whether to ask a human for access, that is more useful than a tool that never appears.
Calling a restricted tool name directly does not return "Unknown tool", a response indistinguishable from a typo or a tool that never existed. It returns a restriction refusal that states how to request access. A self-hosted MCP server would need its own code to filter the tool list per caller. Here the search index applies the same restriction rules to whoever is asking. That falls out of how search is built, rather than a filter bolted on afterwards.
What does an audit row for a tool call carry?
One row per connected-tool call that reaches execution, whether it then succeeds, fails, or is refused at that point. A refusal before execution writes nothing.
Each row records:
- the acting member, and the toolbox the call went through;
- the tool's advertised name and its catalog name, which can differ;
- the connector, and the specific connection the call reached;
- the tool's classification as read, write or delete;
- whether approval was required, and how it was given;
- the calling surface, plus the MCP client's id when that surface is
mcp; - the outcome, and an error code on failure.
The row stores the MCP client's id, not its name, and the name is resolved when the log is read. The outcome is succeeded or failed. Approval is recorded in two fields. approval_required says whether the call's gate asked at all. approval_source says how it was given.
surface is the field reviewers reach for first. It is one of console, api, mcp or app. A call from Claude is distinguishable from one a person made in the console. Elaichi marks a client verified only when every non-loopback redirect address it registered maps to a known client. Seven clients currently qualify for verification; every other client shows the name it self-reported, flagged accordingly. Any client can claim any name it wants. The log records that the name was self-reported rather than trusting it.
The row also records what the call touched: the resource type, a title and a link to the object. The id comes from the single URL path argument that names the object. Beyond that one path argument, no argument values are recorded: neither argument names nor an argument count are logged either. Titles are never retained for a person's own record or for a message body. An email address is never recorded as an id or a title. The originating prompt is not in the row at all. That is a deliberate boundary, not an omission. The audit log can answer what the system did, not what the user asked for.
The log is append-only. Elaichi's internal log client can insert and query. The audit API has one read route, and nothing that edits or deletes an entry. Every member can read their own activity plus activity on connections they own or can edit. audit:view, held by Org Owner, Org Admin and Auditor, widens that to the whole organization. What an AI agent audit log must capture works through the fields a reviewer will ask for, in more depth.
What the audit log will not tell you
Four specific gaps. State them to a reviewer before they find them on their own. This is the honest version of the limitations section.
- A call refused before execution writes no row. A tool a restriction withheld, a missing OAuth scope, an unknown tool name, and a rate limit all fail before any executor runs. "Every denied call is logged" is a false claim about this system. Do not make it to a reviewer. Listing tools and searching them are also not audit entries.
- The audit log is eventually consistent, not real-time. Events reach the log store through a queue; a new entry can take a moment to appear. Do not build a synchronous "block until logged" control on top of it.
- Filtering by category is non-obvious. Connected-tool calls sit in the
assistantcategory, not themcpone. Describe the row by what it records rather than by an action name: the member, the tool, the connection, how approval was given, and the outcome. Themcpcategory holds grant events, not tool calls. A reviewer filtering for "MCP activity" by category alone will miss every tool call. - Retention is 90 days, flat. For longer history, forward events to your own log service: export to your own Datadog comes with the Black plan. Datadog is the destination that currently ships. Audit log retention for AI actions covers the gap between a 90-day default and a seven-year retention obligation. Solve that mismatch before an audit, not during one.
How do Claude, ChatGPT and Cursor share one sign-in?
They all point at the same address, https://api.elaichi.ai/mcp, identical for every organization. The bearer token behind the connection decides the organization, the person and the tool set. There is no per-user or per-team URL to issue, track or revoke. Each person connects their client once and approves their own grant. Every call made with that grant acts as them. Permissions are resolved per request, not frozen into the token.
That shape also settles where tool-level control has to live, and the point reaches beyond Elaichi. A client-side control that gates by tool name sees only a few names: execute_tool, read_tool, run_code, and the two Elaichi operations that run tools by proxy. It acts on each as a whole. It cannot single out one connected app inside that bucket, and gating execute_tool alone leaves read_tool and the proxy paths open. Per-app and per-tool control has to live in a server-side restriction layer, not in client settings. Any MCP deployment relying solely on client-side tool-permission toggles has this same blind spot, not just Elaichi's.
The client's own approval prompt still matters, for a narrower job. Claude's connector tool permissions take one of three settings: Always allow, Needs approval or Blocked. Each applies to a whole connector, a category of its tools, or a single tool (Anthropic's connectors documentation, checked October 2026). Elaichi appears to Claude as one connector. That setting governs the connector as a whole, not the tools behind it. Per-tool enforcement has to happen server-side.
Elaichi has no per-call approval step over MCP. The grant is the approval, chosen once on Elaichi's consent screen for a named client. Any per-call confirmation a person sees is their own client's prompt, not Elaichi's. A reviewer may otherwise assume Elaichi adds a second approval layer on top of the client's.
Who sponsors a shared connection?
A named person, always: there is no org-owned or service-account connection in this model. Teams share access to one connected account by pinning it into a toolbox and sharing the toolbox. The person who pins the connection is recorded as that entry's delegator. They must hold use on the connection before they can pin it.
Members running tools through a shared toolbox gain no direct reach to the underlying connection, only to the tool calls the toolbox exposes. The caller's own restrictions still apply, evaluated for the calling person rather than the delegator. A member blocked from a tool cannot reach it through a shared toolbox. Authentication to the vendor's API comes from the pinned connection's credentials; authorization comes from the caller's role and restrictions. These are deliberately two separate checks, not one collapsed into the other.
If the delegator loses use on that connection, the toolbox entry resolves as unmet for everyone, the toolbox's owner included. The same happens when the delegator stops being an active member. It stays listed but is not advertised or runnable until someone with edit on the toolbox and use on the connection re-pins it. This is intentional. Shared access has a named sponsor. When the sponsor leaves or is deprovisioned, access stops rather than outliving them on an orphaned credential. MCP governance covers the wider shape of this model.
Protecting the governance surface itself
Changing a role or removing a member changes what every AI client that person holds may do. Those actions sit behind step-up confirmation in the console, not just a signed-in session. Changing a member's role, deleting a custom role or a team, and removing a member all require the admin to re-confirm it is them. So do resolving an access request and creating or updating an SSO connection. The confirmation comes immediately before the action runs.
Elaichi lists the confirmation methods a person may use, strongest first. A passkey comes first, then an authenticator app code. For most actions, an emailed six-digit code is the last resort. A governance confirmation covers the next ten minutes in that session. Credential and account actions, such as minting an API token, ask every time.
Organization API tokens are never asked to step up, and that cuts both ways. A token performs member removal, role changes, role and team deletion, invitations, and domain, SSO-connection and group-mapping changes with no step-up at all. What it cannot do is reach the routes that need an interactive sign-in session. Those are resolving access requests, managing API tokens and SCIM tokens, two-factor and passkeys, approving an MCP client's consent request, and deleting the organization. Scope your tokens accordingly, because a leaked one can rewrite most of the governance baseline. Every confirmation attempt, successful or not, is recorded as auth.step_up_succeeded or auth.step_up_failed, naming the specific action it gated.
When a control plane is the wrong shape
Be honest about this before a reviewer forces the question. With a hosted control plane like Elaichi, tool-call metadata leaves your infrastructure. That means the actor, the tool, the connector and a resource id, not the payloads. It lives in Elaichi's log store for up to 90 days, and exporting it beyond that is your responsibility. The single shared sign-in URL is also a single dependency. If api.elaichi.ai is unreachable, every client using it loses access at once, not just one connector. That is the trade for one role, restriction and audit layer instead of per-server logic duplicated everywhere. Weigh it against the alternatives rather than assuming hosted is better.
If nothing may leave your own infrastructure under any circumstance, a hosted plane is the wrong answer regardless of feature set. Lunar.dev positions itself as an enterprise MCP gateway. Its FAQ states that MCPX is fully self-hosted inside the customer's infrastructure (lunar.dev, checked November 2026). That is a different trade. You run and patch the gateway yourself, and you own the uptime. Residency is something you prove directly rather than take on a vendor's word.
Say every tool your agents need is already a REST API behind an API gateway you operate. Start there instead of adding a new control plane. Tyk's documentation describes Tyk Gateway sitting in front of MCP servers and Tyk-managed REST APIs. It proxies MCP traffic through a centrally managed layer you already run (Tyk's MCP Gateway documentation, checked October 2026). A connector catalog buys you less when you already own both the client-facing gateway and the backend APIs it talks to.
If two people use one app each, with no compliance obligation attached, none of this is worth standing up yet. The cost of maintaining roles, restrictions and a reviewed audit trail outweighs the risk at that scale. Roles and audit rows earn their keep once people, apps and clients start moving independently of each other. In practice that is somewhere past a handful of each.
The order to do it in
- Decide roles before tools. One role per persona, since a member holds exactly one. Author a custom role only where two personas genuinely overlap in practice. Designing roles for AI agents walks through that first pass.
- Write restrictions against the roles you just named. Then confirm where they are enforced, rather than assuming a single choke point. Elaichi checks them when listing connectors, when connecting an account, when advertising tools through search, at execution, and on the outbound request to the vendor. Where restriction enforcement actually sits lists all of them.
- Read the log for a week before trusting the design. Check that its gaps match your compliance obligations before you rely on it as a system of record. Start by reading which tools your agents call.
Browse the connector catalog for the apps your teams already run. The governance use case shows what a finished setup looks like.