ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop on the web portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
    Develop locally with the CLI
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
Concepts
Development
Policies
    Policy Catalog
    Authentication
    Authorization
    MCP Authorization
    Security & Validation
      Rate LimitingComplex Rate LimitingAudit LogsRequest ValidationWeb Bot AuthBot DetectionPrompt Injection DetectionMCP Capability FilterRequire OriginRequest Size LimitSecret MaskingData Loss Prevention InboundData Loss Prevention OutboundStripe Webhook AuthAkamai AI Firewall
    Metrics, Billing & Quotas
    Testing
    Request Modification
    Response Modification
    Upstream Authentication
    GraphQL
    Other
    Guides
Handlers
API Keys
Rate Limiting
MCP Server
MCP Gateway
AI Gateway
Developer Portal
Monetization
GraphQL
Deploying & Source Control
Analytics
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Security & Validation

MCP Capability Filter Policy

MCP Gateway Policy

This policy is for use with the MCP Gateway. See the MCP Gateway documentation to learn how to proxy and secure MCP servers with Zuplo.

Curate the tools, prompts, resources, and resource templates an upstream MCP server exposes through the gateway.

Use this after McpTokenExchangeInboundPolicy to enforce a per-route allow-list on the upstream MCP capabilities. Each entry can be a name (or URI) string or a projection object that overrides the downstream-facing description, annotations, and _meta while keeping the upstream identity intact. Omit a capability option to pass that capability type through unchanged; use an empty array to expose none.

Configuration

The configuration shows how to configure the policy in the 'policies.json' document.

