Manage Access with Policies
Per-key allowed_tools grants work well for a handful of callers, but a fleet of hundreds of API keys turns every new MCP server or tool into a mass key edit. MCP access policies lift the grant one or two levels up: configure it once on the environment or on a team, and member keys inherit it — including servers and tools registered later.
This guide explains the policy layers and how they combine, how to configure them, how to migrate existing keys, and how to inspect the access a key ends up with.
How Policies Combine
Three layers participate, from broadest to narrowest:
- Environment default policy — the base grant for keys in the environment.
- Team entitlement — a team-level grant that replaces the environment default for the team's keys, in every environment of the organization.
- Key
mcp_accessblock — how one key participates: inherit the grant, narrow it, or opt out.
A key's effective access is resolved per request:
base grant = team entitlement if the key's team has one, else the environment default
effective = (base grant ∩ key restriction) − every applicable deny pattern
Two rules keep the model predictable:
- A key can narrow the inherited grant, never widen it. A
restrictkey intersects its own patterns with the base grant; patterns outside the base add nothing. - Deny always wins. Deny patterns from the environment policy, the team entitlement, and the key are all subtracted — an environment-level deny holds even when a team entitlement replaces the environment's grant, and it also applies to keys that still use an explicit
allowed_toolslist.
Keys without an mcp_access block keep the exact per-key allowlist behavior: their allowed_tools list is the whole allow side, policies never widen them, and only policy deny patterns overlay them. Upgrading cannot silently grant access.
All patterns use the same <server>__<tool> naming and single-* glob matching as allowed_tools.
Set the Environment Default Policy
The examples on this page use the AISIX Cloud Admin API. Follow the AISIX On-Premises Quickstart to run the stack, create an admin token with write scope, and create an environment, then set:
export AISIX_CP="http://localhost:8080/api"
export AISIX_TOKEN="aisix_pat_YOUR_ADMIN_TOKEN"
export ENV_ID="YOUR_ENVIRONMENT_ID"
A policy has one of three modes:
| Mode | Grants |
|---|---|
none | No MCP tools. |
selected | Exactly the tools matched by the allow patterns. |
all | Every tool on every registered MCP server — including servers and tools registered later. |
Save the environment default:
curl -sS -X PUT "$AISIX_CP/environments/$ENV_ID/mcp_policy" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mode": "selected",
"allow": ["github__*", "runbooks__search"],
"deny": ["github__delete_repository"]
}'
❶ all is always an explicit choice, never a default — because it covers future tools, a newly registered destructive tool would immediately be available to every inheriting key.
❷ Deny patterns apply to every key in the environment, including keys still using an explicit allowed_tools list.
In the dashboard, the same editor lives under Environment → MCP Access.
Grant a Team Entitlement
A team entitlement is configured once per team and applies to the team's caller API keys in every environment of the organization. When a team is synced from an identity provider group (SCIM directory sync), group membership changes propagate automatically — no per-key edits.
export TEAM_ID="YOUR_TEAM_ID"
curl -sS -X PUT "$AISIX_CP/teams/$TEAM_ID/entitlements" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mcp": { "mode": "selected", "allow": ["postgres__*"] }
}'
The entitlement replaces the environment default for member keys — it does not union with it. Send "mcp": null to remove the entitlement; member keys fall back to each environment's default policy.
Choose a Key's Participation
A key's mcp_access block selects how it combines with the policies:
| Mode | Behavior |
|---|---|
inherit | Use the inherited grant unchanged. |
restrict | Intersect the key's own allow patterns with the inherited grant, and subtract its deny patterns. |
deny | No MCP tool access at all. |
curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/api_keys" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"display_name": "inheriting-caller",
"allowed_models": [],
"mcp_access": { "mode": "inherit" }
}'
A restrict key that should reach only one server of a broader grant:
"mcp_access": {
"mode": "restrict",
"allow": ["github__*"],
"deny": ["github__delete_repository"]
}
Omit mcp_access entirely to keep the key on its explicit allowed_tools allowlist. Send "mcp_access": null on an update to return a policy-managed key to that behavior.
Migrate Existing Keys
Existing keys keep their explicit allowed_tools behavior until they are switched to inherit — a policy never widens them implicitly. The migration is a previewed, one-shot batch:
Preview the impact first. The response counts affected keys and lists per-key pattern changes (up to 50 samples, changed keys first):
curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/mcp_policy/preview" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
{
"preview": {
"total_keys": 214,
"legacy_keys": 209,
"policy_managed_keys": 5,
"keys_gaining": 187,
"keys_losing": 12,
"keys_unchanged": 10,
"samples": [
{ "id": "…", "display_name": "ci-bot", "mode": "legacy", "gained": ["github__*"], "lost": [] }
]
}
}
Send a prospective policy in the body ({"policy": {…}}) to evaluate it before saving it. The comparison is pattern-level: it reports granted patterns, not an expansion into concrete tool names.
Then switch the keys:
curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/mcp_policy/apply" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Every key still using an explicit allowlist flips to mcp_access: {"mode": "inherit"} (send key_ids to switch a subset). Each key's previous allowed_tools value is kept, so a key can be reverted individually by clearing its mcp_access. After the migration, registering a new MCP server or tool requires no key edits at all.
Inspect a Key's Effective Access
Every allow and deny pattern in the answer carries the level that contributed it, so an unexpected grant or refusal is traceable:
curl -sS "$AISIX_CP/environments/$ENV_ID/api_keys/$API_KEY_ID/effective_permissions" \
-H "Authorization: Bearer $AISIX_TOKEN"
{
"effective_permissions": {
"mcp": {
"key_mode": "inherit",
"base_source": "team_policy",
"all_tools": false,
"allow": [ { "pattern": "postgres__*", "source": "team_policy" } ],
"deny": [ { "pattern": "github__delete_repository", "source": "env_policy" } ]
}
}
}
key_mode: legacy marks a key still governed by its explicit allowed_tools. The dashboard shows the same view from the key's row on the API Keys page.
Operational Notes
- A policy can carry
"enabled": falseor an"expires_at"timestamp; a disabled or expired policy neither grants nor denies anything, and keys fall back to the next applicable layer. - Deleting the environment default while keys are in
inheritmode leaves those keys (outside entitled teams) with no MCP access — the resolution is fail-closed, never fail-open. - Enforcement is unchanged from per-key allowlists: unauthorized tools are filtered from
tools/listand rejected ontools/callbefore any upstream routing, with the same neutral error. - Every policy write, entitlement change, and batch migration is recorded in the audit log.