# 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](./how-to/curate-tools.mdx).

## 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**:

```json
{
  "jsonrpc": "2.0",
  "id": "1",
  "error": {
    "code": -32601,
    "message": "Method not found"
  }
}
```

The filter blocks calls before forwarding upstream, so 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.** `inputSchema` and `outputSchema` always 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-level
  `serverCapabilities` block in the `initialize` response 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-inbound`](../policies/rate-limit-inbound.mdx)
  policy when usage controls are needed.

## Related

- [Curate the tools an upstream exposes](./how-to/curate-tools.mdx) — how to
  attach the policy, override descriptions, and verify the filter is active.
- [`McpProxyHandler` reference](./code-config/mcp-proxy-handler.mdx) — the route
  handler the filter runs in front of.
- [Per-user OAuth to upstream MCP servers](./auth/upstream-oauth.mdx) — 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](https://modelcontextprotocol.io/specification/2025-11-25/server/tools).