Code
{ "name": "my-mcp-capability-filter-inbound-policy", "policyType": "mcp-capability-filter-inbound", "handler": { "export": "McpCapabilityFilterInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "accessControl": { "groupClaim": "groups", "identifier": {}, "mode": "allPublic", "roleClaim": "roles" }, "prompts": [], "resourceTemplates": [], "resources": [], "tools": [] } } }

Policy Configuration

  • name <string> - The name of your policy instance. This is used as a reference in your routes.
  • policyType <string> - The identifier of the policy. This is used by the Zuplo UI. Value should be mcp-capability-filter-inbound.
  • handler.export <string> - The name of the exported type. Value should be McpCapabilityFilterInboundPolicy.
  • handler.module <string> - The module containing the policy. Value should be $import(@zuplo/runtime).
  • handler.options <object> - The options for this policy. See Policy Options below.

Policy Options

The options for this policy are specified below. All properties are optional unless specifically marked as required.

  • tools <undefined[]> - Tools to expose. Use a string for name-only filtering, or an object to expose and project tool description, annotations, and _meta. Omit to pass through all upstream tools; use an empty array to expose no tools.
  • prompts <undefined[]> - Prompts to expose. Use a string for name-only filtering, or an object to expose and project prompt description and _meta. Omit to pass through all upstream prompts; use an empty array to expose no prompts.
  • resources <undefined[]> - Resources to expose. Use a string for URI-only filtering, or an object to expose and project resource name, description, MIME type, and _meta. Omit to pass through all upstream resources; use an empty array to expose no resources.
  • resourceTemplates <undefined[]> - Resource templates to expose. Use a string for URI-template-only filtering, or an object to expose and project template name, description, MIME type, and _meta. Omit to pass through all upstream resource templates; use an empty array to expose no resource templates.
  • accessControl <object> - Optional per-caller access control. Defaults to mode 'allPublic' (no access control). Set mode to 'rolesAndGroups' to match the caller's role/group claims against each capability's markup (a capability with no roles/groups/public markup is available to no one), or 'function' to delegate to a custom resolver.
    • mode <string> - Access-control strategy. 'allPublic' (default): no access control; roles/groups/public markup is not allowed. 'rolesAndGroups': match the caller's role and group claims against each capability's markup. 'function': delegate to the resolver named by 'identifier'. Allowed values are allPublic, rolesAndGroups, function. Defaults to "allPublic".
    • roleClaim <string> - Property on request.user.data holding the caller's roles (commonly a JWT 'roles' claim, but request.user.data is set by whichever auth policy ran — JWT, API key, etc.), matched against each capability's 'roles'. Supports a dot-path for nested values, e.g. 'realm_access.roles'. Used in mode 'rolesAndGroups'. Defaults to 'roles'. Defaults to "roles".
    • groupClaim <string> - Property on request.user.data holding the caller's groups (commonly a JWT 'groups' claim, but request.user.data is set by whichever auth policy ran — JWT, API key, etc.), matched against each capability's 'groups'. Supports a dot-path for nested values, e.g. 'cognito'. Used in mode 'rolesAndGroups'. Defaults to 'groups'. Defaults to "groups".
    • identifier <object> - Custom capability resolver, required when mode is 'function'. It fully replaces built-in roles/groups matching.
      • module (required) <string> - Module to load the resolver from, e.g. $import(./modules/mcp-access-control).
      • export (required) <string> - Named export of the resolver function, e.g. 'default'.

Using the Policy

Overview

The mcp-capability-filter-inbound policy curates the MCP capabilities exposed by a proxied upstream server. It filters successful JSON-RPC list responses and blocks direct JSON-RPC access to hidden tools, prompts, and resources before the request is forwarded upstream.

Omit a capability option to pass that capability type through unchanged. Set an option to an empty array to expose none of that capability type. Matching is case-sensitive and exact.

Each entry can be either a string identifier or a projection object. Projection objects still use the name/URI as the stable upstream identity, but can override the downstream-facing description, merge annotations for tools, and merge _meta for future metadata such as role hints. Keep input and output schemas out of this policy config; schemas are supplied by the upstream list response.

Configuration

Code
{ "name": "mcp-filter-stripe", "policyType": "mcp-capability-filter-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpCapabilityFilterInboundPolicy", "options": { "tools": [ { "name": "create_invoice", "description": "Create an invoice for accounting users.", "annotations": { "destructiveHint": false }, "_meta": { "roles": ["accounting"] } } ], "prompts": ["summarize_customer"], "resources": ["stripe://customers"], "resourceTemplates": ["stripe://customers/{id}"] } } }

Place this policy after mcp-token-exchange-inbound when the route uses gateway-managed upstream OAuth credentials. That order lets the token exchange policy retry or replace a 401 response first; this policy then filters the final upstream JSON-RPC response.

Access control

accessControl narrows the curated catalog per caller. Its mode selects the strategy:

  • allPublic (default): no access control — every curated capability is available to all callers. Setting roles/groups/public on a capability in this mode is a configuration error (it would silently do nothing).
  • rolesAndGroups: match the caller's roles and groups — read from request.user.data (see below) — against each capability's markup.
  • function: delegate to a custom resolver.

Mode rolesAndGroups

Mark each capability with roles and/or groups, or public: true. Nothing is ever exposed by omission — a capability with no markup stays in the catalog but is available to no one until you classify it (so you can list it now and assign roles later). Combining public: true with roles/groups is contradictory and rejected at load:

Code
{ "options": { "tools": [ { "name": "search_invoices", "roles": ["billing", "admin"] }, { "name": "create_invoice", "roles": ["admin"], "groups": ["finance"] }, { "name": "list_products", "public": true } ], "accessControl": { "mode": "rolesAndGroups" } } }

A caller may use a capability if it is public, or if the caller matches any listed role or any listed group (so create_invoice above is available to admins or to the finance team). Anonymous callers get only public capabilities. roles/groups/public are enforcement markup and are stripped from downstream responses — use _meta for hints you want clients to see.

The caller's roles and groups come from request.user.data — the object an upstream authentication policy sets for the authenticated caller. What lands there depends on the policy: a JWT auth policy stores the token's claims, an API key policy stores the key's metadata, and so on. This policy reads the roles and groups properties by default. For example, behind a JWT auth policy a token with these claims:

Code
{ "sub": "user_8f3a", "roles": ["billing"], "groups": ["sales"], "iss": "https://acme.us.auth0.com/", "aud": "https://mcp.acme.com", "exp": 1893456000 }

request.user.data.roles is ["billing"] and request.user.data.groups is ["sales"]. Against the config above, this caller may use search_invoices (matches the billing role) and the public list_products, but not create_invoice (which needs the admin role or the finance group). A request with no authenticated caller (no request.user) has no roles or groups, so it sees only public capabilities.

Use roleClaim/groupClaim to read different request.user.data properties, including a dot-path for nested values or a literal namespaced key:

Code
{ "accessControl": { "mode": "rolesAndGroups", "groupClaim": "cognito:groups" } }
Code
{ "accessControl": { "mode": "rolesAndGroups", "roleClaim": "realm_access.roles" } }

Mode function

For any other mapping — looking up entitlements from an external API, requiring both a role AND a group, blocklists, etc. — set mode: "function" and point identifier at a resolver (same { module, export } shape as rate-limit). It fully replaces the built-in roles/groups matching. It receives (request, context, options) and returns the allowed identifiers; the result is always clamped to the configured catalog, so a resolver can only narrow access. A capability type left as passthrough (no static list) is not narrowed. A resolver that throws or returns no value fails closed.

Code
{ "accessControl": { "mode": "function", "identifier": { "module": "$import(./modules/mcp-access-control)", "export": "default" } } }
Code
// modules/mcp-access-control.ts import { ZuploRequest, ZuploContext, environment } from "@zuplo/runtime"; import type { McpCapabilityFilterInboundPolicyOptions, AllowedCapabilities, } from "@zuplo/runtime/mcp-gateway"; export default async function resolveCapabilities( request: ZuploRequest, context: ZuploContext, options: McpCapabilityFilterInboundPolicyOptions, ): Promise<AllowedCapabilities> { const subject = request.user?.sub; if (!subject) return { tools: [] }; // fail closed for anonymous const res = await fetch( `https://entitlements.example.com/users/${encodeURIComponent(subject)}`, { headers: { authorization: `Bearer ${environment.ENTITLEMENTS_API_KEY}` }, }, ); if (!res.ok) { context.log.warn(`entitlements lookup failed: ${res.status}`); return { tools: [] }; } const { allowedTools } = (await res.json()) as { allowedTools: string[] }; return { tools: allowedTools }; }

Batch Requests

For JSON-RPC batch requests, list responses are filtered per response item when the item id can be matched to the original list request. If any batch item directly calls a hidden tool, prompt, or resource, the policy blocks the whole batch with a not-found JSON-RPC error response.

Read more about how policies work

Edit this page
Last modified on July 25, 2026
Prompt Injection DetectionRequire Origin
On this page
  • Configuration
    • Policy Configuration
    • Policy Options
  • Using the Policy
  • Overview
  • Configuration
  • Access control
    • Mode rolesAndGroups
    • Mode function
  • Batch Requests
JSON
JSON
JSON
JSON
JSON
JSON
JSON
TypeScript