Back to blog
Product

From your IdP to a tool call: how agent authorization works

Tiago Zien-Mendes

Tiago Zien-Mendes

July 19, 2026 · 7 min read

From your IdP to a tool call: how agent authorization works

An agent connects to one of your MCP servers and asks to call create_refund. Before that call executes, Speakeasy has to answer 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.”

Most gateways
Who’s driving?
One shared service account for every agent
Allowed to make this call?
A server-level allowlist, the same for everyone
Who can answer later?
”The agent used the service account”
Speakeasy
Who’s driving?
A real user, signed in through your IdP
Allowed to make this call?
Per-tool policy, evaluated by the tool’s disposition
Who can answer later?
The named user and the exact rule that allowed it

How an agent session authenticates through your IdP

Private MCP servers on Speakeasy 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. That causes the agent’s client (Claude, Cursor, a custom harness) to start an OAuth flow, and the person behind the agent is able to sign in through the company’s identity provider.

Directory sync means Speakeasy’s MCP gateways gets a rich set of 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.

For sessions established via popular AI agents, you have a detailed identity profile available: this user, in this organization, holding these roles.

How connecting to an MCP server is authorized

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 in Speakeasy 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. 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.

How each MCP tool call is authorized

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 is derived from the tool’s MCP annotations at call time. Tools declaring readOnlyHint evaluate as read_only; destructiveHint maps to destructive; the rest of the vocabulary is idempotent and open_world. Because the disposition comes from the tool’s own metadata, you can write policy about classes of behavior instead of maintaining a per-tool list that goes stale every deploy.

Here’s a support role that can connect to the billing server but only invoke tools that declare themselves read-only:

{
"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 if you want to name specific tools rather than classes of them.

How an allow-or-deny decision evaluates

When a check fires, the engine evaluates it in four steps:

  1. Load the caller’s grants. Everything the principal holds (through their synced role and any direct grants) is assembled for the organization.
  2. Expand scopes. Higher privileges satisfy lower ones: mcp:write satisfies an mcp:read check, and either satisfies mcp:connect. You never have to grant the same family three times.
  3. 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 exactly why a grant scoped to "disposition": "read_only" still satisfies the connection-level check that doesn’t ask about disposition.
  4. 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 (and in any real organization they will), the outcome isn’t order-dependent or “last rule wins.” It’s deny wins, the same safe default behind network-level access. Suppose the member role grants mcp:connect on every server, and you add a deny rule 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, so “allowed” never happens by accident because two rules overlapped.

How every decision becomes an audit record

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.

That closes the loop in both directions. When access was allowed and someone asks why, the answer is in the record, not in someone’s memory of how the roles were configured back then. When access is denied and shouldn’t have been, an admin sees the challenge, sees exactly which scope was missing, and grants the right role straight from the review, with no guessing about which permission to add.

Rolling out

Directory sync and identity-stamped telemetry are available now. Authorization challenge logging is gated per organization; reach out to have it enabled for yours.

Get started

Directory sync is configured from the Identity section of your org settings through the WorkOS admin portal. Once it’s connected, roles track your IdP automatically, every session resolves to a real person, and every tool call (from mcp:connect down to the disposition of a single tool) evaluates against grants you can read, reason about, and audit.


Need to prove who did what across your agents? Book time with our team and we’ll walk through it with you.

Last updated on

AI everywhere.