refactor(mcp): resolve tool access as three intersecting layers - #992

Merged
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection
Aug 19, 2026
Merged

refactor(mcp): resolve tool access as three intersecting layers#992
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

MCP tool access was resolved through two different mechanisms glued together by a mode field: an mcp_access.mode of inherit/restrict/deny on the key, plus a legacy state (no block at all) where the key's allowed_tools was its whole grant and the policies could not reach it. A team policy replaced the environment grant rather than narrowing it, and allowed_tools and mcp_access.allow were two fields expressing the same thing, with mode deciding which one counted. The result was four key states, three policy modes, and a per-environment "migrate legacy keys" batch operation in the control plane to move keys between them.

This collapses all of it into one rule. Three layers of identical shape contribute to the ACL — the environment policy, the key's team policy, and the key's own mcp_access block — each carrying allow and deny pattern lists. Allow sides intersect, deny sides union, and a layer that is absent (no row, disabled, or no block on the key) imposes no constraint. With no layer present at all the grant is empty, so MCP access is still granted explicitly and never by the absence of configuration.

What that removes:

  • McpPolicy.mode (none/selected/all) — all is allow: ["*"], none is allow: []
  • McpAccess.mode (inherit/restrict/deny) — inheriting is what a key does when it has no block, narrowing is what its allow list does, and denying everything is allow: []
  • ApiKey.allowed_tools — the key's layer is mcp_access.allow

allow is now required on every layer, so a layer that only means to subtract tools has to spell its allow side ["*"] instead of silently granting nothing. A policy or key block carrying only deny is a schema error, not a lockout.

Behaviour changes beyond the field shapes: a team policy now narrows the environment layer instead of replacing it, so it can no longer grant a tool the environment does not; and a key with no mcp_access block now follows the policy layers instead of standing outside them.

The control-plane half (schema, projection, dashboard, and the removal of the legacy-key migration flow) follows in a paired PR.

Environment policy, team policy and the key's own block now carry the
same allow/deny shape and combine by intersecting allow and unioning
deny. A team policy narrows the environment layer instead of replacing
it, and a key that configures nothing adds no constraint.
Drops the mode machinery this replaces: McpPolicy.mode, McpAccess.mode
and the key's allowed_tools field. A layer that grants nothing spells
its allow side [], and one that only subtracts spells it ["*"]; allow
is required on every layer so neither is reachable by omission. With no
layer present at all the grant is empty, so MCP access stays granted
explicitly.
Rewrites the access-policy e2e around the three-layer contract: a key
with no block of its own, a team layer narrowing the environment, a
wide-open key that still cannot widen, all three layers intersecting,
an empty allow list blocking everything, and deletion of the last layer
dropping to no access rather than to everything.
@nic-6443
nic-6443 requested a lite review from CopilotAugust 19, 2026 06:15

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in:44 minutes

Limit details: You’ve used all 2 included reviews currently available. Your 55 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83bfa42a-63a0-4aea-aaf4-d2f2f0fc639e

📥 Commits

Reviewing files that changed from the base of the PR and between 4a98a83 and 0967c7c.

📒 Files selected for processing (24)
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/models/apikey.rs
  • crates/aisix-core/src/models/mcp_policy.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-gateway/src/upstream_headers.rs
  • crates/aisix-mcp/src/gateway.rs
  • crates/aisix-mcp/tests/gateway_aggregation.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/passthrough_route.rs
  • schemas/resources/api_key.schema.json
  • schemas/resources/mcp_policy.schema.json
  • tests/e2e/src/cases/mcp-access-policy-e2e.test.ts
  • tests/e2e/src/cases/mcp-anonymous-access-e2e.test.ts
  • tests/e2e/src/cases/mcp-cleartext-credential-warn-e2e.test.ts
  • tests/e2e/src/cases/mcp-guardrail-e2e.test.ts
  • tests/e2e/src/cases/mcp-openapi-e2e.test.ts
  • tests/e2e/src/cases/mcp-scoped-endpoint-e2e.test.ts
  • tests/e2e/src/cases/mcp-server-ratelimit-e2e.test.ts
  • tests/e2e/src/cases/passthrough-model-acl-e2e.test.ts
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 7a9fc86 into mainAug 19, 2026
13 checks passed
@jarvis9443
jarvis9443 deleted the refactor/mcp-acl-three-layer-intersection branch August 19, 2026 06:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

refactor(mcp): resolve tool access as three intersecting layers - #992

