Governance · Govern agent actions with enterprise identity
Govern agent actions with enterprise identity
The full authorization path from the identity provider to a single MCP tool call: how every allow-or-deny decision is made and recorded.
An agent connects to an MCP server and asks to call create_refund. Before that call executes, the platform answers two questions: who is driving this session, and is that person allowed to make this exact call?
Most gateways answer the first question with a shared service account and the second with a server-level allowlist, which leaves security asking “who issued this refund, and why were they allowed to?” with no better answer than “the agent used the service account.” This guide walks the full authorization path on the platform, from the identity provider down to the disposition of a single tool.
Authenticate agent sessions through the IdP
Section titled “Authenticate agent sessions through the IdP”Private MCP servers reject anonymous traffic. When an agent connects without credentials, the server responds with a 401 and a WWW-Authenticate header pointing at OAuth protected-resource metadata, the standard MCP authorization handshake. The agent’s client (Claude, Cursor, or a custom harness) starts an OAuth flow, and the person behind the agent signs in through the company’s identity provider.
With Directory Sync connected, the MCP gateway gets a rich set of identity metadata that stays current:
- Users, groups, and attributes flow in from Okta or Entra and stay fresh.
- Role assignments track group membership, so someone joining a team gets the matching access automatically.
- When someone is deprovisioned, their access disappears with them instead of outliving them as a stale grant.
Every session established through a supported agent then carries a detailed identity profile: this user, in this organization, holding these roles.
Authorize the connection to an MCP server
Section titled “Authorize the connection to an MCP server”With identity resolved, the first authorization gate fires when the session attaches to a server: the caller must hold the mcp:connect scope for that specific server.
Permissions are grants: a scope paired with a selector that says which resources it reaches. Roles are named bundles of grants. The built-in Admin role holds every scope; the built-in Member role holds the read-and-connect subset, and custom roles narrow further. A grant that lets a role connect to one server in one project looks like this:
{ "scope": "mcp:connect", "selectors": [ { "resource_kind": "mcp", "resource_id": "<server-id>", "project_id": "<project-id>" } ]}Selectors support explicit wildcards ("resource_id": "*" reaches every server), so a role can be as broad or as narrow as the team it describes. A grant only constrains the keys it names: pin project_id and the grant follows the project; leave it off and the grant applies everywhere the scope does.
If no grant satisfies the check, the session never attaches. The agent doesn’t see a degraded server; it sees a denied connection, tied to the person whose session it was.
Authorize each tool call
Section titled “Authorize each tool call”Connecting to a server is not the same as being allowed to use everything on it. Each individual tools/call passes through a second check (same mcp:connect scope, same server) that carries two more dimensions: the tool name and the tool’s disposition.
Disposition comes from a tool’s MCP annotations. Tools declaring readOnlyHint evaluate as read_only; destructiveHint maps to destructive; the rest of the vocabulary is idempotent and open_world. Because the disposition describes what a tool does, policy can target classes of behavior instead of a per-tool list that goes stale every deploy.
Where those annotations are read from depends on the backend, and the difference matters for who controls policy:
- On a built server, annotations are captured on the tool definition when a deployment is processed.
- On a remote or tunneled server, annotations are the ones an administrator recorded on the server’s Inspect tab, stored by the Control Plane and read from there on every call. The upstream response is not consulted on the hot path, so a third party cannot change the disposition its tools evaluate under by editing its own annotations. See Recording tool metadata.
A remote server with no recorded metadata resolves to the empty disposition. Grants naming a disposition still match it, because selector matching skips keys the check does not carry, so recording metadata is what makes disposition-based policy bite on a remote server.
A support role that can connect to the billing server but only invoke tools that declare themselves read-only looks like this:
{ "scope": "mcp:connect", "selectors": [ { "resource_kind": "mcp", "resource_id": "<billing-server-id>", "disposition": "read_only" } ]}With that grant, get_invoice succeeds and create_refund (a destructive tool on the same server) is denied, for this user, on this call, with no change to the server itself. Add a tool key instead to name specific tools rather than classes of them.
How an allow-or-deny decision evaluates
Section titled “How an allow-or-deny decision evaluates”When a check fires, the engine evaluates it in four steps:
- Load the caller’s grants. Everything the principal holds, through their synced role and any direct grants, is assembled for the organization.
- Expand scopes. Higher privileges satisfy lower ones:
mcp:writesatisfies anmcp:readcheck, and either satisfiesmcp:connect. The same family never needs to be granted three times. - Match selectors. For every key a grant constrains, the value must equal the check’s value or be
*. Keys the grant doesn’t mention are skipped, which is why a grant scoped to"disposition": "read_only"still satisfies the connection-level check that doesn’t ask about disposition. - Apply denies. Deny rules evaluate as a separate pass with stricter matching, and a matching deny always beats a matching allow.
That last step is the safety property worth dwelling on. When multiple grants touch the same resource, the outcome isn’t order-dependent or “last rule wins”: deny wins. Suppose the Member role grants mcp:connect on every server, and a deny rule exists for destructive tools on the billing server. A member’s session connects fine and reads fine, and the moment the agent reaches for create_refund, the deny matches and the allow doesn’t matter. “Allowed” never happens by accident because two rules overlapped.
Audit every decision
Section titled “Audit every decision”The hardest question in access control is retrospective: not “what can this person do” but “why was this specific call allowed three weeks ago.”
Every check described above can be recorded as an authorization challenge that captures:
- Who was challenged, as a directory identity rather than a token id.
- What scope was required, against which server and tool.
- Whether the outcome was allow or deny.
- How many grants were evaluated to reach that outcome.
Because tool-call telemetry is stamped with the caller’s directory snapshot (their attributes, groups, and role slugs at the moment of the call), the record composes: this person, in these groups, holding these roles, made this call, and here is the rule that permitted it.
Denied challenges are actionable from the Roles & Permissions page: opening a challenge leads into the grant flow, where the missing permission can be added to one of the member’s roles, scoped as narrowly as needed.
Rolling out
Directory Sync and identity-stamped telemetry are available now. Authorization challenge logging is gated per organization; contact the team to have it enabled.
Get started
Section titled “Get started”Directory Sync is configured from the IDP and SSO page through the admin portal. Once connected, roles track the IdP automatically, every session resolves to a real person, and every tool call evaluates against grants that can be read, reasoned about, and audited.
For the announcement and product context, see the blog post From your IdP to a tool call: how agent authorization works.