Capability Filtering
The Model Context Protocol lets a server advertise tools, prompts, resources, and resource templates. When the Zuplo MCP Gateway proxies an upstream server, every one of those capabilities flows through to the client by default. That's the right behavior when the upstream is small and trusted, and the wrong behavior when the upstream exposes dozens of operations only a few of which belong in front of an AI client.
The mcp-capability-filter-inbound policy is how the gateway curates that
surface area. This page covers what the policy filters, the rules that govern
when capabilities are exposed versus hidden, the projection model that lets the
gateway rewrite descriptions, and the boundary the filter actually enforces.
To attach the policy to a route and walk through worked examples, see Curate the tools an upstream exposes.
What the policy filters
The policy operates on four MCP capability types, each matched by the upstream identifier the protocol uses:
| Capability | Matched by | List method | Invocation method |
|---|---|---|---|
tools | name | tools/list | tools/call |
prompts | name | prompts/list | prompts/get |
resources | uri | resources/list | resources/read |
resourceTemplates | uriTemplate | resources/templates/list | resources/read |
Matching is case-sensitive and exact. There's no regex, glob, or category
matching — if the upstream returns a tool named createUser and the policy
lists create_user, the tool stays hidden.
Omit versus empty array
The behavior of each option depends on whether it's present at all:
- Omit the option — every capability of that type passes through unchanged. This is the default and is useful when filtering tools but leaving prompts and resources alone.
- Provide an empty array — expose nothing of that type. The list response
becomes empty and every direct call returns
MethodNotFound. - Provide entries — expose only the listed items. Everything else is filtered or blocked.
The omit-versus-empty-array distinction is the single most consequential rule in the filter. Omitting an option is a pass-through; an empty array is the opposite — it hides every capability of that type. Confusing the two is the most common source of "why can the client still see that tool?" reports.
Projections
Each allow-list entry is either a plain string (name only) or a projection
object that keeps the upstream identifier but overrides what the client sees.
Projections let the gateway rewrite the description for clarity, override tool
annotations like destructiveHint or readOnlyHint, attach _meta fields that
downstream middleware reads, or rewrite a resource's name and mimeType for a
curated catalog.
The upstream identifier — name for tools and prompts, uri for resources,
uriTemplate for resource templates — is always required and serves as the
stable match key. Annotation and _meta overrides are deep-merged with the
upstream values: fields the projection specifies win, fields it doesn't specify
pass through.
Schema fields stay upstream. inputSchema and outputSchema always come from
the upstream list response — the projection can't rewrite parameter shapes or
enforce additional validation. A separate policy on the route handles those
concerns when they come up.
How the filter behaves at runtime
When the gateway sees a successful response to tools/list, prompts/list,
resources/list, or resources/templates/list, it reads the list from the
upstream response, keeps only items whose identifier appears on the allow-list,
merges any projection overrides into the kept items, and returns the filtered
list. Items the upstream returned that aren't on the allow-list are silently
dropped — the client never learns they exist.
When the gateway sees tools/call, prompts/get, or resources/read, it reads
the target identifier from the request (params.name for tools and prompts,
params.uri for resources). If the identifier isn't on the matching allow-list,
the gateway returns a JSON-RPC MethodNotFound error before forwarding
upstream:
Code
That early block is what makes the filter a real boundary rather than cosmetic
curation. A client that already knows a hidden tool's name — from a cached
tools/list, a different gateway, or guesswork — still can't invoke it. The
same block fires when the option is set to an empty array: every direct call of
that capability type returns MethodNotFound.
Batch requests
The policy handles JSON-RPC batch requests with two rules. List responses inside
a batch are filtered per item — the policy matches each response item to its
originating list request by ID and applies the same filtering and projection
rules as for a single response. Hidden invocations inside a batch block the
whole batch with a single MethodNotFound error; the gateway does not split,
partially filter, or forward sibling items.
Where it sits in the policy chain
The capability filter belongs after any policy that produces or replaces the
upstream response — mcp-token-exchange-inbound is the most common one. The
filter operates on the final response, so policies that transform the response
upstream of it have already done their work by the time the filter runs.
Keep the filter last in the chain even when there's no
mcp-token-exchange-inbound policy on the route (for example, an API-key
upstream via set-headers-inbound or set-upstream-api-key-inbound), so any
future inbound policies that produce or replace responses run before it.
What the filter does not do
A few capabilities are intentionally out of scope:
- No schema overrides.
inputSchemaandoutputSchemaalways come from the upstream list response. - No regex, glob, or category matching. Allow-lists are exact, by identifier. If the upstream renames a tool, the policy entry must be updated to match.
- No non-JSON filtering. Filtering applies only to JSON responses. Streamed or binary responses pass through untouched.
- No effect on capability metadata in
initialize. The protocol-levelserverCapabilitiesblock in theinitializeresponse advertises which capability types the server supports (tools, prompts, resources). The filter doesn't strip those flags. A client sees that the gateway supports tools even when the tool allow-list is empty; only the list and call responses change. - No quota or rate limit. Capability filtering trims the surface area the
gateway exposes but doesn't bound how often clients can call what remains.
Pair it with the
rate-limit-inboundpolicy when usage controls are needed.
Related
- Curate the tools an upstream exposes — how to attach the policy, override descriptions, and verify the filter is active.
McpProxyHandlerreference — the route handler the filter runs in front of.- Per-user OAuth to upstream MCP servers — the upstream side of the picture; the filter usually composes with the token-exchange policy on the same route.
- MCP capability semantics in the specification.