Merged
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection
Aug 19, 2026
Merged

refactor(mcp): resolve tool access as three intersecting layers#992
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

MCP tool access was resolved through two different mechanisms glued together by a mode field: an mcp_access.mode of inherit/restrict/deny on the key, plus a legacy state (no block at all) where the key's allowed_tools was its whole grant and the policies could not reach it. A team policy replaced the environment grant rather than narrowing it, and allowed_tools and mcp_access.allow were two fields expressing the same thing, with mode deciding which one counted. The result was four key states, three policy modes, and a per-environment "migrate legacy keys" batch operation in the control plane to move keys between them.

This collapses all of it into one rule. Three layers of identical shape contribute to the ACL — the environment policy, the key's team policy, and the key's own mcp_access block — each carrying allow and deny pattern lists. Allow sides intersect, deny sides union, and a layer that is absent (no row, disabled, or no block on the key) imposes no constraint. With no layer present at all the grant is empty, so MCP access is still granted explicitly and never by the absence of configuration.

What that removes:

  • McpPolicy.mode (none/selected/all) — all is allow: ["*"], none is allow: []
  • McpAccess.mode (inherit/restrict/deny) — inheriting is what a key does when it has no block, narrowing is what its allow list does, and denying everything is allow: []
  • ApiKey.allowed_tools — the key's layer is mcp_access.allow

allow is now required on every layer, so a layer that only means to subtract tools has to spell its allow side ["*"] instead of silently granting nothing. A policy or key block carrying only deny is a schema error, not a lockout.

Behaviour changes beyond the field shapes: a team policy now narrows the environment layer instead of replacing it, so it can no longer grant a tool the environment does not; and a key with no mcp_access block now follows the policy layers instead of standing outside them.

The control-plane half (schema, projection, dashboard, and the removal of the legacy-key migration flow) follows in a paired PR.

Environment policy, team policy and the key's own block now carry the
same allow/deny shape and combine by intersecting allow and unioning
deny. A team policy narrows the environment layer instead of replacing
it, and a key that configures nothing adds no constraint.
Drops the mode machinery this replaces: McpPolicy.mode, McpAccess.mode
and the key's allowed_tools field. A layer that grants nothing spells
its allow side [], and one that only subtracts spells it ["*"]; allow
is required on every layer so neither is reachable by omission. With no
layer present at all the grant is empty, so MCP access stays granted
explicitly.
Rewrites the access-policy e2e around the three-layer contract: a key
with no block of its own, a team layer narrowing the environment, a
wide-open key that still cannot widen, all three layers intersecting,
an empty allow list blocking everything, and deletion of the last layer
dropping to no access rather than to everything.
@nic-6443
nic-6443 requested a lite review from CopilotAugust 19, 2026 06:15

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in:44 minutes

Limit details: You’ve used all 2 included reviews currently available. Your 55 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83bfa42a-63a0-4aea-aaf4-d2f2f0fc639e

📥 Commits

Reviewing files that changed from the base of the PR and between 4a98a83 and 0967c7c.

📒 Files selected for processing (24)
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/models/apikey.rs
  • crates/aisix-core/src/models/mcp_policy.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-gateway/src/upstream_headers.rs
  • crates/aisix-mcp/src/gateway.rs
  • crates/aisix-mcp/tests/gateway_aggregation.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/passthrough_route.rs
  • schemas/resources/api_key.schema.json
  • schemas/resources/mcp_policy.schema.json
  • tests/e2e/src/cases/mcp-access-policy-e2e.test.ts
  • tests/e2e/src/cases/mcp-anonymous-access-e2e.test.ts
  • tests/e2e/src/cases/mcp-cleartext-credential-warn-e2e.test.ts
  • tests/e2e/src/cases/mcp-guardrail-e2e.test.ts
  • tests/e2e/src/cases/mcp-openapi-e2e.test.ts
  • tests/e2e/src/cases/mcp-scoped-endpoint-e2e.test.ts
  • tests/e2e/src/cases/mcp-server-ratelimit-e2e.test.ts
  • tests/e2e/src/cases/passthrough-model-acl-e2e.test.ts
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 7a9fc86 into mainAug 19, 2026
13 checks passed
@jarvis9443
jarvis9443 deleted the refactor/mcp-acl-three-layer-intersection branch August 19, 2026 06:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(mcp): resolve tool access as three intersecting layers - #992

Merged
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection
Aug 19, 2026
Merged

refactor(mcp): resolve tool access as three intersecting layers#992
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

