Team Access
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"
| Field | Description |
|---|---|
groups | IdP 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. |
servers | MCP servers (datasources) this binding covers. |
scopes | Optional 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. |
decisionMode | Allow, Audit, Warn, RequireApproval, or Deny — see decision mode mapping below. |
conditions | Optional 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
| Point | What happens |
|---|---|
| Token issuance | When 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 call | The 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:
decisionMode | Resulting decision |
|---|---|
Allow | Allow |
Audit | Audit |
Warn | Warn |
RequireApproval | Held (routed to the Approvals queue) |
Deny | Deny — 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.