feat(mcp): per-server wildcard in api_key MCP tool ACL - #709

Merged
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard
Jul 3, 2026
Merged

feat(mcp): per-server wildcard in api_key MCP tool ACL#709
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard

Conversation

@moonming

@moonmingmoonming commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

An API key's allowed_tools (per-tool MCP ACL, #668) matched only a bare "*" (every tool) or exact <server>__<tool> names. Since the discovered tool list isn't surfaced anywhere yet, an operator can't know the exact names — so the only usable grant today is all-or-nothing ("*").

This adds a per-server wildcard: entries are now matched as single-* globs (reusing the very same wildcard::wildcard_matches helper that allowed_models already uses), so "<server>__*" grants every tool on one server:

  • "github__*" permits github__create_issue, github__delete_repo
  • and does not leak across the boundary: slack__post_message and the prefix-sharing githubenterprise__create_issue are both denied.

A bare "*" and exact names keep their exact meaning. The change is purely additive — no existing grant shifts, because real composed tool names contain no *.

Why this shape

  • Reuses the model matcher.allowed_models already globs openai/*; tools now glob github__* through the identical helper, so the two ACLs stay semantically consistent and there's one source of truth.
  • Both matchers move together.ApiKey::can_access_tool (core) and the proxy-path ToolAcl::permits (aisix-mcp) both delegate to wildcard_matches, preserving their documented "mirror" relationship. ToolAcl::from_allowed still folds a bare "*" into AllowAll; a <server>__* becomes a scoped Allow.
  • Server granularity is the granularity an operator can actually express without a live tool list, and it maps directly onto the MCP servers they've already registered ("give this key the GitHub server's tools").

Tests

  • can_access_tool_enforces_namespaced_allowlist extended: github__* grants github tools, denies a different server and a prefix-sharing server name.
  • tool_acl_from_allowed_semantics: a per-server wildcard is a scoped Allow, not AllowAll.
  • tool_acl_per_server_wildcard_scopes_to_one_server (new, full gateway): alpha__* over two real upstreams exposes only alpha's tools in tools/list, calls alpha, and rejects beta (defense-in-depth) — exercises permits end-to-end.

cargo fmt / clippy -D warnings / aisix-core + aisix-mcp suites green; dump-schema regenerated api_key.schema.json (description only) and is idempotent.

Control-plane pairing

This exposes a richer matching semantics on an existing field; the field itself still needs to become reachable — the CP api_key resource has no allowed_tools today. The paired AISIX-Cloud PR adds allowed_tools to the api_key model/validation/projection + a dashboard tool-ACL picker that composes <server>__* grants from the org's registered MCP servers (plus a global-* toggle and an exact-name escape hatch). Refs AISIX-Cloud#894 (Phase 2, per-key tool ACL). The live individual-tool list (via a DP→CP report) is a separate fast-follow.

Summary by CodeRabbit

  • New Features

    • Tool access rules now support wildcard patterns, including server-scoped grants like <server>__*.
    • API keys can now allow all tools, a single tool, or all tools for one server using pattern-based access.
  • Bug Fixes

    • Tightened tool listing and tool call permissions so access is limited to the intended server or tool names.
    • Keys without tool अनुमति remain unable to access MCP tools unless explicitly granted.
  • Documentation

    • Updated access descriptions to clearly explain wildcard and exact-match behavior.

Independent audit response

A cold audit returned FIX-FIRST (no BLOCK): it traced the matcher character-by-character and confirmed github__* matches github tools while correctly rejecting the prefix-sharing githubenterprise__*, no privilege-widening for existing data, and that tools/list filter + tools/call reject both go through the same permits. Two MEDIUMs, both fixed in eb8ff95:

  • MEDIUM-1 — stale admin OpenAPI description. The hand-maintained allowed_tools descriptions in openapi.rs (request + public schemas) still carried the pre-wildcard wording, so GET /admin/openapi.json disagreed with the shipped behavior. Updated both to match the code doc and api_key.schema.json; the schema-guard tests now assert the <server>__* wording so future drift fails CI. (No separate generated artifact exists — openapi.rs is the source of truth; verified the generator propagates the new wording.)
  • MEDIUM-2 — leading/middle-* breadth untested. Entries are single-* globs anywhere, mirroring allowed_models — so "*__readonly" is a genuine any-server grant of a same-named tool. Pinned with a test (asserting cross-server match + suffix anchoring) and kept the "single-* globs, mirroring allowed_models" framing that states the general rule. This is intentional and consistent with the model ACL, not an accident.

LOWs: added a one-line note that can_access_tool is test-only (the live path is ToolAcl::permits); the literal-*-in-name and debug_assert! server-name notes are benign/pre-existing (no real grant flips, since composed tool names contain no *).

`allowed_tools` matched only a bare `"*"` (all tools) or exact
`<server>__<tool>` names. Without a live tool list an operator can't
know the exact names, so the only usable grant was all-or-nothing.
Match entries as single-`*` globs instead, reusing the same
`wildcard::wildcard_matches` helper `allowed_models` already uses, so
`"<server>__*"` grants every tool on one server (e.g. `"github__*"`
permits `github__create_issue` but not `slack__post` or
`githubenterprise__create_issue`). A bare `"*"` and exact names keep
their meaning; the change is purely additive (no existing grant shifts,
since real tool names contain no `*`). Both matchers move together —
`ApiKey::can_access_tool` and the proxy-path `ToolAcl::permits` — so
their documented "mirror" relationship holds.
This makes per-server tool governance reachable ahead of the live
tool-list report; the control-plane api_key `allowed_tools` field +
dashboard picker (which composes `<server>__*` from the org's
registered MCP servers) ship as the paired AISIX-Cloud PR.
@coderabbitai

coderabbitaiBot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Tool access authorization changes from exact-match/HashSet-based checks to glob-based wildcard matching using wildcard_matches, applied to both ApiKey::can_access_tool and ToolAcl::permits. Documentation and schema descriptions are updated accordingly, and tests are extended to cover per-server wildcard scoping.

Changes

Wildcard Tool ACL

Layer / File(s)Summary
ApiKey tool access glob matching
crates/aisix-core/src/models/apikey.rs
can_access_tool now uses wildcard_matches instead of exact/"*" checks; doc comment clarifies single-* glob semantics; test extended to verify server-scoped wildcard non-leakage.
Gateway ToolAcl glob matching
crates/aisix-mcp/src/gateway.rs, crates/aisix-mcp/tests/gateway_aggregation.rs
ToolAcl::permits replaces HashSet membership with wildcard_matches; docs updated; tests assert scoped Allow behavior and a new test validates per-server wildcard scoping in tools/list/tools/call.
Schema description update
schemas/resources/api_key.schema.json
allowed_tools schema description updated to document glob/wildcard matching semantics.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
E2e Test Quality Review✅ PassedPASS: tool_acl_per_server_wildcard_scopes_to_one_server uses real HTTP upstreams/gateway and validates list/call scoping; helpers are isolated and readable.
Security Check✅ PassedNo security regressions found: wildcard ACLs remain anchored and additive, secrets aren't logged or stored, and auth checks still short-circuit before routing.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: per-server wildcard support for MCP tool ACLs on API keys.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-tool-acl-wildcard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

- Update the admin OpenAPI `allowed_tools` description (request + public
schemas) to match the code doc and resource schema — it still carried
the pre-wildcard wording, so the served contract disagreed with the
shipped behavior. Tighten the schema-guard tests to assert the
`<server>__*` wording so future drift fails CI.
- Pin the leading/middle-`*` breadth with a test: entries are single-`*`
globs anywhere (like `allowed_models`), so `"*__readonly"` is a real
any-server grant of a same-named tool — documented as intentional, not
an accident.
- Note on `can_access_tool` that it's test-only; `ToolAcl::permits` is
the live enforcement path they mirror.
@moonming
moonming merged commit bba94b5 into mainJul 3, 2026
12 checks passed
@moonming
moonming deleted the feat/mcp-tool-acl-wildcard branch July 3, 2026 05:27
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.

1 participant

@moonming
, '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

feat(mcp): per-server wildcard in api_key MCP tool ACL - #709

Merged
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard
Jul 3, 2026
Merged

feat(mcp): per-server wildcard in api_key MCP tool ACL#709
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard

Conversation

@moonming

@moonmingmoonming commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

An API key's allowed_tools (per-tool MCP ACL, #668) matched only a bare "*" (every tool) or exact <server>__<tool> names. Since the discovered tool list isn't surfaced anywhere yet, an operator can't know the exact names — so the only usable grant today is all-or-nothing ("*").

This adds a per-server wildcard: entries are now matched as single-* globs (reusing the very same wildcard::wildcard_matches helper that allowed_models already uses), so "<server>__*" grants every tool on one server:

  • "github__*" permits github__create_issue, github__delete_repo
  • and does not leak across the boundary: slack__post_message and the prefix-sharing githubenterprise__create_issue are both denied.

A bare "*" and exact names keep their exact meaning. The change is purely additive — no existing grant shifts, because real composed tool names contain no *.

Why this shape

  • Reuses the model matcher.allowed_models already globs openai/*; tools now glob github__* through the identical helper, so the two ACLs stay semantically consistent and there's one source of truth.
  • Both matchers move together.ApiKey::can_access_tool (core) and the proxy-path ToolAcl::permits (aisix-mcp) both delegate to wildcard_matches, preserving their documented "mirror" relationship. ToolAcl::from_allowed still folds a bare "*" into AllowAll; a <server>__* becomes a scoped Allow.
  • Server granularity is the granularity an operator can actually express without a live tool list, and it maps directly onto the MCP servers they've already registered ("give this key the GitHub server's tools").

Tests

  • can_access_tool_enforces_namespaced_allowlist extended: github__* grants github tools, denies a different server and a prefix-sharing server name.
  • tool_acl_from_allowed_semantics: a per-server wildcard is a scoped Allow, not AllowAll.
  • tool_acl_per_server_wildcard_scopes_to_one_server (new, full gateway): alpha__* over two real upstreams exposes only alpha's tools in tools/list, calls alpha, and rejects beta (defense-in-depth) — exercises permits end-to-end.

cargo fmt / clippy -D warnings / aisix-core + aisix-mcp suites green; dump-schema regenerated api_key.schema.json (description only) and is idempotent.

Control-plane pairing

This exposes a richer matching semantics on an existing field; the field itself still needs to become reachable — the CP api_key resource has no allowed_tools today. The paired AISIX-Cloud PR adds allowed_tools to the api_key model/validation/projection + a dashboard tool-ACL picker that composes <server>__* grants from the org's registered MCP servers (plus a global-* toggle and an exact-name escape hatch). Refs AISIX-Cloud#894 (Phase 2, per-key tool ACL). The live individual-tool list (via a DP→CP report) is a separate fast-follow.

Summary by CodeRabbit

  • New Features

    • Tool access rules now support wildcard patterns, including server-scoped grants like <server>__*.
    • API keys can now allow all tools, a single tool, or all tools for one server using pattern-based access.
  • Bug Fixes

    • Tightened tool listing and tool call permissions so access is limited to the intended server or tool names.
    • Keys without tool अनुमति remain unable to access MCP tools unless explicitly granted.
  • Documentation

    • Updated access descriptions to clearly explain wildcard and exact-match behavior.

Independent audit response

A cold audit returned FIX-FIRST (no BLOCK): it traced the matcher character-by-character and confirmed github__* matches github tools while correctly rejecting the prefix-sharing githubenterprise__*, no privilege-widening for existing data, and that tools/list filter + tools/call reject both go through the same permits. Two MEDIUMs, both fixed in eb8ff95:

  • MEDIUM-1 — stale admin OpenAPI description. The hand-maintained allowed_tools descriptions in openapi.rs (request + public schemas) still carried the pre-wildcard wording, so GET /admin/openapi.json disagreed with the shipped behavior. Updated both to match the code doc and api_key.schema.json; the schema-guard tests now assert the <server>__* wording so future drift fails CI. (No separate generated artifact exists — openapi.rs is the source of truth; verified the generator propagates the new wording.)
  • MEDIUM-2 — leading/middle-* breadth untested. Entries are single-* globs anywhere, mirroring allowed_models — so "*__readonly" is a genuine any-server grant of a same-named tool. Pinned with a test (asserting cross-server match + suffix anchoring) and kept the "single-* globs, mirroring allowed_models" framing that states the general rule. This is intentional and consistent with the model ACL, not an accident.

LOWs: added a one-line note that can_access_tool is test-only (the live path is ToolAcl::permits); the literal-*-in-name and debug_assert! server-name notes are benign/pre-existing (no real grant flips, since composed tool names contain no *).

`allowed_tools` matched only a bare `"*"` (all tools) or exact
`<server>__<tool>` names. Without a live tool list an operator can't
know the exact names, so the only usable grant was all-or-nothing.
Match entries as single-`*` globs instead, reusing the same
`wildcard::wildcard_matches` helper `allowed_models` already uses, so
`"<server>__*"` grants every tool on one server (e.g. `"github__*"`
permits `github__create_issue` but not `slack__post` or
`githubenterprise__create_issue`). A bare `"*"` and exact names keep
their meaning; the change is purely additive (no existing grant shifts,
since real tool names contain no `*`). Both matchers move together —
`ApiKey::can_access_tool` and the proxy-path `ToolAcl::permits` — so
their documented "mirror" relationship holds.
This makes per-server tool governance reachable ahead of the live
tool-list report; the control-plane api_key `allowed_tools` field +
dashboard picker (which composes `<server>__*` from the org's
registered MCP servers) ship as the paired AISIX-Cloud PR.
@coderabbitai

coderabbitaiBot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Tool access authorization changes from exact-match/HashSet-based checks to glob-based wildcard matching using wildcard_matches, applied to both ApiKey::can_access_tool and ToolAcl::permits. Documentation and schema descriptions are updated accordingly, and tests are extended to cover per-server wildcard scoping.

Changes

Wildcard Tool ACL

Layer / File(s)Summary
ApiKey tool access glob matching
crates/aisix-core/src/models/apikey.rs
can_access_tool now uses wildcard_matches instead of exact/"*" checks; doc comment clarifies single-* glob semantics; test extended to verify server-scoped wildcard non-leakage.
Gateway ToolAcl glob matching
crates/aisix-mcp/src/gateway.rs, crates/aisix-mcp/tests/gateway_aggregation.rs
ToolAcl::permits replaces HashSet membership with wildcard_matches; docs updated; tests assert scoped Allow behavior and a new test validates per-server wildcard scoping in tools/list/tools/call.
Schema description update
schemas/resources/api_key.schema.json
allowed_tools schema description updated to document glob/wildcard matching semantics.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
E2e Test Quality Review✅ PassedPASS: tool_acl_per_server_wildcard_scopes_to_one_server uses real HTTP upstreams/gateway and validates list/call scoping; helpers are isolated and readable.
Security Check✅ PassedNo security regressions found: wildcard ACLs remain anchored and additive, secrets aren't logged or stored, and auth checks still short-circuit before routing.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: per-server wildcard support for MCP tool ACLs on API keys.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-tool-acl-wildcard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

- Update the admin OpenAPI `allowed_tools` description (request + public
schemas) to match the code doc and resource schema — it still carried
the pre-wildcard wording, so the served contract disagreed with the
shipped behavior. Tighten the schema-guard tests to assert the
`<server>__*` wording so future drift fails CI.
- Pin the leading/middle-`*` breadth with a test: entries are single-`*`
globs anywhere (like `allowed_models`), so `"*__readonly"` is a real
any-server grant of a same-named tool — documented as intentional, not
an accident.
- Note on `can_access_tool` that it's test-only; `ToolAcl::permits` is
the live enforcement path they mirror.
@moonming
moonming merged commit bba94b5 into mainJul 3, 2026
12 checks passed
@moonming
moonming deleted the feat/mcp-tool-acl-wildcard branch July 3, 2026 05:27
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.

1 participant

@moonming
, '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

feat(mcp): per-server wildcard in api_key MCP tool ACL - #709

Merged
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard
Jul 3, 2026
Merged

feat(mcp): per-server wildcard in api_key MCP tool ACL#709
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard

Conversation

@moonming

@moonmingmoonming commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

An API key's allowed_tools (per-tool MCP ACL, #668) matched only a bare "*" (every tool) or exact <server>__<tool> names. Since the discovered tool list isn't surfaced anywhere yet, an operator can't know the exact names — so the only usable grant today is all-or-nothing ("*").

This adds a per-server wildcard: entries are now matched as single-* globs (reusing the very same wildcard::wildcard_matches helper that allowed_models already uses), so "<server>__*" grants every tool on one server:

  • "github__*" permits github__create_issue, github__delete_repo
  • and does not leak across the boundary: slack__post_message and the prefix-sharing githubenterprise__create_issue are both denied.

A bare "*" and exact names keep their exact meaning. The change is purely additive — no existing grant shifts, because real composed tool names contain no *.

Why this shape

  • Reuses the model matcher.allowed_models already globs openai/*; tools now glob github__* through the identical helper, so the two ACLs stay semantically consistent and there's one source of truth.
  • Both matchers move together.ApiKey::can_access_tool (core) and the proxy-path ToolAcl::permits (aisix-mcp) both delegate to wildcard_matches, preserving their documented "mirror" relationship. ToolAcl::from_allowed still folds a bare "*" into AllowAll; a <server>__* becomes a scoped Allow.
  • Server granularity is the granularity an operator can actually express without a live tool list, and it maps directly onto the MCP servers they've already registered ("give this key the GitHub server's tools").

Tests

  • can_access_tool_enforces_namespaced_allowlist extended: github__* grants github tools, denies a different server and a prefix-sharing server name.
  • tool_acl_from_allowed_semantics: a per-server wildcard is a scoped Allow, not AllowAll.
  • tool_acl_per_server_wildcard_scopes_to_one_server (new, full gateway): alpha__* over two real upstreams exposes only alpha's tools in tools/list, calls alpha, and rejects beta (defense-in-depth) — exercises permits end-to-end.

cargo fmt / clippy -D warnings / aisix-core + aisix-mcp suites green; dump-schema regenerated api_key.schema.json (description only) and is idempotent.

Control-plane pairing

This exposes a richer matching semantics on an existing field; the field itself still needs to become reachable — the CP api_key resource has no allowed_tools today. The paired AISIX-Cloud PR adds allowed_tools to the api_key model/validation/projection + a dashboard tool-ACL picker that composes <server>__* grants from the org's registered MCP servers (plus a global-* toggle and an exact-name escape hatch). Refs AISIX-Cloud#894 (Phase 2, per-key tool ACL). The live individual-tool list (via a DP→CP report) is a separate fast-follow.

Summary by CodeRabbit

  • New Features

    • Tool access rules now support wildcard patterns, including server-scoped grants like <server>__*.
    • API keys can now allow all tools, a single tool, or all tools for one server using pattern-based access.
  • Bug Fixes

    • Tightened tool listing and tool call permissions so access is limited to the intended server or tool names.
    • Keys without tool अनुमति remain unable to access MCP tools unless explicitly granted.
  • Documentation

    • Updated access descriptions to clearly explain wildcard and exact-match behavior.

Independent audit response

A cold audit returned FIX-FIRST (no BLOCK): it traced the matcher character-by-character and confirmed github__* matches github tools while correctly rejecting the prefix-sharing githubenterprise__*, no privilege-widening for existing data, and that tools/list filter + tools/call reject both go through the same permits. Two MEDIUMs, both fixed in eb8ff95:

  • MEDIUM-1 — stale admin OpenAPI description. The hand-maintained allowed_tools descriptions in openapi.rs (request + public schemas) still carried the pre-wildcard wording, so GET /admin/openapi.json disagreed with the shipped behavior. Updated both to match the code doc and api_key.schema.json; the schema-guard tests now assert the <server>__* wording so future drift fails CI. (No separate generated artifact exists — openapi.rs is the source of truth; verified the generator propagates the new wording.)
  • MEDIUM-2 — leading/middle-* breadth untested. Entries are single-* globs anywhere, mirroring allowed_models — so "*__readonly" is a genuine any-server grant of a same-named tool. Pinned with a test (asserting cross-server match + suffix anchoring) and kept the "single-* globs, mirroring allowed_models" framing that states the general rule. This is intentional and consistent with the model ACL, not an accident.

LOWs: added a one-line note that can_access_tool is test-only (the live path is ToolAcl::permits); the literal-*-in-name and debug_assert! server-name notes are benign/pre-existing (no real grant flips, since composed tool names contain no *).

`allowed_tools` matched only a bare `"*"` (all tools) or exact
`<server>__<tool>` names. Without a live tool list an operator can't
know the exact names, so the only usable grant was all-or-nothing.
Match entries as single-`*` globs instead, reusing the same
`wildcard::wildcard_matches` helper `allowed_models` already uses, so
`"<server>__*"` grants every tool on one server (e.g. `"github__*"`
permits `github__create_issue` but not `slack__post` or
`githubenterprise__create_issue`). A bare `"*"` and exact names keep
their meaning; the change is purely additive (no existing grant shifts,
since real tool names contain no `*`). Both matchers move together —
`ApiKey::can_access_tool` and the proxy-path `ToolAcl::permits` — so
their documented "mirror" relationship holds.
This makes per-server tool governance reachable ahead of the live
tool-list report; the control-plane api_key `allowed_tools` field +
dashboard picker (which composes `<server>__*` from the org's
registered MCP servers) ship as the paired AISIX-Cloud PR.
@coderabbitai

coderabbitaiBot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Tool access authorization changes from exact-match/HashSet-based checks to glob-based wildcard matching using wildcard_matches, applied to both ApiKey::can_access_tool and ToolAcl::permits. Documentation and schema descriptions are updated accordingly, and tests are extended to cover per-server wildcard scoping.

Changes

Wildcard Tool ACL

Layer / File(s)Summary
ApiKey tool access glob matching
crates/aisix-core/src/models/apikey.rs
can_access_tool now uses wildcard_matches instead of exact/"*" checks; doc comment clarifies single-* glob semantics; test extended to verify server-scoped wildcard non-leakage.
Gateway ToolAcl glob matching
crates/aisix-mcp/src/gateway.rs, crates/aisix-mcp/tests/gateway_aggregation.rs
ToolAcl::permits replaces HashSet membership with wildcard_matches; docs updated; tests assert scoped Allow behavior and a new test validates per-server wildcard scoping in tools/list/tools/call.
Schema description update
schemas/resources/api_key.schema.json
allowed_tools schema description updated to document glob/wildcard matching semantics.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
E2e Test Quality Review✅ PassedPASS: tool_acl_per_server_wildcard_scopes_to_one_server uses real HTTP upstreams/gateway and validates list/call scoping; helpers are isolated and readable.
Security Check✅ PassedNo security regressions found: wildcard ACLs remain anchored and additive, secrets aren't logged or stored, and auth checks still short-circuit before routing.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: per-server wildcard support for MCP tool ACLs on API keys.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-tool-acl-wildcard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

- Update the admin OpenAPI `allowed_tools` description (request + public
schemas) to match the code doc and resource schema — it still carried
the pre-wildcard wording, so the served contract disagreed with the
shipped behavior. Tighten the schema-guard tests to assert the
`<server>__*` wording so future drift fails CI.
- Pin the leading/middle-`*` breadth with a test: entries are single-`*`
globs anywhere (like `allowed_models`), so `"*__readonly"` is a real
any-server grant of a same-named tool — documented as intentional, not
an accident.
- Note on `can_access_tool` that it's test-only; `ToolAcl::permits` is
the live enforcement path they mirror.
@moonming
moonming merged commit bba94b5 into mainJul 3, 2026
12 checks passed
@moonming
moonming deleted the feat/mcp-tool-acl-wildcard branch July 3, 2026 05:27
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.

1 participant

@moonming
, '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

feat(mcp): per-server wildcard in api_key MCP tool ACL - #709

Merged
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard
Jul 3, 2026
Merged

feat(mcp): per-server wildcard in api_key MCP tool ACL#709
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard

Conversation

@moonming

@moonmingmoonming commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

An API key's allowed_tools (per-tool MCP ACL, #668) matched only a bare "*" (every tool) or exact <server>__<tool> names. Since the discovered tool list isn't surfaced anywhere yet, an operator can't know the exact names — so the only usable grant today is all-or-nothing ("*").

This adds a per-server wildcard: entries are now matched as single-* globs (reusing the very same wildcard::wildcard_matches helper that allowed_models already uses), so "<server>__*" grants every tool on one server:

  • "github__*" permits github__create_issue, github__delete_repo
  • and does not leak across the boundary: slack__post_message and the prefix-sharing githubenterprise__create_issue are both denied.

A bare "*" and exact names keep their exact meaning. The change is purely additive — no existing grant shifts, because real composed tool names contain no *.

Why this shape

  • Reuses the model matcher.allowed_models already globs openai/*; tools now glob github__* through the identical helper, so the two ACLs stay semantically consistent and there's one source of truth.
  • Both matchers move together.ApiKey::can_access_tool (core) and the proxy-path ToolAcl::permits (aisix-mcp) both delegate to wildcard_matches, preserving their documented "mirror" relationship. ToolAcl::from_allowed still folds a bare "*" into AllowAll; a <server>__* becomes a scoped Allow.
  • Server granularity is the granularity an operator can actually express without a live tool list, and it maps directly onto the MCP servers they've already registered ("give this key the GitHub server's tools").

Tests

  • can_access_tool_enforces_namespaced_allowlist extended: github__* grants github tools, denies a different server and a prefix-sharing server name.
  • tool_acl_from_allowed_semantics: a per-server wildcard is a scoped Allow, not AllowAll.
  • tool_acl_per_server_wildcard_scopes_to_one_server (new, full gateway): alpha__* over two real upstreams exposes only alpha's tools in tools/list, calls alpha, and rejects beta (defense-in-depth) — exercises permits end-to-end.

cargo fmt / clippy -D warnings / aisix-core + aisix-mcp suites green; dump-schema regenerated api_key.schema.json (description only) and is idempotent.

Control-plane pairing

This exposes a richer matching semantics on an existing field; the field itself still needs to become reachable — the CP api_key resource has no allowed_tools today. The paired AISIX-Cloud PR adds allowed_tools to the api_key model/validation/projection + a dashboard tool-ACL picker that composes <server>__* grants from the org's registered MCP servers (plus a global-* toggle and an exact-name escape hatch). Refs AISIX-Cloud#894 (Phase 2, per-key tool ACL). The live individual-tool list (via a DP→CP report) is a separate fast-follow.

Summary by CodeRabbit

  • New Features

    • Tool access rules now support wildcard patterns, including server-scoped grants like <server>__*.
    • API keys can now allow all tools, a single tool, or all tools for one server using pattern-based access.
  • Bug Fixes

    • Tightened tool listing and tool call permissions so access is limited to the intended server or tool names.
    • Keys without tool अनुमति remain unable to access MCP tools unless explicitly granted.
  • Documentation

    • Updated access descriptions to clearly explain wildcard and exact-match behavior.

Independent audit response

A cold audit returned FIX-FIRST (no BLOCK): it traced the matcher character-by-character and confirmed github__* matches github tools while correctly rejecting the prefix-sharing githubenterprise__*, no privilege-widening for existing data, and that tools/list filter + tools/call reject both go through the same permits. Two MEDIUMs, both fixed in eb8ff95:

  • MEDIUM-1 — stale admin OpenAPI description. The hand-maintained allowed_tools descriptions in openapi.rs (request + public schemas) still carried the pre-wildcard wording, so GET /admin/openapi.json disagreed with the shipped behavior. Updated both to match the code doc and api_key.schema.json; the schema-guard tests now assert the <server>__* wording so future drift fails CI. (No separate generated artifact exists — openapi.rs is the source of truth; verified the generator propagates the new wording.)
  • MEDIUM-2 — leading/middle-* breadth untested. Entries are single-* globs anywhere, mirroring allowed_models — so "*__readonly" is a genuine any-server grant of a same-named tool. Pinned with a test (asserting cross-server match + suffix anchoring) and kept the "single-* globs, mirroring allowed_models" framing that states the general rule. This is intentional and consistent with the model ACL, not an accident.

LOWs: added a one-line note that can_access_tool is test-only (the live path is ToolAcl::permits); the literal-*-in-name and debug_assert! server-name notes are benign/pre-existing (no real grant flips, since composed tool names contain no *).

`allowed_tools` matched only a bare `"*"` (all tools) or exact
`<server>__<tool>` names. Without a live tool list an operator can't
know the exact names, so the only usable grant was all-or-nothing.
Match entries as single-`*` globs instead, reusing the same
`wildcard::wildcard_matches` helper `allowed_models` already uses, so
`"<server>__*"` grants every tool on one server (e.g. `"github__*"`
permits `github__create_issue` but not `slack__post` or
`githubenterprise__create_issue`). A bare `"*"` and exact names keep
their meaning; the change is purely additive (no existing grant shifts,
since real tool names contain no `*`). Both matchers move together —
`ApiKey::can_access_tool` and the proxy-path `ToolAcl::permits` — so
their documented "mirror" relationship holds.
This makes per-server tool governance reachable ahead of the live
tool-list report; the control-plane api_key `allowed_tools` field +
dashboard picker (which composes `<server>__*` from the org's
registered MCP servers) ship as the paired AISIX-Cloud PR.
@coderabbitai

coderabbitaiBot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Tool access authorization changes from exact-match/HashSet-based checks to glob-based wildcard matching using wildcard_matches, applied to both ApiKey::can_access_tool and ToolAcl::permits. Documentation and schema descriptions are updated accordingly, and tests are extended to cover per-server wildcard scoping.

Changes

Wildcard Tool ACL

Layer / File(s)Summary
ApiKey tool access glob matching
crates/aisix-core/src/models/apikey.rs
can_access_tool now uses wildcard_matches instead of exact/"*" checks; doc comment clarifies single-* glob semantics; test extended to verify server-scoped wildcard non-leakage.
Gateway ToolAcl glob matching
crates/aisix-mcp/src/gateway.rs, crates/aisix-mcp/tests/gateway_aggregation.rs
ToolAcl::permits replaces HashSet membership with wildcard_matches; docs updated; tests assert scoped Allow behavior and a new test validates per-server wildcard scoping in tools/list/tools/call.
Schema description update
schemas/resources/api_key.schema.json
allowed_tools schema description updated to document glob/wildcard matching semantics.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
E2e Test Quality Review✅ PassedPASS: tool_acl_per_server_wildcard_scopes_to_one_server uses real HTTP upstreams/gateway and validates list/call scoping; helpers are isolated and readable.
Security Check✅ PassedNo security regressions found: wildcard ACLs remain anchored and additive, secrets aren't logged or stored, and auth checks still short-circuit before routing.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: per-server wildcard support for MCP tool ACLs on API keys.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-tool-acl-wildcard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

- Update the admin OpenAPI `allowed_tools` description (request + public
schemas) to match the code doc and resource schema — it still carried
the pre-wildcard wording, so the served contract disagreed with the
shipped behavior. Tighten the schema-guard tests to assert the
`<server>__*` wording so future drift fails CI.
- Pin the leading/middle-`*` breadth with a test: entries are single-`*`
globs anywhere (like `allowed_models`), so `"*__readonly"` is a real
any-server grant of a same-named tool — documented as intentional, not
an accident.
- Note on `can_access_tool` that it's test-only; `ToolAcl::permits` is
the live enforcement path they mirror.
@moonming
moonming merged commit bba94b5 into mainJul 3, 2026
12 checks passed
@moonming
moonming deleted the feat/mcp-tool-acl-wildcard branch July 3, 2026 05:27
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.

1 participant

@moonming
, '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

feat(mcp): per-server wildcard in api_key MCP tool ACL - #709

Merged
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard
Jul 3, 2026
Merged

feat(mcp): per-server wildcard in api_key MCP tool ACL#709
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard

Conversation

@moonming

@moonmingmoonming commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

An API key's allowed_tools (per-tool MCP ACL, #668) matched only a bare "*" (every tool) or exact <server>__<tool> names. Since the discovered tool list isn't surfaced anywhere yet, an operator can't know the exact names — so the only usable grant today is all-or-nothing ("*").

This adds a per-server wildcard: entries are now matched as single-* globs (reusing the very same wildcard::wildcard_matches helper that allowed_models already uses), so "<server>__*" grants every tool on one server:

  • "github__*" permits github__create_issue, github__delete_repo
  • and does not leak across the boundary: slack__post_message and the prefix-sharing githubenterprise__create_issue are both denied.

A bare "*" and exact names keep their exact meaning. The change is purely additive — no existing grant shifts, because real composed tool names contain no *.

Why this shape

  • Reuses the model matcher.allowed_models already globs openai/*; tools now glob github__* through the identical helper, so the two ACLs stay semantically consistent and there's one source of truth.
  • Both matchers move together.ApiKey::can_access_tool (core) and the proxy-path ToolAcl::permits (aisix-mcp) both delegate to wildcard_matches, preserving their documented "mirror" relationship. ToolAcl::from_allowed still folds a bare "*" into AllowAll; a <server>__* becomes a scoped Allow.
  • Server granularity is the granularity an operator can actually express without a live tool list, and it maps directly onto the MCP servers they've already registered ("give this key the GitHub server's tools").

Tests

  • can_access_tool_enforces_namespaced_allowlist extended: github__* grants github tools, denies a different server and a prefix-sharing server name.
  • tool_acl_from_allowed_semantics: a per-server wildcard is a scoped Allow, not AllowAll.
  • tool_acl_per_server_wildcard_scopes_to_one_server (new, full gateway): alpha__* over two real upstreams exposes only alpha's tools in tools/list, calls alpha, and rejects beta (defense-in-depth) — exercises permits end-to-end.

cargo fmt / clippy -D warnings / aisix-core + aisix-mcp suites green; dump-schema regenerated api_key.schema.json (description only) and is idempotent.

Control-plane pairing

This exposes a richer matching semantics on an existing field; the field itself still needs to become reachable — the CP api_key resource has no allowed_tools today. The paired AISIX-Cloud PR adds allowed_tools to the api_key model/validation/projection + a dashboard tool-ACL picker that composes <server>__* grants from the org's registered MCP servers (plus a global-* toggle and an exact-name escape hatch). Refs AISIX-Cloud#894 (Phase 2, per-key tool ACL). The live individual-tool list (via a DP→CP report) is a separate fast-follow.

Summary by CodeRabbit

  • New Features

    • Tool access rules now support wildcard patterns, including server-scoped grants like <server>__*.
    • API keys can now allow all tools, a single tool, or all tools for one server using pattern-based access.
  • Bug Fixes

    • Tightened tool listing and tool call permissions so access is limited to the intended server or tool names.
    • Keys without tool अनुमति remain unable to access MCP tools unless explicitly granted.
  • Documentation

    • Updated access descriptions to clearly explain wildcard and exact-match behavior.

Independent audit response

A cold audit returned FIX-FIRST (no BLOCK): it traced the matcher character-by-character and confirmed github__* matches github tools while correctly rejecting the prefix-sharing githubenterprise__*, no privilege-widening for existing data, and that tools/list filter + tools/call reject both go through the same permits. Two MEDIUMs, both fixed in eb8ff95:

  • MEDIUM-1 — stale admin OpenAPI description. The hand-maintained allowed_tools descriptions in openapi.rs (request + public schemas) still carried the pre-wildcard wording, so GET /admin/openapi.json disagreed with the shipped behavior. Updated both to match the code doc and api_key.schema.json; the schema-guard tests now assert the <server>__* wording so future drift fails CI. (No separate generated artifact exists — openapi.rs is the source of truth; verified the generator propagates the new wording.)
  • MEDIUM-2 — leading/middle-* breadth untested. Entries are single-* globs anywhere, mirroring allowed_models — so "*__readonly" is a genuine any-server grant of a same-named tool. Pinned with a test (asserting cross-server match + suffix anchoring) and kept the "single-* globs, mirroring allowed_models" framing that states the general rule. This is intentional and consistent with the model ACL, not an accident.

LOWs: added a one-line note that can_access_tool is test-only (the live path is ToolAcl::permits); the literal-*-in-name and debug_assert! server-name notes are benign/pre-existing (no real grant flips, since composed tool names contain no *).

`allowed_tools` matched only a bare `"*"` (all tools) or exact
`<server>__<tool>` names. Without a live tool list an operator can't
know the exact names, so the only usable grant was all-or-nothing.
Match entries as single-`*` globs instead, reusing the same
`wildcard::wildcard_matches` helper `allowed_models` already uses, so
`"<server>__*"` grants every tool on one server (e.g. `"github__*"`
permits `github__create_issue` but not `slack__post` or
`githubenterprise__create_issue`). A bare `"*"` and exact names keep
their meaning; the change is purely additive (no existing grant shifts,
since real tool names contain no `*`). Both matchers move together —
`ApiKey::can_access_tool` and the proxy-path `ToolAcl::permits` — so
their documented "mirror" relationship holds.
This makes per-server tool governance reachable ahead of the live
tool-list report; the control-plane api_key `allowed_tools` field +
dashboard picker (which composes `<server>__*` from the org's
registered MCP servers) ship as the paired AISIX-Cloud PR.
@coderabbitai

coderabbitaiBot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Tool access authorization changes from exact-match/HashSet-based checks to glob-based wildcard matching using wildcard_matches, applied to both ApiKey::can_access_tool and ToolAcl::permits. Documentation and schema descriptions are updated accordingly, and tests are extended to cover per-server wildcard scoping.

Changes

Wildcard Tool ACL

Layer / File(s)Summary
ApiKey tool access glob matching
crates/aisix-core/src/models/apikey.rs
can_access_tool now uses wildcard_matches instead of exact/"*" checks; doc comment clarifies single-* glob semantics; test extended to verify server-scoped wildcard non-leakage.
Gateway ToolAcl glob matching
crates/aisix-mcp/src/gateway.rs, crates/aisix-mcp/tests/gateway_aggregation.rs
ToolAcl::permits replaces HashSet membership with wildcard_matches; docs updated; tests assert scoped Allow behavior and a new test validates per-server wildcard scoping in tools/list/tools/call.
Schema description update
schemas/resources/api_key.schema.json
allowed_tools schema description updated to document glob/wildcard matching semantics.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
E2e Test Quality Review✅ PassedPASS: tool_acl_per_server_wildcard_scopes_to_one_server uses real HTTP upstreams/gateway and validates list/call scoping; helpers are isolated and readable.
Security Check✅ PassedNo security regressions found: wildcard ACLs remain anchored and additive, secrets aren't logged or stored, and auth checks still short-circuit before routing.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: per-server wildcard support for MCP tool ACLs on API keys.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-tool-acl-wildcard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

- Update the admin OpenAPI `allowed_tools` description (request + public
schemas) to match the code doc and resource schema — it still carried
the pre-wildcard wording, so the served contract disagreed with the
shipped behavior. Tighten the schema-guard tests to assert the
`<server>__*` wording so future drift fails CI.
- Pin the leading/middle-`*` breadth with a test: entries are single-`*`
globs anywhere (like `allowed_models`), so `"*__readonly"` is a real
any-server grant of a same-named tool — documented as intentional, not
an accident.
- Note on `can_access_tool` that it's test-only; `ToolAcl::permits` is
the live enforcement path they mirror.
@moonming
moonming merged commit bba94b5 into mainJul 3, 2026
12 checks passed
@moonming
moonming deleted the feat/mcp-tool-acl-wildcard branch July 3, 2026 05:27
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.

1 participant

@moonming
, '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

feat(mcp): per-server wildcard in api_key MCP tool ACL - #709

Merged
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard
Jul 3, 2026
Merged

feat(mcp): per-server wildcard in api_key MCP tool ACL#709
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard

Conversation

@moonming

@moonmingmoonming commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

An API key's allowed_tools (per-tool MCP ACL, #668) matched only a bare "*" (every tool) or exact <server>__<tool> names. Since the discovered tool list isn't surfaced anywhere yet, an operator can't know the exact names — so the only usable grant today is all-or-nothing ("*").

This adds a per-server wildcard: entries are now matched as single-* globs (reusing the very same wildcard::wildcard_matches helper that allowed_models already uses), so "<server>__*" grants every tool on one server:

  • "github__*" permits github__create_issue, github__delete_repo
  • and does not leak across the boundary: slack__post_message and the prefix-sharing githubenterprise__create_issue are both denied.

A bare "*" and exact names keep their exact meaning. The change is purely additive — no existing grant shifts, because real composed tool names contain no *.

Why this shape

  • Reuses the model matcher.allowed_models already globs openai/*; tools now glob github__* through the identical helper, so the two ACLs stay semantically consistent and there's one source of truth.
  • Both matchers move together.ApiKey::can_access_tool (core) and the proxy-path ToolAcl::permits (aisix-mcp) both delegate to wildcard_matches, preserving their documented "mirror" relationship. ToolAcl::from_allowed still folds a bare "*" into AllowAll; a <server>__* becomes a scoped Allow.
  • Server granularity is the granularity an operator can actually express without a live tool list, and it maps directly onto the MCP servers they've already registered ("give this key the GitHub server's tools").

Tests

  • can_access_tool_enforces_namespaced_allowlist extended: github__* grants github tools, denies a different server and a prefix-sharing server name.
  • tool_acl_from_allowed_semantics: a per-server wildcard is a scoped Allow, not AllowAll.
  • tool_acl_per_server_wildcard_scopes_to_one_server (new, full gateway): alpha__* over two real upstreams exposes only alpha's tools in tools/list, calls alpha, and rejects beta (defense-in-depth) — exercises permits end-to-end.

cargo fmt / clippy -D warnings / aisix-core + aisix-mcp suites green; dump-schema regenerated api_key.schema.json (description only) and is idempotent.

Control-plane pairing

This exposes a richer matching semantics on an existing field; the field itself still needs to become reachable — the CP api_key resource has no allowed_tools today. The paired AISIX-Cloud PR adds allowed_tools to the api_key model/validation/projection + a dashboard tool-ACL picker that composes <server>__* grants from the org's registered MCP servers (plus a global-* toggle and an exact-name escape hatch). Refs AISIX-Cloud#894 (Phase 2, per-key tool ACL). The live individual-tool list (via a DP→CP report) is a separate fast-follow.

Summary by CodeRabbit

  • New Features

    • Tool access rules now support wildcard patterns, including server-scoped grants like <server>__*.
    • API keys can now allow all tools, a single tool, or all tools for one server using pattern-based access.
  • Bug Fixes

    • Tightened tool listing and tool call permissions so access is limited to the intended server or tool names.
    • Keys without tool अनुमति remain unable to access MCP tools unless explicitly granted.
  • Documentation

    • Updated access descriptions to clearly explain wildcard and exact-match behavior.

Independent audit response

A cold audit returned FIX-FIRST (no BLOCK): it traced the matcher character-by-character and confirmed github__* matches github tools while correctly rejecting the prefix-sharing githubenterprise__*, no privilege-widening for existing data, and that tools/list filter + tools/call reject both go through the same permits. Two MEDIUMs, both fixed in eb8ff95:

  • MEDIUM-1 — stale admin OpenAPI description. The hand-maintained allowed_tools descriptions in openapi.rs (request + public schemas) still carried the pre-wildcard wording, so GET /admin/openapi.json disagreed with the shipped behavior. Updated both to match the code doc and api_key.schema.json; the schema-guard tests now assert the <server>__* wording so future drift fails CI. (No separate generated artifact exists — openapi.rs is the source of truth; verified the generator propagates the new wording.)
  • MEDIUM-2 — leading/middle-* breadth untested. Entries are single-* globs anywhere, mirroring allowed_models — so "*__readonly" is a genuine any-server grant of a same-named tool. Pinned with a test (asserting cross-server match + suffix anchoring) and kept the "single-* globs, mirroring allowed_models" framing that states the general rule. This is intentional and consistent with the model ACL, not an accident.

LOWs: added a one-line note that can_access_tool is test-only (the live path is ToolAcl::permits); the literal-*-in-name and debug_assert! server-name notes are benign/pre-existing (no real grant flips, since composed tool names contain no *).

`allowed_tools` matched only a bare `"*"` (all tools) or exact
`<server>__<tool>` names. Without a live tool list an operator can't
know the exact names, so the only usable grant was all-or-nothing.
Match entries as single-`*` globs instead, reusing the same
`wildcard::wildcard_matches` helper `allowed_models` already uses, so
`"<server>__*"` grants every tool on one server (e.g. `"github__*"`
permits `github__create_issue` but not `slack__post` or
`githubenterprise__create_issue`). A bare `"*"` and exact names keep
their meaning; the change is purely additive (no existing grant shifts,
since real tool names contain no `*`). Both matchers move together —
`ApiKey::can_access_tool` and the proxy-path `ToolAcl::permits` — so
their documented "mirror" relationship holds.
This makes per-server tool governance reachable ahead of the live
tool-list report; the control-plane api_key `allowed_tools` field +
dashboard picker (which composes `<server>__*` from the org's
registered MCP servers) ship as the paired AISIX-Cloud PR.
@coderabbitai

coderabbitaiBot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Tool access authorization changes from exact-match/HashSet-based checks to glob-based wildcard matching using wildcard_matches, applied to both ApiKey::can_access_tool and ToolAcl::permits. Documentation and schema descriptions are updated accordingly, and tests are extended to cover per-server wildcard scoping.

Changes

Wildcard Tool ACL

Layer / File(s)Summary
ApiKey tool access glob matching
crates/aisix-core/src/models/apikey.rs
can_access_tool now uses wildcard_matches instead of exact/"*" checks; doc comment clarifies single-* glob semantics; test extended to verify server-scoped wildcard non-leakage.
Gateway ToolAcl glob matching
crates/aisix-mcp/src/gateway.rs, crates/aisix-mcp/tests/gateway_aggregation.rs
ToolAcl::permits replaces HashSet membership with wildcard_matches; docs updated; tests assert scoped Allow behavior and a new test validates per-server wildcard scoping in tools/list/tools/call.
Schema description update
schemas/resources/api_key.schema.json
allowed_tools schema description updated to document glob/wildcard matching semantics.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
E2e Test Quality Review✅ PassedPASS: tool_acl_per_server_wildcard_scopes_to_one_server uses real HTTP upstreams/gateway and validates list/call scoping; helpers are isolated and readable.
Security Check✅ PassedNo security regressions found: wildcard ACLs remain anchored and additive, secrets aren't logged or stored, and auth checks still short-circuit before routing.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: per-server wildcard support for MCP tool ACLs on API keys.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-tool-acl-wildcard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

- Update the admin OpenAPI `allowed_tools` description (request + public
schemas) to match the code doc and resource schema — it still carried
the pre-wildcard wording, so the served contract disagreed with the
shipped behavior. Tighten the schema-guard tests to assert the
`<server>__*` wording so future drift fails CI.
- Pin the leading/middle-`*` breadth with a test: entries are single-`*`
globs anywhere (like `allowed_models`), so `"*__readonly"` is a real
any-server grant of a same-named tool — documented as intentional, not
an accident.
- Note on `can_access_tool` that it's test-only; `ToolAcl::permits` is
the live enforcement path they mirror.
@moonming
moonming merged commit bba94b5 into mainJul 3, 2026
12 checks passed
@moonming
moonming deleted the feat/mcp-tool-acl-wildcard branch July 3, 2026 05:27
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.

1 participant

@moonming
, '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

feat(mcp): per-server wildcard in api_key MCP tool ACL - #709

Merged
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard
Jul 3, 2026
Merged

feat(mcp): per-server wildcard in api_key MCP tool ACL#709
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard

Conversation

@moonming

@moonmingmoonming commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

An API key's allowed_tools (per-tool MCP ACL, #668) matched only a bare "*" (every tool) or exact <server>__<tool> names. Since the discovered tool list isn't surfaced anywhere yet, an operator can't know the exact names — so the only usable grant today is all-or-nothing ("*").

This adds a per-server wildcard: entries are now matched as single-* globs (reusing the very same wildcard::wildcard_matches helper that allowed_models already uses), so "<server>__*" grants every tool on one server:

  • "github__*" permits github__create_issue, github__delete_repo
  • and does not leak across the boundary: slack__post_message and the prefix-sharing githubenterprise__create_issue are both denied.

A bare "*" and exact names keep their exact meaning. The change is purely additive — no existing grant shifts, because real composed tool names contain no *.

Why this shape

  • Reuses the model matcher.allowed_models already globs openai/*; tools now glob github__* through the identical helper, so the two ACLs stay semantically consistent and there's one source of truth.
  • Both matchers move together.ApiKey::can_access_tool (core) and the proxy-path ToolAcl::permits (aisix-mcp) both delegate to wildcard_matches, preserving their documented "mirror" relationship. ToolAcl::from_allowed still folds a bare "*" into AllowAll; a <server>__* becomes a scoped Allow.
  • Server granularity is the granularity an operator can actually express without a live tool list, and it maps directly onto the MCP servers they've already registered ("give this key the GitHub server's tools").

Tests

  • can_access_tool_enforces_namespaced_allowlist extended: github__* grants github tools, denies a different server and a prefix-sharing server name.
  • tool_acl_from_allowed_semantics: a per-server wildcard is a scoped Allow, not AllowAll.
  • tool_acl_per_server_wildcard_scopes_to_one_server (new, full gateway): alpha__* over two real upstreams exposes only alpha's tools in tools/list, calls alpha, and rejects beta (defense-in-depth) — exercises permits end-to-end.

cargo fmt / clippy -D warnings / aisix-core + aisix-mcp suites green; dump-schema regenerated api_key.schema.json (description only) and is idempotent.

Control-plane pairing

This exposes a richer matching semantics on an existing field; the field itself still needs to become reachable — the CP api_key resource has no allowed_tools today. The paired AISIX-Cloud PR adds allowed_tools to the api_key model/validation/projection + a dashboard tool-ACL picker that composes <server>__* grants from the org's registered MCP servers (plus a global-* toggle and an exact-name escape hatch). Refs AISIX-Cloud#894 (Phase 2, per-key tool ACL). The live individual-tool list (via a DP→CP report) is a separate fast-follow.

Summary by CodeRabbit

  • New Features

    • Tool access rules now support wildcard patterns, including server-scoped grants like <server>__*.
    • API keys can now allow all tools, a single tool, or all tools for one server using pattern-based access.
  • Bug Fixes

    • Tightened tool listing and tool call permissions so access is limited to the intended server or tool names.
    • Keys without tool अनुमति remain unable to access MCP tools unless explicitly granted.
  • Documentation

    • Updated access descriptions to clearly explain wildcard and exact-match behavior.

Independent audit response

A cold audit returned FIX-FIRST (no BLOCK): it traced the matcher character-by-character and confirmed github__* matches github tools while correctly rejecting the prefix-sharing githubenterprise__*, no privilege-widening for existing data, and that tools/list filter + tools/call reject both go through the same permits. Two MEDIUMs, both fixed in eb8ff95:

  • MEDIUM-1 — stale admin OpenAPI description. The hand-maintained allowed_tools descriptions in openapi.rs (request + public schemas) still carried the pre-wildcard wording, so GET /admin/openapi.json disagreed with the shipped behavior. Updated both to match the code doc and api_key.schema.json; the schema-guard tests now assert the <server>__* wording so future drift fails CI. (No separate generated artifact exists — openapi.rs is the source of truth; verified the generator propagates the new wording.)
  • MEDIUM-2 — leading/middle-* breadth untested. Entries are single-* globs anywhere, mirroring allowed_models — so "*__readonly" is a genuine any-server grant of a same-named tool. Pinned with a test (asserting cross-server match + suffix anchoring) and kept the "single-* globs, mirroring allowed_models" framing that states the general rule. This is intentional and consistent with the model ACL, not an accident.

LOWs: added a one-line note that can_access_tool is test-only (the live path is ToolAcl::permits); the literal-*-in-name and debug_assert! server-name notes are benign/pre-existing (no real grant flips, since composed tool names contain no *).

`allowed_tools` matched only a bare `"*"` (all tools) or exact
`<server>__<tool>` names. Without a live tool list an operator can't
know the exact names, so the only usable grant was all-or-nothing.
Match entries as single-`*` globs instead, reusing the same
`wildcard::wildcard_matches` helper `allowed_models` already uses, so
`"<server>__*"` grants every tool on one server (e.g. `"github__*"`
permits `github__create_issue` but not `slack__post` or
`githubenterprise__create_issue`). A bare `"*"` and exact names keep
their meaning; the change is purely additive (no existing grant shifts,
since real tool names contain no `*`). Both matchers move together —
`ApiKey::can_access_tool` and the proxy-path `ToolAcl::permits` — so
their documented "mirror" relationship holds.
This makes per-server tool governance reachable ahead of the live
tool-list report; the control-plane api_key `allowed_tools` field +
dashboard picker (which composes `<server>__*` from the org's
registered MCP servers) ship as the paired AISIX-Cloud PR.
@coderabbitai

coderabbitaiBot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Tool access authorization changes from exact-match/HashSet-based checks to glob-based wildcard matching using wildcard_matches, applied to both ApiKey::can_access_tool and ToolAcl::permits. Documentation and schema descriptions are updated accordingly, and tests are extended to cover per-server wildcard scoping.

Changes

Wildcard Tool ACL

Layer / File(s)Summary
ApiKey tool access glob matching
crates/aisix-core/src/models/apikey.rs
can_access_tool now uses wildcard_matches instead of exact/"*" checks; doc comment clarifies single-* glob semantics; test extended to verify server-scoped wildcard non-leakage.
Gateway ToolAcl glob matching
crates/aisix-mcp/src/gateway.rs, crates/aisix-mcp/tests/gateway_aggregation.rs
ToolAcl::permits replaces HashSet membership with wildcard_matches; docs updated; tests assert scoped Allow behavior and a new test validates per-server wildcard scoping in tools/list/tools/call.
Schema description update
schemas/resources/api_key.schema.json
allowed_tools schema description updated to document glob/wildcard matching semantics.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
E2e Test Quality Review✅ PassedPASS: tool_acl_per_server_wildcard_scopes_to_one_server uses real HTTP upstreams/gateway and validates list/call scoping; helpers are isolated and readable.
Security Check✅ PassedNo security regressions found: wildcard ACLs remain anchored and additive, secrets aren't logged or stored, and auth checks still short-circuit before routing.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: per-server wildcard support for MCP tool ACLs on API keys.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-tool-acl-wildcard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

- Update the admin OpenAPI `allowed_tools` description (request + public
schemas) to match the code doc and resource schema — it still carried
the pre-wildcard wording, so the served contract disagreed with the
shipped behavior. Tighten the schema-guard tests to assert the
`<server>__*` wording so future drift fails CI.
- Pin the leading/middle-`*` breadth with a test: entries are single-`*`
globs anywhere (like `allowed_models`), so `"*__readonly"` is a real
any-server grant of a same-named tool — documented as intentional, not
an accident.
- Note on `can_access_tool` that it's test-only; `ToolAcl::permits` is
the live enforcement path they mirror.
@moonming
moonming merged commit bba94b5 into mainJul 3, 2026
12 checks passed
@moonming
moonming deleted the feat/mcp-tool-acl-wildcard branch July 3, 2026 05:27
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.

1 participant

@moonming
, '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

feat(mcp): per-server wildcard in api_key MCP tool ACL - #709

Merged
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard
Jul 3, 2026
Merged

feat(mcp): per-server wildcard in api_key MCP tool ACL#709
moonming merged 2 commits into
mainfrom
feat/mcp-tool-acl-wildcard

Conversation

@moonming

@moonmingmoonming commented Jul 3, 2026

Copy link
Copy Markdown
Member

What

An API key's allowed_tools (per-tool MCP ACL, #668) matched only a bare "*" (every tool) or exact <server>__<tool> names. Since the discovered tool list isn't surfaced anywhere yet, an operator can't know the exact names — so the only usable grant today is all-or-nothing ("*").

This adds a per-server wildcard: entries are now matched as single-* globs (reusing the very same wildcard::wildcard_matches helper that allowed_models already uses), so "<server>__*" grants every tool on one server:

  • "github__*" permits github__create_issue, github__delete_repo
  • and does not leak across the boundary: slack__post_message and the prefix-sharing githubenterprise__create_issue are both denied.

A bare "*" and exact names keep their exact meaning. The change is purely additive — no existing grant shifts, because real composed tool names contain no *.

Why this shape

  • Reuses the model matcher.allowed_models already globs openai/*; tools now glob github__* through the identical helper, so the two ACLs stay semantically consistent and there's one source of truth.
  • Both matchers move together.ApiKey::can_access_tool (core) and the proxy-path ToolAcl::permits (aisix-mcp) both delegate to wildcard_matches, preserving their documented "mirror" relationship. ToolAcl::from_allowed still folds a bare "*" into AllowAll; a <server>__* becomes a scoped Allow.
  • Server granularity is the granularity an operator can actually express without a live tool list, and it maps directly onto the MCP servers they've already registered ("give this key the GitHub server's tools").

Tests

  • can_access_tool_enforces_namespaced_allowlist extended: github__* grants github tools, denies a different server and a prefix-sharing server name.
  • tool_acl_from_allowed_semantics: a per-server wildcard is a scoped Allow, not AllowAll.
  • tool_acl_per_server_wildcard_scopes_to_one_server (new, full gateway): alpha__* over two real upstreams exposes only alpha's tools in tools/list, calls alpha, and rejects beta (defense-in-depth) — exercises permits end-to-end.

cargo fmt / clippy -D warnings / aisix-core + aisix-mcp suites green; dump-schema regenerated api_key.schema.json (description only) and is idempotent.

Control-plane pairing

This exposes a richer matching semantics on an existing field; the field itself still needs to become reachable — the CP api_key resource has no allowed_tools today. The paired AISIX-Cloud PR adds allowed_tools to the api_key model/validation/projection + a dashboard tool-ACL picker that composes <server>__* grants from the org's registered MCP servers (plus a global-* toggle and an exact-name escape hatch). Refs AISIX-Cloud#894 (Phase 2, per-key tool ACL). The live individual-tool list (via a DP→CP report) is a separate fast-follow.

Summary by CodeRabbit

  • New Features

    • Tool access rules now support wildcard patterns, including server-scoped grants like <server>__*.
    • API keys can now allow all tools, a single tool, or all tools for one server using pattern-based access.
  • Bug Fixes

    • Tightened tool listing and tool call permissions so access is limited to the intended server or tool names.
    • Keys without tool अनुमति remain unable to access MCP tools unless explicitly granted.
  • Documentation

    • Updated access descriptions to clearly explain wildcard and exact-match behavior.

Independent audit response

A cold audit returned FIX-FIRST (no BLOCK): it traced the matcher character-by-character and confirmed github__* matches github tools while correctly rejecting the prefix-sharing githubenterprise__*, no privilege-widening for existing data, and that tools/list filter + tools/call reject both go through the same permits. Two MEDIUMs, both fixed in eb8ff95:

  • MEDIUM-1 — stale admin OpenAPI description. The hand-maintained allowed_tools descriptions in openapi.rs (request + public schemas) still carried the pre-wildcard wording, so GET /admin/openapi.json disagreed with the shipped behavior. Updated both to match the code doc and api_key.schema.json; the schema-guard tests now assert the <server>__* wording so future drift fails CI. (No separate generated artifact exists — openapi.rs is the source of truth; verified the generator propagates the new wording.)
  • MEDIUM-2 — leading/middle-* breadth untested. Entries are single-* globs anywhere, mirroring allowed_models — so "*__readonly" is a genuine any-server grant of a same-named tool. Pinned with a test (asserting cross-server match + suffix anchoring) and kept the "single-* globs, mirroring allowed_models" framing that states the general rule. This is intentional and consistent with the model ACL, not an accident.

LOWs: added a one-line note that can_access_tool is test-only (the live path is ToolAcl::permits); the literal-*-in-name and debug_assert! server-name notes are benign/pre-existing (no real grant flips, since composed tool names contain no *).

`allowed_tools` matched only a bare `"*"` (all tools) or exact
`<server>__<tool>` names. Without a live tool list an operator can't
know the exact names, so the only usable grant was all-or-nothing.
Match entries as single-`*` globs instead, reusing the same
`wildcard::wildcard_matches` helper `allowed_models` already uses, so
`"<server>__*"` grants every tool on one server (e.g. `"github__*"`
permits `github__create_issue` but not `slack__post` or
`githubenterprise__create_issue`). A bare `"*"` and exact names keep
their meaning; the change is purely additive (no existing grant shifts,
since real tool names contain no `*`). Both matchers move together —
`ApiKey::can_access_tool` and the proxy-path `ToolAcl::permits` — so
their documented "mirror" relationship holds.
This makes per-server tool governance reachable ahead of the live
tool-list report; the control-plane api_key `allowed_tools` field +
dashboard picker (which composes `<server>__*` from the org's
registered MCP servers) ship as the paired AISIX-Cloud PR.
@coderabbitai

coderabbitaiBot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Tool access authorization changes from exact-match/HashSet-based checks to glob-based wildcard matching using wildcard_matches, applied to both ApiKey::can_access_tool and ToolAcl::permits. Documentation and schema descriptions are updated accordingly, and tests are extended to cover per-server wildcard scoping.

Changes

Wildcard Tool ACL

Layer / File(s)Summary
ApiKey tool access glob matching
crates/aisix-core/src/models/apikey.rs
can_access_tool now uses wildcard_matches instead of exact/"*" checks; doc comment clarifies single-* glob semantics; test extended to verify server-scoped wildcard non-leakage.
Gateway ToolAcl glob matching
crates/aisix-mcp/src/gateway.rs, crates/aisix-mcp/tests/gateway_aggregation.rs
ToolAcl::permits replaces HashSet membership with wildcard_matches; docs updated; tests assert scoped Allow behavior and a new test validates per-server wildcard scoping in tools/list/tools/call.
Schema description update
schemas/resources/api_key.schema.json
allowed_tools schema description updated to document glob/wildcard matching semantics.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
E2e Test Quality Review✅ PassedPASS: tool_acl_per_server_wildcard_scopes_to_one_server uses real HTTP upstreams/gateway and validates list/call scoping; helpers are isolated and readable.
Security Check✅ PassedNo security regressions found: wildcard ACLs remain anchored and additive, secrets aren't logged or stored, and auth checks still short-circuit before routing.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly summarizes the main change: per-server wildcard support for MCP tool ACLs on API keys.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-tool-acl-wildcard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

- Update the admin OpenAPI `allowed_tools` description (request + public
schemas) to match the code doc and resource schema — it still carried
the pre-wildcard wording, so the served contract disagreed with the
shipped behavior. Tighten the schema-guard tests to assert the
`<server>__*` wording so future drift fails CI.
- Pin the leading/middle-`*` breadth with a test: entries are single-`*`
globs anywhere (like `allowed_models`), so `"*__readonly"` is a real
any-server grant of a same-named tool — documented as intentional, not
an accident.
- Note on `can_access_tool` that it's test-only; `ToolAcl::permits` is
the live enforcement path they mirror.
@moonming
moonming merged commit bba94b5 into mainJul 3, 2026
12 checks passed
@moonming
moonming deleted the feat/mcp-tool-acl-wildcard branch July 3, 2026 05:27
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.

1 participant

@moonming