MCP tool access was resolved through two different mechanisms glued together by a mode field: an mcp_access.mode of inherit/restrict/deny on the key, plus a legacy state (no block at all) where the key's allowed_tools was its whole grant and the policies could not reach it. A team policy replaced the environment grant rather than narrowing it, and allowed_tools and mcp_access.allow were two fields expressing the same thing, with mode deciding which one counted. The result was four key states, three policy modes, and a per-environment "migrate legacy keys" batch operation in the control plane to move keys between them.

This collapses all of it into one rule. Three layers of identical shape contribute to the ACL — the environment policy, the key's team policy, and the key's own mcp_access block — each carrying allow and deny pattern lists. Allow sides intersect, deny sides union, and a layer that is absent (no row, disabled, or no block on the key) imposes no constraint. With no layer present at all the grant is empty, so MCP access is still granted explicitly and never by the absence of configuration.

What that removes:

  • McpPolicy.mode (none/selected/all) — all is allow: ["*"], none is allow: []
  • McpAccess.mode (inherit/restrict/deny) — inheriting is what a key does when it has no block, narrowing is what its allow list does, and denying everything is allow: []
  • ApiKey.allowed_tools — the key's layer is mcp_access.allow

allow is now required on every layer, so a layer that only means to subtract tools has to spell its allow side ["*"] instead of silently granting nothing. A policy or key block carrying only deny is a schema error, not a lockout.

Behaviour changes beyond the field shapes: a team policy now narrows the environment layer instead of replacing it, so it can no longer grant a tool the environment does not; and a key with no mcp_access block now follows the policy layers instead of standing outside them.

The control-plane half (schema, projection, dashboard, and the removal of the legacy-key migration flow) follows in a paired PR.

Environment policy, team policy and the key's own block now carry the
same allow/deny shape and combine by intersecting allow and unioning
deny. A team policy narrows the environment layer instead of replacing
it, and a key that configures nothing adds no constraint.
Drops the mode machinery this replaces: McpPolicy.mode, McpAccess.mode
and the key's allowed_tools field. A layer that grants nothing spells
its allow side [], and one that only subtracts spells it ["*"]; allow
is required on every layer so neither is reachable by omission. With no
layer present at all the grant is empty, so MCP access stays granted
explicitly.
Rewrites the access-policy e2e around the three-layer contract: a key
with no block of its own, a team layer narrowing the environment, a
wide-open key that still cannot widen, all three layers intersecting,
an empty allow list blocking everything, and deletion of the last layer
dropping to no access rather than to everything.
@nic-6443
nic-6443 requested a lite review from CopilotAugust 19, 2026 06:15

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in:44 minutes

Limit details: You’ve used all 2 included reviews currently available. Your 55 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83bfa42a-63a0-4aea-aaf4-d2f2f0fc639e

📥 Commits

Reviewing files that changed from the base of the PR and between 4a98a83 and 0967c7c.

📒 Files selected for processing (24)
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/models/apikey.rs
  • crates/aisix-core/src/models/mcp_policy.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-gateway/src/upstream_headers.rs
  • crates/aisix-mcp/src/gateway.rs
  • crates/aisix-mcp/tests/gateway_aggregation.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/passthrough_route.rs
  • schemas/resources/api_key.schema.json
  • schemas/resources/mcp_policy.schema.json
  • tests/e2e/src/cases/mcp-access-policy-e2e.test.ts
  • tests/e2e/src/cases/mcp-anonymous-access-e2e.test.ts
  • tests/e2e/src/cases/mcp-cleartext-credential-warn-e2e.test.ts
  • tests/e2e/src/cases/mcp-guardrail-e2e.test.ts
  • tests/e2e/src/cases/mcp-openapi-e2e.test.ts
  • tests/e2e/src/cases/mcp-scoped-endpoint-e2e.test.ts
  • tests/e2e/src/cases/mcp-server-ratelimit-e2e.test.ts
  • tests/e2e/src/cases/passthrough-model-acl-e2e.test.ts
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 7a9fc86 into mainAug 19, 2026
13 checks passed
@jarvis9443
jarvis9443 deleted the refactor/mcp-acl-three-layer-intersection branch August 19, 2026 06:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(mcp): resolve tool access as three intersecting layers - #992

Merged
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection
Aug 19, 2026
Merged

refactor(mcp): resolve tool access as three intersecting layers#992
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

