Skip to content

Add McpServerToolFilter for MCP servers - #7201

Open
ZhangDT-sky wants to merge 1 commit into
spring-projects:mainfrom
ZhangDT-sky:GH-7192
Open

ZhangDT-sky wants to merge 1 commit into
spring-projects:mainfrom
ZhangDT-sky:GH-7192

Conversation

@ZhangDT-sky

Copy link
Copy Markdown
Contributor

Description

Introduce McpServerToolFilter, an Ordered SPI consulted for every tools/list request, and wire the ordered chain into the sync and async builders of both the stateful and stateless MCP servers.

A tool hidden by a filter is omitted from listings only: it stays reachable through tools/call, so authorization still belongs in the tool handler. The SPI documents this and the tests pin it down.

@Bean
McpServerToolFilter hideAdminToolsFromOtherTenants() {
    return (transportContext, tool) -> {
        String tenant = (String) transportContext.get("x-tenant-id");
        return "acme".equals(tenant) || !tool.name().startsWith("admin-");
    };
}

Filters are consulted in Ordered order and combined with AND semantics, short-circuiting at the first rejection. The async path offloads the chain off the reactor event loop, since an implementation may perform blocking work such as a security decision.

Scope

This covers "hide what this caller cannot call", the common denominator of the issues consolidated in #7192. It does not cover the rest of #7192:

Not in this pull request Why
Per-caller description or input schema McpSyncListFilter returns a boolean; the Tool is fixed when the specification is built
Wrapping the tool handler Same reason — the handler is bound at startup
Consulting the SPI on tools/call List filters are listing-only by design; a hidden tool called by name still executes
Passing the annotated Method and its bean Only needed by the Spring Security mapper proposed in the issue

Those require the McpSyncToolRepository extension point from modelcontextprotocol/java-sdk#1170, released as the 2.2.0-M1 pre-release. It owns list/find/call behind one per-request context, which is the shape the remaining asks need. Building a filter hook that a wrapper would silently bypass seemed worse than shipping the part that works on the current SDK.

Notes for review

  • Why an SPI rather than the existing customizers: McpSyncServerCustomizer, McpAsyncServerCustomizer, McpStatelessSyncServerCustomizer and McpStatelessAsyncServerCustomizer all expose the builder, so addToolFilter is already reachable from them. But they are four distinct types, so a caller filtering per request would need one bean per mode, the same filter logic duplicated in each, and prior knowledge of which mode the application runs in. This SPI keeps it to a single bean covering all four.
  • The SPI receives McpTransportContext and McpSchema.Tool rather than a tool specification. That keeps a single extension point for all four server flavors, and covers tools registered through @McpTool as well as through @Tool/ToolCallback, which produce different specification types.
  • Placement: spring-ai-mcp, next to the client-side McpToolFilter it mirrors.
  • Tests: McpServerToolFilterAdapterTests covers the chain; end-to-end tools/list assertions run against both the stateless and the stateful WebFlux server, each also asserting that the hidden tool still executes.
  • Reference docs: a Per-Caller Tool Filtering section in mcp-annotations-server.adoc.

See #7192

Introduce `McpServerToolFilter`, an `Ordered` SPI consulted for every
`tools/list` request, and wire the ordered chain into the sync and async
builders of both the stateful and stateless MCP servers.

A tool hidden by a filter is omitted from listings only: it stays
reachable through `tools/call`, so authorization still belongs in the
tool handler. The SPI documents this and the tests pin it down.

See spring-projects#7192

Signed-off-by: ZhangDT-sky <485918776@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants