Skip to main content

Team Access

Concepts

Access Bindings map an identity provider team (group) to one or more MCP servers, with a decision mode and optional CEL conditions — the common "which teams can use which servers" question, without hand-writing policy for every server.

What an Access Binding is

An Access Binding is a first-class object, not a policy you write by hand:

kind: MCPAccessBinding
metadata:
name: platform-team-k8s
spec:
groups: ["okta:eng-platform", "okta:sre"]
servers: ["k8s-mcp", "github-mcp"]
scopes: ["mcp.read", "mcp.write"]
decisionMode: Allow
conditions:
- expression: 'object.agent.authContext.amr.exists(m, m == "mfa")'
message: "MFA required for platform tools"
FieldDescription
groupsIdP team/group references this binding applies to, e.g. okta:eng-platform. Shown as Teams in the UI — the field is named groups because it references your identity provider's groups directly.
serversMCP servers (datasources) this binding covers.
scopesOptional narrowing of allowed tool scopes. Enforced at both token issuance and per tool call — a request whose scope exceeds the binding's allowed scopes is denied.
decisionModeAllow, Audit, Warn, RequireApproval, or Deny — see decision mode mapping below.
conditionsOptional CEL expressions that must also evaluate to true, each with its own denial message.

Default is no access

A server has no access until a binding says otherwise. Creating the first binding for a server doesn't loosen anything that was previously open — it's the mechanism that grants access in the first place. A team with no binding covering (team, server) is denied by default, not allowed.

Where bindings enforce

PointWhat happens
Token issuanceWhen a developer's tool requests a Cross-App Access token for a server, AIControls checks whether any binding covers (the caller's team, that server), and — if the binding sets scopes — that the requested scope doesn't exceed it. No covering binding, or a scope request that exceeds it, means the request fails with invalid_grant before a token is ever minted.
Per tool callThe binding's decisionMode, conditions, and scopes are evaluated alongside your existing policies on every call to a bound server.

Because the check happens at token issuance, an unauthorized team is stopped at the earliest possible point — it never ends up holding a token it can't use.

Decision mode mapping

decisionMode isn't a separate enforcement engine — each value maps directly onto the same decisions every other AIControls policy produces:

decisionModeResulting decision
AllowAllow
AuditAudit
WarnWarn
RequireApprovalHeld (routed to the Approvals queue)
DenyDeny — invalid_grant at issuance, denied at the tool call

See Decision Types for what each of these means downstream.

Compiled to CEL, not a second engine

Every binding compiles internally to the same CEL evaluation the policy engine already runs for every other request — bindings are a structured way to express a common access pattern, not a parallel access-control system. Editing a binding in the UI is equivalent to editing the CEL rule it compiles to; there's exactly one place a request's fate is decided.

Audit trail

Every audit event for a call to a bound server records binding_name — the specific binding that permitted or governed it — alongside the existing grant_jti for the Cross-App Access grant that authorized the call. See Audit Log.

See also