MCP tool access was resolved through two different mechanisms glued together by a mode field: an mcp_access.mode of inherit/restrict/deny on the key, plus a legacy state (no block at all) where the key's allowed_tools was its whole grant and the policies could not reach it. A team policy replaced the environment grant rather than narrowing it, and allowed_tools and mcp_access.allow were two fields expressing the same thing, with mode deciding which one counted. The result was four key states, three policy modes, and a per-environment "migrate legacy keys" batch operation in the control plane to move keys between them.

This collapses all of it into one rule. Three layers of identical shape contribute to the ACL — the environment policy, the key's team policy, and the key's own mcp_access block — each carrying allow and deny pattern lists. Allow sides intersect, deny sides union, and a layer that is absent (no row, disabled, or no block on the key) imposes no constraint. With no layer present at all the grant is empty, so MCP access is still granted explicitly and never by the absence of configuration.

What that removes:

  • McpPolicy.mode (none/selected/all) — all is allow: ["*"], none is allow: []
  • McpAccess.mode (inherit/restrict/deny) — inheriting is what a key does when it has no block, narrowing is what its allow list does, and denying everything is allow: []
  • ApiKey.allowed_tools — the key's layer is mcp_access.allow

allow is now required on every layer, so a layer that only means to subtract tools has to spell its allow side ["*"] instead of silently granting nothing. A policy or key block carrying only deny is a schema error, not a lockout.

Behaviour changes beyond the field shapes: a team policy now narrows the environment layer instead of replacing it, so it can no longer grant a tool the environment does not; and a key with no mcp_access block now follows the policy layers instead of standing outside them.

The control-plane half (schema, projection, dashboard, and the removal of the legacy-key migration flow) follows in a paired PR.

Environment policy, team policy and the key's own block now carry the
same allow/deny shape and combine by intersecting allow and unioning
deny. A team policy narrows the environment layer instead of replacing
it, and a key that configures nothing adds no constraint.
Drops the mode machinery this replaces: McpPolicy.mode, McpAccess.mode
and the key's allowed_tools field. A layer that grants nothing spells
its allow side [], and one that only subtracts spells it ["*"]; allow
is required on every layer so neither is reachable by omission. With no
layer present at all the grant is empty, so MCP access stays granted
explicitly.
Rewrites the access-policy e2e around the three-layer contract: a key
with no block of its own, a team layer narrowing the environment, a
wide-open key that still cannot widen, all three layers intersecting,
an empty allow list blocking everything, and deletion of the last layer
dropping to no access rather than to everything.
@nic-6443
nic-6443 requested a lite review from CopilotAugust 19, 2026 06:15

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in:44 minutes

Limit details: You’ve used all 2 included reviews currently available. Your 55 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83bfa42a-63a0-4aea-aaf4-d2f2f0fc639e

📥 Commits

Reviewing files that changed from the base of the PR and between 4a98a83 and 0967c7c.

📒 Files selected for processing (24)
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/models/apikey.rs
  • crates/aisix-core/src/models/mcp_policy.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-gateway/src/upstream_headers.rs
  • crates/aisix-mcp/src/gateway.rs
  • crates/aisix-mcp/tests/gateway_aggregation.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/passthrough_route.rs
  • schemas/resources/api_key.schema.json
  • schemas/resources/mcp_policy.schema.json
  • tests/e2e/src/cases/mcp-access-policy-e2e.test.ts
  • tests/e2e/src/cases/mcp-anonymous-access-e2e.test.ts
  • tests/e2e/src/cases/mcp-cleartext-credential-warn-e2e.test.ts
  • tests/e2e/src/cases/mcp-guardrail-e2e.test.ts
  • tests/e2e/src/cases/mcp-openapi-e2e.test.ts
  • tests/e2e/src/cases/mcp-scoped-endpoint-e2e.test.ts
  • tests/e2e/src/cases/mcp-server-ratelimit-e2e.test.ts
  • tests/e2e/src/cases/passthrough-model-acl-e2e.test.ts
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 7a9fc86 into mainAug 19, 2026
13 checks passed
@jarvis9443
jarvis9443 deleted the refactor/mcp-acl-three-layer-intersection branch August 19, 2026 06:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

refactor(mcp): resolve tool access as three intersecting layers - #992

Merged
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection
Aug 19, 2026
Merged

refactor(mcp): resolve tool access as three intersecting layers#992
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

MCP tool access was resolved through two different mechanisms glued together by a mode field: an mcp_access.mode of inherit/restrict/deny on the key, plus a legacy state (no block at all) where the key's allowed_tools was its whole grant and the policies could not reach it. A team policy replaced the environment grant rather than narrowing it, and allowed_tools and mcp_access.allow were two fields expressing the same thing, with mode deciding which one counted. The result was four key states, three policy modes, and a per-environment "migrate legacy keys" batch operation in the control plane to move keys between them.

This collapses all of it into one rule. Three layers of identical shape contribute to the ACL — the environment policy, the key's team policy, and the key's own mcp_access block — each carrying allow and deny pattern lists. Allow sides intersect, deny sides union, and a layer that is absent (no row, disabled, or no block on the key) imposes no constraint. With no layer present at all the grant is empty, so MCP access is still granted explicitly and never by the absence of configuration.

What that removes:

  • McpPolicy.mode (none/selected/all) — all is allow: ["*"], none is allow: []
  • McpAccess.mode (inherit/restrict/deny) — inheriting is what a key does when it has no block, narrowing is what its allow list does, and denying everything is allow: []
  • ApiKey.allowed_tools — the key's layer is mcp_access.allow

allow is now required on every layer, so a layer that only means to subtract tools has to spell its allow side ["*"] instead of silently granting nothing. A policy or key block carrying only deny is a schema error, not a lockout.

Behaviour changes beyond the field shapes: a team policy now narrows the environment layer instead of replacing it, so it can no longer grant a tool the environment does not; and a key with no mcp_access block now follows the policy layers instead of standing outside them.

The control-plane half (schema, projection, dashboard, and the removal of the legacy-key migration flow) follows in a paired PR.

Environment policy, team policy and the key's own block now carry the
same allow/deny shape and combine by intersecting allow and unioning
deny. A team policy narrows the environment layer instead of replacing
it, and a key that configures nothing adds no constraint.
Drops the mode machinery this replaces: McpPolicy.mode, McpAccess.mode
and the key's allowed_tools field. A layer that grants nothing spells
its allow side [], and one that only subtracts spells it ["*"]; allow
is required on every layer so neither is reachable by omission. With no
layer present at all the grant is empty, so MCP access stays granted
explicitly.
Rewrites the access-policy e2e around the three-layer contract: a key
with no block of its own, a team layer narrowing the environment, a
wide-open key that still cannot widen, all three layers intersecting,
an empty allow list blocking everything, and deletion of the last layer
dropping to no access rather than to everything.
@nic-6443
nic-6443 requested a lite review from CopilotAugust 19, 2026 06:15

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in:44 minutes

Limit details: You’ve used all 2 included reviews currently available. Your 55 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83bfa42a-63a0-4aea-aaf4-d2f2f0fc639e

📥 Commits

Reviewing files that changed from the base of the PR and between 4a98a83 and 0967c7c.

📒 Files selected for processing (24)
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/models/apikey.rs
  • crates/aisix-core/src/models/mcp_policy.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-gateway/src/upstream_headers.rs
  • crates/aisix-mcp/src/gateway.rs
  • crates/aisix-mcp/tests/gateway_aggregation.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/passthrough_route.rs
  • schemas/resources/api_key.schema.json
  • schemas/resources/mcp_policy.schema.json
  • tests/e2e/src/cases/mcp-access-policy-e2e.test.ts
  • tests/e2e/src/cases/mcp-anonymous-access-e2e.test.ts
  • tests/e2e/src/cases/mcp-cleartext-credential-warn-e2e.test.ts
  • tests/e2e/src/cases/mcp-guardrail-e2e.test.ts
  • tests/e2e/src/cases/mcp-openapi-e2e.test.ts
  • tests/e2e/src/cases/mcp-scoped-endpoint-e2e.test.ts
  • tests/e2e/src/cases/mcp-server-ratelimit-e2e.test.ts
  • tests/e2e/src/cases/passthrough-model-acl-e2e.test.ts
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 7a9fc86 into mainAug 19, 2026
13 checks passed
@jarvis9443
jarvis9443 deleted the refactor/mcp-acl-three-layer-intersection branch August 19, 2026 06:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(mcp): resolve tool access as three intersecting layers - #992

Merged
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection
Aug 19, 2026
Merged

refactor(mcp): resolve tool access as three intersecting layers#992
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

MCP tool access was resolved through two different mechanisms glued together by a mode field: an mcp_access.mode of inherit/restrict/deny on the key, plus a legacy state (no block at all) where the key's allowed_tools was its whole grant and the policies could not reach it. A team policy replaced the environment grant rather than narrowing it, and allowed_tools and mcp_access.allow were two fields expressing the same thing, with mode deciding which one counted. The result was four key states, three policy modes, and a per-environment "migrate legacy keys" batch operation in the control plane to move keys between them.

