ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop using the Portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingMCP - Quick start
    Develop Locally
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth
Concepts
Development
Policies
Handlers
API Keys
MCP Server
MCP Gateway
    IntroductionBetaQuickstartQuickstart (Local Dev)How it works
    Connect MCP clients
    Authentication
    Configuration
      Set up the gatewayMulti-upstreamLocal developmentCapability filteringCurate toolsCurate tools (in code)McpProxyHandlerCompatibility dates
    Observability
    ReferenceTroubleshooting
AI Gateway
Developer Portal
Monetization
Deploying & Source Control
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Configuration

Curate the tools an upstream exposes (in code)

Choose how to curate tools

Curate the same way in the Portal or in code. Both write the same mcp-capability-filter-inbound policy, so you can switch approaches at any time.

In the Portal
In code

In code

Configure the mcp-capability-filter-inbound policy directly in your project files with the Zuplo CLI.

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, including what the policy filters, the omit-versus-empty-array rule, and how projections are merged, see Capability filtering.

Prefer the Portal? The Portal version reaches the same result from the MCP Gateway Virtual Server UI.

Add the capability filter policy

  1. Declare the policy in config/policies.json. List the upstream identifiers you want to expose for each capability type (name for tools and prompts, uri for resources, uriTemplate for resource templates):

    config/policies.json
    { "name": "filter-linear-tools", "policyType": "mcp-capability-filter-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpCapabilityFilterInboundPolicy", "options": { "tools": ["list_issues", "get_issue", "create_issue"], }, }, }
  2. Attach the policy to the route in config/routes.oas.json, after mcp-token-exchange-inbound so the filter operates on the final upstream response:

    config/routes.oas.json
    "/mcp/linear-v1": { "get,post": { "operationId": "linear-mcp-server", "x-zuplo-route": { "corsPolicy": "none", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpProxyHandler", "options": { "rewritePattern": "https://mcp.linear.app/mcp" } }, "policies": { "inbound": [ "auth0-managed-oauth", "mcp-token-exchange-linear", "filter-linear-tools" ] } } } }

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
{ "options": { "tools": [ { "name": "create_issue", "description": "Create a Linear issue. Provide a title and team; everything else is optional.", }, "list_issues", "get_issue", ], }, }

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, so fields you specify win and fields you don't specify pass through. The same applies to _meta:

Code
{ "tools": [ { "name": "delete_issue", "description": "Delete a Linear issue. This is irreversible.", "annotations": { "destructiveHint": true, "readOnlyHint": false, }, "_meta": { "io.example.audit": "high", }, }, ], }

Project a resource

Resources use uri as the match key. A resource projection can rewrite the downstream-facing name, description, or mimeType:

Code
{ "resources": [ { "uri": "stripe://customers", "name": "Customers", "description": "All Stripe customers visible to this account.", "mimeType": "application/json", }, ], "resourceTemplates": [ { "uriTemplate": "stripe://customers/{id}", "name": "Customer detail", "description": "A single Stripe customer keyed by ID.", }, ], }

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
{ "options": { "tools": ["safe_tool_a", "safe_tool_b"], "prompts": [], "resources": [], "resourceTemplates": [], }, }

To turn a route into a temporary kill switch, with all capability types disabled without removing the route from configuration, set every type to []:

Code
{ "options": { "tools": [], "prompts": [], "resources": [], "resourceTemplates": [], }, }

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.

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
{ "name": "filter-linear-read-only", "policyType": "mcp-capability-filter-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpCapabilityFilterInboundPolicy", "options": { "tools": [ { "name": "list_issues", "description": "List Linear issues. Filter by team, state, assignee, or label.", }, { "name": "get_issue", "description": "Get a single Linear issue by ID or identifier (e.g. ENG-123).", }, { "name": "list_teams", "description": "List the teams in the current Linear workspace.", }, { "name": "list_projects", "description": "List the projects in the current Linear workspace.", "annotations": { "readOnlyHint": true, }, }, ], "prompts": [], "resources": [], "resourceTemplates": [], }, }, }

Attach the policy to a dedicated route in config/routes.oas.json:

config/routes.oas.json
"/mcp/linear-readonly": { "get,post": { "operationId": "linear-readonly-mcp-server", "x-zuplo-route": { "corsPolicy": "none", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpProxyHandler", "options": { "rewritePattern": "https://mcp.linear.app/mcp" } }, "policies": { "inbound": [ "auth0-managed-oauth", "mcp-token-exchange-linear", "filter-linear-read-only" ] } } } }

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:

  1. Connect a test client (the MCP Inspector is the fastest option) to the filtered route.
  2. Call tools/list. Only the allow-listed tools should appear.
  3. Call tools/call with a tool name that isn't on the list. The gateway returns a JSON-RPC MethodNotFound error 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

  • Curate tools in the Portal: do the same from the Virtual Server UI.
  • Capability filtering: the conceptual model behind the policy.
  • McpProxyHandler reference: 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.
Edit this page
Last modified on May 29, 2026
Curate toolsMcpProxyHandler
On this page
  • Add the capability filter policy
  • Override a tool description
  • Override tool annotations
  • Project a resource
  • Block everything from a capability type
  • Example: read-only Linear
  • Verify the filter
  • Related
JSON
JSON
JSON
JSON
JSON
JSON
JSON
JSON
JSON