Skip to content

feat(mcp-tools-acl): restrict the MCP tools a consumer may see and call - #13963

Open
AlinsRan wants to merge 1 commit into
apache:masterfrom
AlinsRan:feat/mcp-tools-acl
Open

AlinsRan wants to merge 1 commit into
apache:masterfrom
AlinsRan:feat/mcp-tools-acl

Conversation

@AlinsRan

Copy link
Copy Markdown
Contributor

Description

openapi-to-mcp turns every operation of an OpenAPI document into a tool, and a route serving it hands the whole set to whoever connects. That is rarely what a given client should get: a read-only agent has no business calling deletePet, and it should not even be told the tool exists.

mcp-tools-acl narrows that set per consumer. It runs at priority 539 — below openapi-to-mcp (540) and below the auth plugins, so the consumer is already known when a rule is matched. It:

  • refuses a tools/call for a tool the matched rule does not allow, so the API is never called;
  • removes the same tools from the tools/list answer, over both transports — the JSON body of a streamable_http answer, and the SSE events of an sse one, buffering an incomplete event across body_filter chunks.

Rules are selected per request with lua-resty-expr, so one route can serve a different tool set to each consumer. A rule without an expression always matches; when no rule matches, nothing is enforced.

{
  "mcp-tools-acl": {
    "rules": [
      { "expr": [["consumer_name", "==", "reader"]], "allow_tools": ["getPetById"] },
      { "deny_tools": ["deletePet"], "rejected_msg": "deletePet is not allowed" }
    ]
  }
}

openapi-to-mcp gains one line, ctx.openapi_to_mcp_active = true. The ACL plugin has no other way to tell an MCP conversation from an ordinary route — the request body alone looks the same — and on a route without openapi-to-mcp it now stands aside with a warning rather than trying to police plain JSON.

This is the plugin API7 Enterprise has been shipping, contributed here unchanged. Its tests are new: the enterprise suite drives the Node sidecar that build used, which says nothing about the in-process implementation this repository has, so the suite was rewritten against lib.openapi_to_mcp_fixture.

Which issue(s) this PR fixes

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible (If not, please discuss on the APISIX mailing list first)

openapi-to-mcp turns every operation of an OpenAPI document into a tool, and a
route that serves it hands the whole set to whoever connects. That is rarely
what a given client should get: a read-only agent has no business calling
deletePet, and it should not even be told the tool exists.

mcp-tools-acl runs at priority 539, below openapi-to-mcp (540) and below the
auth plugins, so the consumer is known by the time a rule is matched. It
refuses a tools/call for a tool the matched rule does not allow, and removes
the same tools from the tools/list answer -- over both transports, which means
filtering the JSON body of a streamable_http answer and the SSE events of an
sse one, buffering an incomplete event across body_filter chunks.

Rules are picked per request with lua-resty-expr, so one route can serve a
different set of tools to each consumer. A rule without an expression always
matches; when no rule matches, nothing is enforced.

openapi-to-mcp gains one line, `ctx.openapi_to_mcp_active = true`. The ACL
plugin has no other way to tell an MCP conversation from an ordinary route --
the request body alone looks the same -- and on a route without openapi-to-mcp
it now stands aside with a warning instead of trying to police plain JSON.

The plugin is the one API7 Enterprise has been shipping; this is that code,
with its tests rewritten against the in-process openapi-to-mcp implementation
this repository has rather than the sidecar the enterprise build used.
@AlinsRan AlinsRan self-assigned this Sep 18, 2026
Sign up for free to 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