This collapses all of it into one rule. Three layers of identical shape contribute to the ACL — the environment policy, the key's team policy, and the key's own mcp_access block — each carrying allow and deny pattern lists. Allow sides intersect, deny sides union, and a layer that is absent (no row, disabled, or no block on the key) imposes no constraint. With no layer present at all the grant is empty, so MCP access is still granted explicitly and never by the absence of configuration.

What that removes:

  • McpPolicy.mode (none/selected/all) — all is allow: ["*"], none is allow: []
  • McpAccess.mode (inherit/restrict/deny) — inheriting is what a key does when it has no block, narrowing is what its allow list does, and denying everything is allow: []
  • ApiKey.allowed_tools — the key's layer is mcp_access.allow

allow is now required on every layer, so a layer that only means to subtract tools has to spell its allow side ["*"] instead of silently granting nothing. A policy or key block carrying only deny is a schema error, not a lockout.

Behaviour changes beyond the field shapes: a team policy now narrows the environment layer instead of replacing it, so it can no longer grant a tool the environment does not; and a key with no mcp_access block now follows the policy layers instead of standing outside them.

The control-plane half (schema, projection, dashboard, and the removal of the legacy-key migration flow) follows in a paired PR.

Environment policy, team policy and the key's own block now carry the
same allow/deny shape and combine by intersecting allow and unioning
deny. A team policy narrows the environment layer instead of replacing
it, and a key that configures nothing adds no constraint.
Drops the mode machinery this replaces: McpPolicy.mode, McpAccess.mode
and the key's allowed_tools field. A layer that grants nothing spells
its allow side [], and one that only subtracts spells it ["*"]; allow
is required on every layer so neither is reachable by omission. With no
layer present at all the grant is empty, so MCP access stays granted
explicitly.
Rewrites the access-policy e2e around the three-layer contract: a key
with no block of its own, a team layer narrowing the environment, a
wide-open key that still cannot widen, all three layers intersecting,
an empty allow list blocking everything, and deletion of the last layer
dropping to no access rather than to everything.
@nic-6443
nic-6443 requested a lite review from CopilotAugust 19, 2026 06:15

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in:44 minutes

Limit details: You’ve used all 2 included reviews currently available. Your 55 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83bfa42a-63a0-4aea-aaf4-d2f2f0fc639e

📥 Commits

Reviewing files that changed from the base of the PR and between 4a98a83 and 0967c7c.

📒 Files selected for processing (24)
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/models/apikey.rs
  • crates/aisix-core/src/models/mcp_policy.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-gateway/src/upstream_headers.rs
  • crates/aisix-mcp/src/gateway.rs
  • crates/aisix-mcp/tests/gateway_aggregation.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/passthrough_route.rs
  • schemas/resources/api_key.schema.json
  • schemas/resources/mcp_policy.schema.json
  • tests/e2e/src/cases/mcp-access-policy-e2e.test.ts
  • tests/e2e/src/cases/mcp-anonymous-access-e2e.test.ts
  • tests/e2e/src/cases/mcp-cleartext-credential-warn-e2e.test.ts
  • tests/e2e/src/cases/mcp-guardrail-e2e.test.ts
  • tests/e2e/src/cases/mcp-openapi-e2e.test.ts
  • tests/e2e/src/cases/mcp-scoped-endpoint-e2e.test.ts
  • tests/e2e/src/cases/mcp-server-ratelimit-e2e.test.ts
  • tests/e2e/src/cases/passthrough-model-acl-e2e.test.ts
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 7a9fc86 into mainAug 19, 2026
13 checks passed
@jarvis9443
jarvis9443 deleted the refactor/mcp-acl-three-layer-intersection branch August 19, 2026 06:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

refactor(mcp): resolve tool access as three intersecting layers - #992

Merged
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection
Aug 19, 2026
Merged

refactor(mcp): resolve tool access as three intersecting layers#992
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

MCP tool access was resolved through two different mechanisms glued together by a mode field: an mcp_access.mode of inherit/restrict/deny on the key, plus a legacy state (no block at all) where the key's allowed_tools was its whole grant and the policies could not reach it. A team policy replaced the environment grant rather than narrowing it, and allowed_tools and mcp_access.allow were two fields expressing the same thing, with mode deciding which one counted. The result was four key states, three policy modes, and a per-environment "migrate legacy keys" batch operation in the control plane to move keys between them.

This collapses all of it into one rule. Three layers of identical shape contribute to the ACL — the environment policy, the key's team policy, and the key's own mcp_access block — each carrying allow and deny pattern lists. Allow sides intersect, deny sides union, and a layer that is absent (no row, disabled, or no block on the key) imposes no constraint. With no layer present at all the grant is empty, so MCP access is still granted explicitly and never by the absence of configuration.

What that removes:

  • McpPolicy.mode (none/selected/all) — all is allow: ["*"], none is allow: []
  • McpAccess.mode (inherit/restrict/deny) — inheriting is what a key does when it has no block, narrowing is what its allow list does, and denying everything is allow: []
  • ApiKey.allowed_tools — the key's layer is mcp_access.allow

allow is now required on every layer, so a layer that only means to subtract tools has to spell its allow side ["*"] instead of silently granting nothing. A policy or key block carrying only deny is a schema error, not a lockout.

Behaviour changes beyond the field shapes: a team policy now narrows the environment layer instead of replacing it, so it can no longer grant a tool the environment does not; and a key with no mcp_access block now follows the policy layers instead of standing outside them.

The control-plane half (schema, projection, dashboard, and the removal of the legacy-key migration flow) follows in a paired PR.

Environment policy, team policy and the key's own block now carry the
same allow/deny shape and combine by intersecting allow and unioning
deny. A team policy narrows the environment layer instead of replacing
it, and a key that configures nothing adds no constraint.
Drops the mode machinery this replaces: McpPolicy.mode, McpAccess.mode
and the key's allowed_tools field. A layer that grants nothing spells
its allow side [], and one that only subtracts spells it ["*"]; allow
is required on every layer so neither is reachable by omission. With no
layer present at all the grant is empty, so MCP access stays granted
explicitly.
Rewrites the access-policy e2e around the three-layer contract: a key
with no block of its own, a team layer narrowing the environment, a
wide-open key that still cannot widen, all three layers intersecting,
an empty allow list blocking everything, and deletion of the last layer
dropping to no access rather than to everything.
@nic-6443
nic-6443 requested a lite review from CopilotAugust 19, 2026 06:15

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in:44 minutes

Limit details: You’ve used all 2 included reviews currently available. Your 55 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83bfa42a-63a0-4aea-aaf4-d2f2f0fc639e

📥 Commits

Reviewing files that changed from the base of the PR and between 4a98a83 and 0967c7c.

📒 Files selected for processing (24)
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/models/apikey.rs
  • crates/aisix-core/src/models/mcp_policy.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-gateway/src/upstream_headers.rs
  • crates/aisix-mcp/src/gateway.rs
  • crates/aisix-mcp/tests/gateway_aggregation.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/passthrough_route.rs
  • schemas/resources/api_key.schema.json
  • schemas/resources/mcp_policy.schema.json
  • tests/e2e/src/cases/mcp-access-policy-e2e.test.ts
  • tests/e2e/src/cases/mcp-anonymous-access-e2e.test.ts
  • tests/e2e/src/cases/mcp-cleartext-credential-warn-e2e.test.ts
  • tests/e2e/src/cases/mcp-guardrail-e2e.test.ts
  • tests/e2e/src/cases/mcp-openapi-e2e.test.ts
  • tests/e2e/src/cases/mcp-scoped-endpoint-e2e.test.ts
  • tests/e2e/src/cases/mcp-server-ratelimit-e2e.test.ts
  • tests/e2e/src/cases/passthrough-model-acl-e2e.test.ts
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 7a9fc86 into mainAug 19, 2026
13 checks passed
@jarvis9443
jarvis9443 deleted the refactor/mcp-acl-three-layer-intersection branch August 19, 2026 06:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jarvis9443
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

refactor(mcp): resolve tool access as three intersecting layers - #992

Merged
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection
Aug 19, 2026
Merged

refactor(mcp): resolve tool access as three intersecting layers#992
jarvis9443 merged 2 commits into
mainfrom
refactor/mcp-acl-three-layer-intersection

Conversation

@jarvis9443

Copy link
Copy Markdown
Contributor

MCP tool access was resolved through two different mechanisms glued together by a mode field: an mcp_access.mode of inherit/restrict/deny on the key, plus a legacy state (no block at all) where the key's allowed_tools was its whole grant and the policies could not reach it. A team policy replaced the environment grant rather than narrowing it, and allowed_tools and mcp_access.allow were two fields expressing the same thing, with mode deciding which one counted. The result was four key states, three policy modes, and a per-environment "migrate legacy keys" batch operation in the control plane to move keys between them.

