Curate the tools an upstream exposes
When an upstream MCP server exposes more capabilities than belong in front of an
AI client, attach the mcp-capability-filter-inbound policy to the route to
allow-list the subset that should pass through, override descriptions or
annotations, and block direct calls to anything outside the list.
For the conceptual model behind capability filtering — what the policy filters, the omit-versus-empty-array rule, and how projections are merged — see Capability filtering.
Add the capability filter policy
-
Declare the policy in
config/policies.json. List the upstream identifiers you want to expose for each capability type —namefor tools and prompts,urifor resources,uriTemplatefor resource templates:config/policies.json -
Attach the policy to the route in
config/routes.oas.json, aftermcp-token-exchange-inboundso the filter operates on the final upstream response:config/routes.oas.json
Because prompts, resources, and resourceTemplates are omitted from the
options, the upstream's prompts and resources flow through unmodified. Only the
tool list is restricted.
Override a tool description
To rewrite the description or annotations a client sees while keeping the upstream identifier as the match key, replace the string entry with a projection object:
Code
The string entries ("list_issues", "get_issue") pass through with the
upstream's own descriptions. The projection object overrides create_issue's
description while keeping the upstream's input schema, output schema, and name
untouched.
Override tool annotations
Tool annotations
are deep-merged with the upstream's annotations — fields you specify win, fields
you don't specify pass through. The same applies to _meta:
Code
Project a resource
Resources use uri as the match key. A resource projection can rewrite the
downstream-facing name, description, or mimeType:
Code
Block everything from a capability type
Provide an empty array to expose nothing of that type. The list response becomes
empty and every direct call returns MethodNotFound:
Code
To turn a route into a temporary kill switch — all capability types disabled
without removing the route from configuration — set every type to []:
Code
Omitting an option behaves like a pass-through; an empty array ("tools": [])
hides every capability of that type. Confusing the two is the most common source
of "why can the client still see that tool?" reports.
Worked example: read-only Linear
Suppose the corp Linear upstream exposes more than two dozen tools and only the read-only subset belongs in front of the team's AI assistant. Allow-list the read tools, override descriptions for clarity, and hide all prompts and resources:
config/policies.json
Attach the policy to a dedicated route in config/routes.oas.json:
config/routes.oas.json
The same upstream Linear MCP server is now reachable at two routes — the
full-featured /mcp/linear-v1 and the curated /mcp/linear-readonly — each
with its own surface area.
Verify the filter
After deploying (or restarting zuplo dev), confirm the filter is active:
- Connect a test client (the MCP Inspector is the fastest option) to the filtered route.
- Call
tools/list. Only the allow-listed tools should appear. - Call
tools/callwith a tool name that isn't on the list. The gateway returns a JSON-RPCMethodNotFounderror before the request reaches the upstream.
If a tool you expected to see doesn't appear, check the upstream's tools/list
response directly — the match is case-sensitive and exact, so a typo or
capitalization difference makes the entry not match.
Related
- Capability filtering — the conceptual model behind the policy.
McpProxyHandlerreference — the route handler the filter runs in front of.- Connect a gateway to an upstream OAuth provider — pair the filter with per-user upstream OAuth.