Team Access
Access Bindings map who (a team, user, or agent) may use what (whole MCP servers, or individual tools across servers) — with a decision mode, optional input/output conditions, and rate limits — without hand-writing policy for the common cases.
What an Access Binding is
An Access Binding is a first-class object, not a policy you write by hand:
kind: MCPAccessBinding
metadata:
name: data-science-github-read
spec:
groups: ["okta:data-science"]
subjects:
- type: agentLabel
key: env
value: prod
servers: ["github-mcp"]
toolSelectors:
- servers: ["github-mcp"]
tools: ["get_issue", "list_pull_requests"]
- servers: ["*"]
tools: ["read_*"]
inputConditions:
- expression: 'string(object.tool.arguments.path).startsWith("/workspace/")'
message: "path outside workspace root"
outputConditions:
- expression: '!object.mcp.response.hasPII'
message: "PII detected in tool response"
rateLimits:
- calls: 100
window: 1h
per: agent
onExceed: deny
decisionMode: Allow
| 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. |
subjects | Additional principals beyond IdP teams: team, group, user (email), agent (ID, trailing * glob allowed), or agentLabel (key + value). Subjects and groups are OR-combined — matching any one of them makes the binding apply. |
servers | MCP servers (datasources) this binding covers. |
toolSelectors | Optional narrowing to individual tools, across servers. Each selector names servers (or "*" for all) and tools — exact names or simple globs like read_*. A binding with no selectors grants the whole server, exactly as before. |
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. With toolSelectors set, the mode applies to the selected tools only. |
conditions | Optional CEL expressions that must also evaluate to true, each with its own denial message. |
inputConditions | Optional CEL conditions on the request — typically tool arguments (object.tool.arguments). A violation always denies, regardless of the binding's decision mode. |
outputConditions | Optional CEL conditions evaluated on the tool's response (object.mcp.response.* — Access Bindings compile against MCP tool responses only). A violation denies the response before it reaches the agent. |
rateLimits | Optional call-rate limits — see Rate limits below. |
Tool-level grants
Tool selectors answer the granular question — "this team may use get_issue and list_pull_requests, nothing else on that server" — and the cross-server one — "this agent may use any read_* tool anywhere":
- Tool names are the per-server names shown in the tool inventory, never a prefixed form — renaming a server's tool prefix cannot orphan a grant.
deny-mode selectors always win: if one binding grants a tool and another denies it for the same caller, the call is denied.- An
allow-mode binding with selectors doesn't emit a blocking rule of its own — its grants feed the server's unlisted-tool posture below.
Unlisted-tool posture (default deny)
Each MCP server has a tool default action — what happens when a tool has no explicit grant:
allow(default): tools not named in any binding remain reachable, subject to your other policies.deny: only tools granted by an enabled binding are reachable; everything else on that server is denied with a clear message. Granting a whole server (a binding with no selectors, or a selector with tools"*") satisfies the posture for all its tools.
Set it per server — see Control MCP tool access. Combined with the server-level rule below, this gives a full default-deny-then-allowlist configuration.
Seeing effective access
Two complementary views compose bindings, tool selectors, and posture into a single answer, so you never have to work it out by hand:
- Setup → Settings → Access Bindings → Access Matrix — pick one identity (team/user/agent/label) and see its effective allow/deny across every server and tool.
- Inventory → MCP Servers → (a server's) Access tab — pick one server and see every identity's effective allow/deny across its tools.
Both read the same bindings, toolsets, and posture, so a grant reads identically in either view.
Rate limits
A binding can cap how often its tools are called: calls per window (1m, 1h, or 1d), counted per user, agent, team, label, or all (everyone combined), optionally scoped by a selector to a subset of the binding's tools. Counts are shared across all AIControls replicas, so the limit is exact fleet-wide. On exceed, onExceed picks the response: deny rejects the call (HTTP 429, audited with the policy name binding-rate-limit:<binding>), warn lets it through and raises a rate_limit_exceeded notification, or require-approval parks each over-limit call in the Approvals queue ("approve to burst"). See Rate-limit MCP tools.
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.
The binding editor shows the compiled policies live, and each synthesized policy has a predictable name you'll see in audit events:
| Policy name | What it enforces |
|---|---|
mcpaccessbinding-gate:<server> | The per-server default: callers matching no binding for the server are denied |
mcpaccessbinding:<binding> | The binding's own decision mode (whole-server bindings) |
mcpaccessbinding:<binding>:tools | The decision mode scoped to the binding's tool selectors |
mcpaccessbinding:<binding>:conditions / :scopes | CEL condition refinements and scope narrowing |
mcpaccessbinding:<binding>:input / :output | Input (request) and output (response) condition refinements |
mcptooldefault:<server> | The server's unlisted-tool deny posture |
binding-rate-limit:<binding> | A rate-limit denial (enforced by the counter engine, named like a policy for audit traceability) |
Precedence
When several rules apply to one call, the most restrictive outcome wins: an explicit deny beats any allow; RequireApproval beats Warn beats Allow. Rate-limit denials and the unlisted-tool posture deny independently of decision modes, and a drifted or quarantined tool is blocked regardless of any grant.
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
- Create a Team Access Binding
- Control MCP tool access
- Toolsets — reusable tool sets you attach to bindings
- Rate-limit MCP tools
- MCP Governance
- Policy Context
- Link Your Enterprise Identity