This collapses all of it into one rule. Three layers of identical shape contribute to the ACL — the environment policy, the key's team policy, and the key's own mcp_access block — each carrying allow and deny pattern lists. Allow sides intersect, deny sides union, and a layer that is absent (no row, disabled, or no block on the key) imposes no constraint. With no layer present at all the grant is empty, so MCP access is still granted explicitly and never by the absence of configuration.

What that removes:

  • McpPolicy.mode (none/selected/all) — all is allow: ["*"], none is allow: []
  • McpAccess.mode (inherit/restrict/deny) — inheriting is what a key does when it has no block, narrowing is what its allow list does, and denying everything is allow: []
  • ApiKey.allowed_tools — the key's layer is mcp_access.allow

allow is now required on every layer, so a layer that only means to subtract tools has to spell its allow side ["*"] instead of silently granting nothing. A policy or key block carrying only deny is a schema error, not a lockout.

Behaviour changes beyond the field shapes: a team policy now narrows the environment layer instead of replacing it, so it can no longer grant a tool the environment does not; and a key with no mcp_access block now follows the policy layers instead of standing outside them.

The control-plane half (schema, projection, dashboard, and the removal of the legacy-key migration flow) follows in a paired PR.

Environment policy, team policy and the key's own block now carry the
same allow/deny shape and combine by intersecting allow and unioning
deny. A team policy narrows the environment layer instead of replacing
it, and a key that configures nothing adds no constraint.
Drops the mode machinery this replaces: McpPolicy.mode, McpAccess.mode
and the key's allowed_tools field. A layer that grants nothing spells
its allow side [], and one that only subtracts spells it ["*"]; allow
is required on every layer so neither is reachable by omission. With no
layer present at all the grant is empty, so MCP access stays granted
explicitly.
Rewrites the access-policy e2e around the three-layer contract: a key
with no block of its own, a team layer narrowing the environment, a
wide-open key that still cannot widen, all three layers intersecting,
an empty allow list blocking everything, and deletion of the last layer
dropping to no access rather than to everything.
@nic-6443
nic-6443 requested a lite review from CopilotAugust 19, 2026 06:15

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in:44 minutes

Limit details: You’ve used all 2 included reviews currently available. Your 55 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83bfa42a-63a0-4aea-aaf4-d2f2f0fc639e

📥 Commits

Reviewing files that changed from the base of the PR and between 4a98a83 and 0967c7c.

📒 Files selected for processing (24)
  • crates/aisix-admin/src/apikeys_handlers.rs
  • crates/aisix-admin/src/lib.rs
  • crates/aisix-admin/src/openapi.rs
  • crates/aisix-admin/tests/etcd_integration.rs
  • crates/aisix-core/src/models/apikey.rs
  • crates/aisix-core/src/models/mcp_policy.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-gateway/src/upstream_headers.rs
  • crates/aisix-mcp/src/gateway.rs
  • crates/aisix-mcp/tests/gateway_aggregation.rs
  • crates/aisix-proxy/src/mcp.rs
  • crates/aisix-proxy/src/passthrough_route.rs
  • schemas/resources/api_key.schema.json
  • schemas/resources/mcp_policy.schema.json
  • tests/e2e/src/cases/mcp-access-policy-e2e.test.ts
  • tests/e2e/src/cases/mcp-anonymous-access-e2e.test.ts
  • tests/e2e/src/cases/mcp-cleartext-credential-warn-e2e.test.ts
  • tests/e2e/src/cases/mcp-guardrail-e2e.test.ts
  • tests/e2e/src/cases/mcp-openapi-e2e.test.ts
  • tests/e2e/src/cases/mcp-scoped-endpoint-e2e.test.ts
  • tests/e2e/src/cases/mcp-server-ratelimit-e2e.test.ts
  • tests/e2e/src/cases/passthrough-model-acl-e2e.test.ts
  • tests/e2e/src/cases/url-rewrite-e2e.test.ts

Comment @coderabbitai help to get the list of available commands.

@jarvis9443
jarvis9443 merged commit 7a9fc86 into mainAug 19, 2026
13 checks passed
@jarvis9443
jarvis9443 deleted the refactor/mcp-acl-three-layer-intersection branch August 19, 2026 06:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jarvis9443