Skip to main content

Team Access

Concepts

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
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.
subjectsAdditional 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.
serversMCP servers (datasources) this binding covers.
toolSelectorsOptional 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.
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. With toolSelectors set, the mode applies to the selected tools only.
conditionsOptional CEL expressions that must also evaluate to true, each with its own denial message.
inputConditionsOptional CEL conditions on the request — typically tool arguments (object.tool.arguments). A violation always denies, regardless of the binding's decision mode.
outputConditionsOptional 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.
rateLimitsOptional 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

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.

The binding editor shows the compiled policies live, and each synthesized policy has a predictable name you'll see in audit events:

Policy nameWhat 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>:toolsThe decision mode scoped to the binding's tool selectors
mcpaccessbinding:<binding>:conditions / :scopesCEL condition refinements and scope narrowing
mcpaccessbinding:<binding>:input / :outputInput (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