zuplo 6.70.53 → 6.70.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/mcp-gateway/auth/configuring-auth0.mdx +216 -0
- package/docs/mcp-gateway/auth/configuring-clerk.mdx +153 -0
- package/docs/mcp-gateway/auth/configuring-cognito.mdx +128 -0
- package/docs/mcp-gateway/auth/configuring-entra.mdx +134 -0
- package/docs/mcp-gateway/auth/configuring-generic-oidc.mdx +242 -0
- package/docs/mcp-gateway/auth/configuring-google.mdx +117 -0
- package/docs/mcp-gateway/auth/configuring-keycloak.mdx +125 -0
- package/docs/mcp-gateway/auth/configuring-logto.mdx +116 -0
- package/docs/mcp-gateway/auth/configuring-okta.mdx +199 -0
- package/docs/mcp-gateway/auth/configuring-onelogin.mdx +122 -0
- package/docs/mcp-gateway/auth/configuring-ping.mdx +157 -0
- package/docs/mcp-gateway/auth/configuring-workos.mdx +117 -0
- package/docs/mcp-gateway/auth/manual-oauth-testing.mdx +528 -0
- package/docs/mcp-gateway/auth/overview.mdx +314 -0
- package/docs/mcp-gateway/auth/upstream-oauth.mdx +221 -0
- package/docs/mcp-gateway/capability-filtering.mdx +162 -0
- package/docs/mcp-gateway/code-config/compatibility-dates.mdx +33 -0
- package/docs/mcp-gateway/code-config/local-development.mdx +198 -0
- package/docs/mcp-gateway/code-config/mcp-proxy-handler.mdx +186 -0
- package/docs/mcp-gateway/code-config/multi-upstream.mdx +293 -0
- package/docs/mcp-gateway/code-config/overview.mdx +210 -0
- package/docs/mcp-gateway/connect-clients/chatgpt.mdx +127 -0
- package/docs/mcp-gateway/connect-clients/claude-code.mdx +184 -0
- package/docs/mcp-gateway/connect-clients/claude-desktop.mdx +160 -0
- package/docs/mcp-gateway/connect-clients/cursor.mdx +100 -0
- package/docs/mcp-gateway/connect-clients/other-clients.mdx +207 -0
- package/docs/mcp-gateway/connect-clients/overview.mdx +137 -0
- package/docs/mcp-gateway/connect-clients/vs-code.mdx +128 -0
- package/docs/mcp-gateway/how-it-works.mdx +266 -0
- package/docs/mcp-gateway/how-to/connect-upstream-oauth.mdx +268 -0
- package/docs/mcp-gateway/how-to/curate-tools.mdx +278 -0
- package/docs/mcp-gateway/introduction.mdx +151 -0
- package/docs/mcp-gateway/observability/analytics.mdx +191 -0
- package/docs/mcp-gateway/observability/logging.mdx +191 -0
- package/docs/mcp-gateway/quickstart.mdx +266 -0
- package/docs/mcp-gateway/reference.mdx +148 -0
- package/docs/mcp-gateway/test-clients.mdx +130 -0
- package/docs/mcp-gateway/troubleshooting.mdx +228 -0
- package/docs/mcp-server/introduction.mdx +10 -0
- package/docs/mcp-server/openai-apps-sdk.mdx +12 -0
- package/docs/policies/_index.md +14 -0
- package/docs/policies/akamai-ai-firewall/schema.json +1 -0
- package/docs/policies/akamai-firewall-for-ai-inbound/schema.json +1 -0
- package/docs/policies/akamai-firewall-for-ai-outbound/schema.json +1 -0
- package/docs/policies/amberflo-metering-inbound/schema.json +1 -0
- package/docs/policies/api-key-inbound/schema.json +1 -0
- package/docs/policies/audit-log-inbound/schema.json +1 -0
- package/docs/policies/auth0-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/authzen-inbound/schema.json +1 -0
- package/docs/policies/axiomatics-authz-inbound/schema.json +1 -0
- package/docs/policies/basic-auth-inbound/schema.json +1 -0
- package/docs/policies/bot-detection-inbound/schema.json +1 -0
- package/docs/policies/brownout-inbound/schema.json +1 -0
- package/docs/policies/caching-inbound/schema.json +1 -0
- package/docs/policies/change-method-inbound/schema.json +1 -0
- package/docs/policies/clear-headers-inbound/schema.json +1 -0
- package/docs/policies/clear-headers-outbound/schema.json +1 -0
- package/docs/policies/clerk-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/cognito-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/comet-opik-tracing-inbound/schema.json +1 -0
- package/docs/policies/complex-rate-limit-inbound/schema.json +1 -0
- package/docs/policies/composite-inbound/schema.json +1 -0
- package/docs/policies/composite-outbound/schema.json +1 -0
- package/docs/policies/curity-phantom-token-inbound/schema.json +1 -0
- package/docs/policies/firebase-jwt-inbound/schema.json +1 -0
- package/docs/policies/formdata-to-json-inbound/schema.json +1 -0
- package/docs/policies/galileo-tracing-inbound/schema.json +1 -0
- package/docs/policies/geo-filter-inbound/schema.json +1 -0
- package/docs/policies/graphql-complexity-limit-inbound/schema.json +1 -0
- package/docs/policies/graphql-disable-introspection-inbound/schema.json +1 -0
- package/docs/policies/graphql-introspection-filter-outbound/schema.json +1 -0
- package/docs/policies/http-deprecation-outbound/schema.json +1 -0
- package/docs/policies/jwt-scopes-inbound/schema.json +1 -0
- package/docs/policies/ldap-auth-inbound/schema.json +1 -0
- package/docs/policies/mcp-auth0-oauth-inbound/doc.md +54 -0
- package/docs/policies/mcp-auth0-oauth-inbound/intro.md +7 -0
- package/docs/policies/mcp-auth0-oauth-inbound/schema.json +135 -0
- package/docs/policies/mcp-capability-filter-inbound/doc.md +58 -0
- package/docs/policies/mcp-capability-filter-inbound/intro.md +9 -0
- package/docs/policies/mcp-capability-filter-inbound/schema.json +212 -0
- package/docs/policies/mcp-clerk-oauth-inbound/doc.md +34 -0
- package/docs/policies/mcp-clerk-oauth-inbound/intro.md +1 -0
- package/docs/policies/mcp-clerk-oauth-inbound/schema.json +134 -0
- package/docs/policies/mcp-cognito-oauth-inbound/doc.md +52 -0
- package/docs/policies/mcp-cognito-oauth-inbound/intro.md +7 -0
- package/docs/policies/mcp-cognito-oauth-inbound/schema.json +152 -0
- package/docs/policies/mcp-entra-oauth-inbound/doc.md +51 -0
- package/docs/policies/mcp-entra-oauth-inbound/intro.md +6 -0
- package/docs/policies/mcp-entra-oauth-inbound/schema.json +131 -0
- package/docs/policies/mcp-google-oauth-inbound/doc.md +52 -0
- package/docs/policies/mcp-google-oauth-inbound/intro.md +6 -0
- package/docs/policies/mcp-google-oauth-inbound/schema.json +125 -0
- package/docs/policies/mcp-keycloak-oauth-inbound/doc.md +43 -0
- package/docs/policies/mcp-keycloak-oauth-inbound/intro.md +2 -0
- package/docs/policies/mcp-keycloak-oauth-inbound/schema.json +140 -0
- package/docs/policies/mcp-logto-oauth-inbound/doc.md +52 -0
- package/docs/policies/mcp-logto-oauth-inbound/intro.md +6 -0
- package/docs/policies/mcp-logto-oauth-inbound/schema.json +131 -0
- package/docs/policies/mcp-oauth-inbound/doc.md +70 -0
- package/docs/policies/mcp-oauth-inbound/intro.md +11 -0
- package/docs/policies/mcp-oauth-inbound/schema.json +177 -0
- package/docs/policies/mcp-okta-oauth-inbound/doc.md +61 -0
- package/docs/policies/mcp-okta-oauth-inbound/intro.md +7 -0
- package/docs/policies/mcp-okta-oauth-inbound/schema.json +137 -0
- package/docs/policies/mcp-onelogin-oauth-inbound/doc.md +50 -0
- package/docs/policies/mcp-onelogin-oauth-inbound/intro.md +6 -0
- package/docs/policies/mcp-onelogin-oauth-inbound/schema.json +131 -0
- package/docs/policies/mcp-ping-oauth-inbound/doc.md +80 -0
- package/docs/policies/mcp-ping-oauth-inbound/intro.md +7 -0
- package/docs/policies/mcp-ping-oauth-inbound/schema.json +151 -0
- package/docs/policies/mcp-token-exchange-inbound/doc.md +135 -0
- package/docs/policies/mcp-token-exchange-inbound/intro.md +6 -0
- package/docs/policies/mcp-token-exchange-inbound/schema.json +134 -0
- package/docs/policies/mcp-workos-oauth-inbound/doc.md +50 -0
- package/docs/policies/mcp-workos-oauth-inbound/intro.md +6 -0
- package/docs/policies/mcp-workos-oauth-inbound/schema.json +125 -0
- package/docs/policies/mock-api-inbound/schema.json +1 -0
- package/docs/policies/moesif-inbound/schema.json +1 -0
- package/docs/policies/monetization-inbound/schema.json +1 -0
- package/docs/policies/mtls-auth-inbound/schema.json +1 -0
- package/docs/policies/okta-fga-authz-inbound/schema.json +1 -0
- package/docs/policies/okta-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/open-id-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/openfga-authz-inbound/schema.json +1 -0
- package/docs/policies/openmeter-inbound/schema.json +1 -0
- package/docs/policies/prompt-injection-outbound/schema.json +1 -0
- package/docs/policies/propel-auth-jwt-inbound/schema.json +1 -0
- package/docs/policies/query-param-to-header-inbound/schema.json +1 -0
- package/docs/policies/quota-inbound/schema.json +1 -0
- package/docs/policies/rate-limit-inbound/schema.json +1 -0
- package/docs/policies/readme-metrics-inbound/schema.json +1 -0
- package/docs/policies/remove-headers-inbound/schema.json +1 -0
- package/docs/policies/remove-headers-outbound/schema.json +1 -0
- package/docs/policies/remove-query-params-inbound/schema.json +1 -0
- package/docs/policies/replace-string-outbound/schema.json +1 -0
- package/docs/policies/request-size-limit-inbound/schema.json +1 -0
- package/docs/policies/request-validation-inbound/schema.json +1 -0
- package/docs/policies/require-origin-inbound/schema.json +1 -0
- package/docs/policies/secret-masking-outbound/schema.json +1 -0
- package/docs/policies/semantic-cache-inbound/schema.json +1 -0
- package/docs/policies/set-body-inbound/schema.json +1 -0
- package/docs/policies/set-headers-inbound/schema.json +1 -0
- package/docs/policies/set-headers-outbound/schema.json +1 -0
- package/docs/policies/set-query-params-inbound/schema.json +1 -0
- package/docs/policies/set-status-outbound/schema.json +1 -0
- package/docs/policies/set-upstream-api-key-inbound/schema.json +1 -0
- package/docs/policies/sleep-inbound/schema.json +1 -0
- package/docs/policies/stripe-webhook-verification-inbound/schema.json +1 -0
- package/docs/policies/supabase-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-azure-ad-service-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-firebase-admin-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-firebase-user-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-gcp-federated-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-gcp-jwt-inbound/schema.json +1 -0
- package/docs/policies/upstream-gcp-service-auth-inbound/schema.json +1 -0
- package/docs/policies/upstream-zuplo-jwt-auth-inbound/schema.json +1 -0
- package/docs/policies/validate-json-schema-inbound/schema.json +1 -0
- package/docs/policies/web-bot-auth-inbound/schema.json +1 -0
- package/docs/policies/xml-to-json-outbound/schema.json +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Capability filtering"
|
|
3
|
+
sidebar_label: "Capability filtering"
|
|
4
|
+
description:
|
|
5
|
+
How the Zuplo MCP Gateway curates the tools, prompts, resources, and resource
|
|
6
|
+
templates an upstream MCP server exposes — what the
|
|
7
|
+
mcp-capability-filter-inbound policy filters, how projections work, and where
|
|
8
|
+
the boundary actually lives.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
The Model Context Protocol lets a server advertise tools, prompts, resources,
|
|
12
|
+
and resource templates. When the Zuplo MCP Gateway proxies an upstream server,
|
|
13
|
+
every one of those capabilities flows through to the client by default. That's
|
|
14
|
+
the right behavior when the upstream is small and trusted, and the wrong
|
|
15
|
+
behavior when the upstream exposes dozens of operations only a few of which
|
|
16
|
+
belong in front of an AI client.
|
|
17
|
+
|
|
18
|
+
The `mcp-capability-filter-inbound` policy is how the gateway curates that
|
|
19
|
+
surface area. This page covers what the policy filters, the rules that govern
|
|
20
|
+
when capabilities are exposed versus hidden, the projection model that lets the
|
|
21
|
+
gateway rewrite descriptions, and the boundary the filter actually enforces.
|
|
22
|
+
|
|
23
|
+
To attach the policy to a route and walk through worked examples, see
|
|
24
|
+
[Curate the tools an upstream exposes](./how-to/curate-tools.mdx).
|
|
25
|
+
|
|
26
|
+
## What the policy filters
|
|
27
|
+
|
|
28
|
+
The policy operates on four MCP capability types, each matched by the upstream
|
|
29
|
+
identifier the protocol uses:
|
|
30
|
+
|
|
31
|
+
| Capability | Matched by | List method | Invocation method |
|
|
32
|
+
| ------------------- | ------------- | -------------------------- | ----------------- |
|
|
33
|
+
| `tools` | `name` | `tools/list` | `tools/call` |
|
|
34
|
+
| `prompts` | `name` | `prompts/list` | `prompts/get` |
|
|
35
|
+
| `resources` | `uri` | `resources/list` | `resources/read` |
|
|
36
|
+
| `resourceTemplates` | `uriTemplate` | `resources/templates/list` | `resources/read` |
|
|
37
|
+
|
|
38
|
+
Matching is case-sensitive and exact. There's no regex, glob, or category
|
|
39
|
+
matching — if the upstream returns a tool named `createUser` and the policy
|
|
40
|
+
lists `create_user`, the tool stays hidden.
|
|
41
|
+
|
|
42
|
+
## Omit versus empty array
|
|
43
|
+
|
|
44
|
+
The behavior of each option depends on whether it's present at all:
|
|
45
|
+
|
|
46
|
+
- **Omit the option** — every capability of that type passes through unchanged.
|
|
47
|
+
This is the default and is useful when filtering tools but leaving prompts and
|
|
48
|
+
resources alone.
|
|
49
|
+
- **Provide an empty array** — expose nothing of that type. The list response
|
|
50
|
+
becomes empty and every direct call returns `MethodNotFound`.
|
|
51
|
+
- **Provide entries** — expose only the listed items. Everything else is
|
|
52
|
+
filtered or blocked.
|
|
53
|
+
|
|
54
|
+
The omit-versus-empty-array distinction is the single most consequential rule in
|
|
55
|
+
the filter. Omitting an option is a pass-through; an empty array is the opposite
|
|
56
|
+
— it hides every capability of that type. Confusing the two is the most common
|
|
57
|
+
source of "why can the client still see that tool?" reports.
|
|
58
|
+
|
|
59
|
+
## Projections
|
|
60
|
+
|
|
61
|
+
Each allow-list entry is either a plain string (name only) or a projection
|
|
62
|
+
object that keeps the upstream identifier but overrides what the client sees.
|
|
63
|
+
Projections let the gateway rewrite the description for clarity, override tool
|
|
64
|
+
annotations like `destructiveHint` or `readOnlyHint`, attach `_meta` fields that
|
|
65
|
+
downstream middleware reads, or rewrite a resource's `name` and `mimeType` for a
|
|
66
|
+
curated catalog.
|
|
67
|
+
|
|
68
|
+
The upstream identifier — `name` for tools and prompts, `uri` for resources,
|
|
69
|
+
`uriTemplate` for resource templates — is always required and serves as the
|
|
70
|
+
stable match key. Annotation and `_meta` overrides are deep-merged with the
|
|
71
|
+
upstream values: fields the projection specifies win, fields it doesn't specify
|
|
72
|
+
pass through.
|
|
73
|
+
|
|
74
|
+
Schema fields stay upstream. `inputSchema` and `outputSchema` always come from
|
|
75
|
+
the upstream list response — the projection can't rewrite parameter shapes or
|
|
76
|
+
enforce additional validation. A separate policy on the route handles those
|
|
77
|
+
concerns when they come up.
|
|
78
|
+
|
|
79
|
+
## How the filter behaves at runtime
|
|
80
|
+
|
|
81
|
+
When the gateway sees a successful response to `tools/list`, `prompts/list`,
|
|
82
|
+
`resources/list`, or `resources/templates/list`, it reads the list from the
|
|
83
|
+
upstream response, keeps only items whose identifier appears on the allow-list,
|
|
84
|
+
merges any projection overrides into the kept items, and returns the filtered
|
|
85
|
+
list. Items the upstream returned that aren't on the allow-list are silently
|
|
86
|
+
dropped — the client never learns they exist.
|
|
87
|
+
|
|
88
|
+
When the gateway sees `tools/call`, `prompts/get`, or `resources/read`, it reads
|
|
89
|
+
the target identifier from the request (`params.name` for tools and prompts,
|
|
90
|
+
`params.uri` for resources). If the identifier isn't on the matching allow-list,
|
|
91
|
+
the gateway returns a JSON-RPC `MethodNotFound` error **before forwarding
|
|
92
|
+
upstream**:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"jsonrpc": "2.0",
|
|
97
|
+
"id": "1",
|
|
98
|
+
"error": {
|
|
99
|
+
"code": -32601,
|
|
100
|
+
"message": "Method not found"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The filter blocks calls before forwarding upstream, so a client that already
|
|
106
|
+
knows a hidden tool's name — from a cached `tools/list`, a different gateway, or
|
|
107
|
+
guesswork — still can't invoke it. The same block fires when the option is set
|
|
108
|
+
to an empty array: every direct call of that capability type returns
|
|
109
|
+
`MethodNotFound`.
|
|
110
|
+
|
|
111
|
+
## Batch requests
|
|
112
|
+
|
|
113
|
+
The policy handles JSON-RPC batch requests with two rules. List responses inside
|
|
114
|
+
a batch are filtered per item — the policy matches each response item to its
|
|
115
|
+
originating list request by ID and applies the same filtering and projection
|
|
116
|
+
rules as for a single response. Hidden invocations inside a batch block the
|
|
117
|
+
whole batch with a single `MethodNotFound` error; the gateway does not split,
|
|
118
|
+
partially filter, or forward sibling items.
|
|
119
|
+
|
|
120
|
+
## Where it sits in the policy chain
|
|
121
|
+
|
|
122
|
+
The capability filter belongs **after** any policy that produces or replaces the
|
|
123
|
+
upstream response — `mcp-token-exchange-inbound` is the most common one. The
|
|
124
|
+
filter operates on the final response, so policies that transform the response
|
|
125
|
+
upstream of it have already done their work by the time the filter runs.
|
|
126
|
+
|
|
127
|
+
Keep the filter last in the chain even when there's no
|
|
128
|
+
`mcp-token-exchange-inbound` policy on the route (for example, an API-key
|
|
129
|
+
upstream via `set-headers-inbound` or `set-upstream-api-key-inbound`), so any
|
|
130
|
+
future inbound policies that produce or replace responses run before it.
|
|
131
|
+
|
|
132
|
+
## What the filter does not do
|
|
133
|
+
|
|
134
|
+
A few capabilities are intentionally out of scope:
|
|
135
|
+
|
|
136
|
+
- **No schema overrides.** `inputSchema` and `outputSchema` always come from the
|
|
137
|
+
upstream list response.
|
|
138
|
+
- **No regex, glob, or category matching.** Allow-lists are exact, by
|
|
139
|
+
identifier. If the upstream renames a tool, the policy entry must be updated
|
|
140
|
+
to match.
|
|
141
|
+
- **No non-JSON filtering.** Filtering applies only to JSON responses. Streamed
|
|
142
|
+
or binary responses pass through untouched.
|
|
143
|
+
- **No effect on capability metadata in `initialize`.** The protocol-level
|
|
144
|
+
`serverCapabilities` block in the `initialize` response advertises which
|
|
145
|
+
capability types the server supports (tools, prompts, resources). The filter
|
|
146
|
+
doesn't strip those flags. A client sees that the gateway supports tools even
|
|
147
|
+
when the tool allow-list is empty; only the list and call responses change.
|
|
148
|
+
- **No quota or rate limit.** Capability filtering trims the surface area the
|
|
149
|
+
gateway exposes but doesn't bound how often clients can call what remains.
|
|
150
|
+
Pair it with the [`rate-limit-inbound`](../policies/rate-limit-inbound.mdx)
|
|
151
|
+
policy when usage controls are needed.
|
|
152
|
+
|
|
153
|
+
## Related
|
|
154
|
+
|
|
155
|
+
- [Curate the tools an upstream exposes](./how-to/curate-tools.mdx) — how to
|
|
156
|
+
attach the policy, override descriptions, and verify the filter is active.
|
|
157
|
+
- [`McpProxyHandler` reference](./code-config/mcp-proxy-handler.mdx) — the route
|
|
158
|
+
handler the filter runs in front of.
|
|
159
|
+
- [Per-user OAuth to upstream MCP servers](./auth/upstream-oauth.mdx) — the
|
|
160
|
+
upstream side of the picture; the filter usually composes with the
|
|
161
|
+
token-exchange policy on the same route.
|
|
162
|
+
- [MCP capability semantics in the specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Compatibility dates"
|
|
3
|
+
sidebar_label: "Compatibility dates"
|
|
4
|
+
description:
|
|
5
|
+
The Zuplo MCP Gateway requires compatibilityDate 2026-03-01 or later in
|
|
6
|
+
zuplo.jsonc.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
The Zuplo MCP Gateway requires `compatibilityDate >= 2026-03-01` in
|
|
10
|
+
`zuplo.jsonc`.
|
|
11
|
+
|
|
12
|
+
```jsonc
|
|
13
|
+
// zuplo.jsonc
|
|
14
|
+
{
|
|
15
|
+
"version": 1,
|
|
16
|
+
"compatibilityDate": "2026-03-01",
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
:::caution
|
|
21
|
+
|
|
22
|
+
The build fails if your project uses any MCP Gateway feature (the
|
|
23
|
+
`McpProxyHandler` handler or an `mcp-*-inbound` policy) with a compatibility
|
|
24
|
+
date older than `2026-03-01`. Bump the date in `zuplo.jsonc` before adding those
|
|
25
|
+
features.
|
|
26
|
+
|
|
27
|
+
:::
|
|
28
|
+
|
|
29
|
+
New Zuplo projects default to a recent compatibility date, so this only applies
|
|
30
|
+
to existing projects being upgraded to use the MCP Gateway.
|
|
31
|
+
|
|
32
|
+
For background on Zuplo's compatibility-date system in general, see
|
|
33
|
+
[Compatibility dates](../../programmable-api/compatibility-dates.mdx).
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Local development"
|
|
3
|
+
sidebar_label: "Local development"
|
|
4
|
+
description:
|
|
5
|
+
Run the Zuplo MCP Gateway locally with zuplo dev, bypass your identity
|
|
6
|
+
provider with the loopback /oauth/dev-login shortcut, wire up an MCP client
|
|
7
|
+
against 127.0.0.1, and recover cleanly from the known workerd restart quirk.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
The MCP Gateway runs the same way locally as any Zuplo project — `zuplo dev`,
|
|
11
|
+
port `9000`, hot reload on file changes. A few details are specific to the
|
|
12
|
+
gateway: the gateway prefers `127.0.0.1` over `localhost`, OAuth login can be
|
|
13
|
+
short-circuited entirely in dev, and the local `workerd` worker needs a full
|
|
14
|
+
restart after some MCP client connect attempts.
|
|
15
|
+
|
|
16
|
+
## Start the gateway
|
|
17
|
+
|
|
18
|
+
From the project root:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
zuplo dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The gateway listens at `http://127.0.0.1:9000`. Each MCP route in
|
|
25
|
+
`routes.oas.json` becomes reachable at that origin — for example
|
|
26
|
+
`http://127.0.0.1:9000/mcp/linear-v1`.
|
|
27
|
+
|
|
28
|
+
## Prefer `127.0.0.1` over `localhost`
|
|
29
|
+
|
|
30
|
+
OAuth metadata, callback URLs, and the in-dev login shortcut all key off the
|
|
31
|
+
request origin. Other loopback aliases (`localhost`, `::1`, `[::1]`) can cause
|
|
32
|
+
subtle OAuth issues in local dev.
|
|
33
|
+
|
|
34
|
+
When configuring an MCP client locally, use `127.0.0.1`:
|
|
35
|
+
|
|
36
|
+
```jsonc
|
|
37
|
+
// Good
|
|
38
|
+
"url": "http://127.0.0.1:9000/mcp/linear-v1"
|
|
39
|
+
|
|
40
|
+
// Avoid in local dev — works for most things, breaks subtly for OAuth
|
|
41
|
+
"url": "http://localhost:9000/mcp/linear-v1"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The same applies to any callback or redirect URI you configure with an identity
|
|
45
|
+
provider for local testing.
|
|
46
|
+
|
|
47
|
+
## Bypass your IdP with `/oauth/dev-login`
|
|
48
|
+
|
|
49
|
+
Setting up a real OIDC provider for local development is friction — you'd have
|
|
50
|
+
to register a localhost callback, manage test users, and so on. The gateway
|
|
51
|
+
exposes a loopback-only shortcut that skips the IdP round-trip entirely and
|
|
52
|
+
signs you in as a fixed `dev-browser-user` subject.
|
|
53
|
+
|
|
54
|
+
To use it, set `browserLogin.url` to the dev-login URL when configuring the
|
|
55
|
+
OAuth policy:
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
// config/policies.json — using the generic mcp-oauth-inbound policy
|
|
59
|
+
{
|
|
60
|
+
"name": "dev-oauth",
|
|
61
|
+
"policyType": "mcp-oauth-inbound",
|
|
62
|
+
"handler": {
|
|
63
|
+
"module": "$import(@zuplo/runtime/mcp-gateway)",
|
|
64
|
+
"export": "McpOAuthInboundPolicy",
|
|
65
|
+
"options": {
|
|
66
|
+
"oidc": {
|
|
67
|
+
"issuer": "http://127.0.0.1:9000",
|
|
68
|
+
"jwksUrl": "http://127.0.0.1:9000/.well-known/jwks.json",
|
|
69
|
+
},
|
|
70
|
+
"browserLogin": {
|
|
71
|
+
"url": "http://127.0.0.1:9000/oauth/dev-login",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
When `browserLogin.url` points at `/oauth/dev-login`, the
|
|
79
|
+
`browserLogin.tokenUrl`, `browserLogin.clientId`, and
|
|
80
|
+
`browserLogin.clientSecret` options aren't required. The consent page renders
|
|
81
|
+
normally.
|
|
82
|
+
|
|
83
|
+
:::caution
|
|
84
|
+
|
|
85
|
+
The `/oauth/dev-login` route returns `403 Forbidden` for any request that
|
|
86
|
+
doesn't arrive over loopback. It's not a security risk to leave configured for
|
|
87
|
+
production, but it's also not useful — production deployments should use a real
|
|
88
|
+
OIDC provider through one of the
|
|
89
|
+
[IdP-specific wrappers](../auth/overview.mdx#identity-providers).
|
|
90
|
+
|
|
91
|
+
:::
|
|
92
|
+
|
|
93
|
+
A common pattern is keeping two OAuth policies in the project — one for
|
|
94
|
+
production (Auth0, Okta, Entra, or any other supported IdP) and one for local
|
|
95
|
+
dev — and selecting between them in `routes.oas.json` based on the environment.
|
|
96
|
+
|
|
97
|
+
## Environment variables
|
|
98
|
+
|
|
99
|
+
When the OAuth policy reads from `$env(...)` references, define the values in a
|
|
100
|
+
`.env` file at the project root:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# .env
|
|
104
|
+
|
|
105
|
+
# Auth0 wrapper interpolations
|
|
106
|
+
AUTH0_DOMAIN=your-tenant.us.auth0.com
|
|
107
|
+
AUTH0_CLIENT_ID=your-auth0-web-app-client-id
|
|
108
|
+
AUTH0_CLIENT_SECRET=your-auth0-web-app-client-secret
|
|
109
|
+
|
|
110
|
+
# Optional: the audience the gateway requires on issued tokens
|
|
111
|
+
AUTH0_AUDIENCE=https://mcp-gateway.example.com
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`.env` is read at `zuplo dev` startup. Restart the dev server after adding or
|
|
115
|
+
changing an environment variable.
|
|
116
|
+
|
|
117
|
+
Never commit `.env` to source control. Instead, check in a `.env.example` (or
|
|
118
|
+
`env.example`) that documents which variables are required and an
|
|
119
|
+
empty/placeholder value for each.
|
|
120
|
+
|
|
121
|
+
## Adding the gateway to a local MCP client
|
|
122
|
+
|
|
123
|
+
Once `zuplo dev` is running and the route is reachable, add the gateway URL to
|
|
124
|
+
your MCP client config the same way you'd add any other remote MCP server. For
|
|
125
|
+
example, with Claude Desktop:
|
|
126
|
+
|
|
127
|
+
```jsonc
|
|
128
|
+
// claude_desktop_config.json
|
|
129
|
+
{
|
|
130
|
+
"mcpServers": {
|
|
131
|
+
"linear-via-zuplo-local": {
|
|
132
|
+
"url": "http://127.0.0.1:9000/mcp/linear-v1",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The client triggers the gateway's OAuth flow on first connect. With
|
|
139
|
+
`/oauth/dev-login` configured, the browser tab opens, lands on the consent page
|
|
140
|
+
without any IdP login, and you connect each upstream through its normal browser
|
|
141
|
+
OAuth flow. Subsequent calls reuse the issued tokens until they expire.
|
|
142
|
+
|
|
143
|
+
See [Connect MCP clients](../connect-clients/overview.mdx) for client-specific
|
|
144
|
+
snippets and the connect URL format.
|
|
145
|
+
|
|
146
|
+
## When `zuplo dev` crashes after a connect attempt
|
|
147
|
+
|
|
148
|
+
Some MCP client connect attempts can leave the local dev server in a state where
|
|
149
|
+
hot reload no longer recovers it. If the dev server stops responding after an
|
|
150
|
+
MCP client connects — particularly after browser OAuth callbacks finish — fully
|
|
151
|
+
restart `zuplo dev`:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Stop zuplo dev with Ctrl+C
|
|
155
|
+
# Start it again
|
|
156
|
+
zuplo dev
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Then have the MCP client reconnect. A restart doesn't force a re-consent — your
|
|
160
|
+
upstream tokens are still stored.
|
|
161
|
+
|
|
162
|
+
This is a known dev-only quirk and doesn't affect deployed gateways.
|
|
163
|
+
|
|
164
|
+
## Verifying the gateway is up
|
|
165
|
+
|
|
166
|
+
Two quick checks that don't require an MCP client:
|
|
167
|
+
|
|
168
|
+
**Fetch the well-known OAuth metadata for a route.** The path follows the
|
|
169
|
+
route's `operationId`:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
curl http://127.0.0.1:9000/.well-known/oauth-protected-resource/mcp/linear-v1
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A correct response is JSON with `resource`, `authorization_servers`,
|
|
176
|
+
`bearer_methods_supported`, and `scopes_supported` fields.
|
|
177
|
+
|
|
178
|
+
**Send a POST without a token.** The gateway should return `401` with a
|
|
179
|
+
`WWW-Authenticate` header pointing at the Protected Resource Metadata URL:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
curl -i -X POST http://127.0.0.1:9000/mcp/linear-v1 \
|
|
183
|
+
-H "Content-Type: application/json" \
|
|
184
|
+
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
If you see the 401 plus the challenge, the OAuth policy is wired up correctly.
|
|
188
|
+
The next call from a real client will then start the OAuth dance.
|
|
189
|
+
|
|
190
|
+
## Next steps
|
|
191
|
+
|
|
192
|
+
- [`McpProxyHandler` reference](./mcp-proxy-handler.mdx) — the route handler the
|
|
193
|
+
gateway uses for proxying.
|
|
194
|
+
- [Compatibility dates](./compatibility-dates.mdx) — pin `2026-03-01` in
|
|
195
|
+
`zuplo.jsonc`.
|
|
196
|
+
- [Multi-upstream pattern](./multi-upstream.mdx) — one project, many upstreams.
|
|
197
|
+
- [Connect MCP clients](../connect-clients/overview.mdx) — wire each client to
|
|
198
|
+
the local or deployed gateway URL.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "McpProxyHandler reference"
|
|
3
|
+
sidebar_label: "McpProxyHandler"
|
|
4
|
+
description:
|
|
5
|
+
The route handler that turns a Zuplo path into an MCP Gateway endpoint —
|
|
6
|
+
forwards POST requests to an upstream MCP server, rejects GET with 405, and
|
|
7
|
+
emits per-capability analytics on every call.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
`McpProxyHandler` is the route handler that backs every MCP Gateway route. It
|
|
11
|
+
accepts stateless Streamable HTTP requests over POST, forwards them to the
|
|
12
|
+
configured upstream MCP server using Zuplo's standard URL rewrite, and emits a
|
|
13
|
+
pair of analytics events per request so the gateway dashboard knows what each
|
|
14
|
+
capability call did.
|
|
15
|
+
|
|
16
|
+
## When to use it
|
|
17
|
+
|
|
18
|
+
Use `McpProxyHandler` on any route that proxies to an upstream MCP server. Pair
|
|
19
|
+
it with at least one MCP OAuth policy on the inbound chain; add an
|
|
20
|
+
`mcp-token-exchange-inbound` policy when the upstream itself requires OAuth, and
|
|
21
|
+
optionally `mcp-capability-filter-inbound` to curate what the upstream
|
|
22
|
+
advertises.
|
|
23
|
+
|
|
24
|
+
If the upstream uses a static API key or static header instead of OAuth, keep
|
|
25
|
+
the MCP OAuth policy on the route, drop the token exchange policy, and add
|
|
26
|
+
[`set-upstream-api-key-inbound`](../../policies/set-upstream-api-key-inbound.mdx)
|
|
27
|
+
or [`set-headers-inbound`](../../policies/set-headers-inbound.mdx) to attach the
|
|
28
|
+
credential before the handler forwards.
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
The handler is referenced from the route's `x-zuplo-route.handler` block in
|
|
33
|
+
`routes.oas.json`:
|
|
34
|
+
|
|
35
|
+
```jsonc
|
|
36
|
+
"x-zuplo-route": {
|
|
37
|
+
"corsPolicy": "none",
|
|
38
|
+
"handler": {
|
|
39
|
+
"module": "$import(@zuplo/runtime/mcp-gateway)",
|
|
40
|
+
"export": "McpProxyHandler",
|
|
41
|
+
"options": {
|
|
42
|
+
"rewritePattern": "https://mcp.linear.app/mcp"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"policies": {
|
|
46
|
+
"inbound": [
|
|
47
|
+
"auth0-managed-oauth",
|
|
48
|
+
"mcp-token-exchange-linear"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Set `corsPolicy` to `"none"`. MCP clients aren't browser-based and shouldn't be
|
|
55
|
+
sending ambient credentials.
|
|
56
|
+
|
|
57
|
+
## Options
|
|
58
|
+
|
|
59
|
+
### `rewritePattern` (required)
|
|
60
|
+
|
|
61
|
+
The upstream MCP server URL. The handler forwards each authenticated POST to
|
|
62
|
+
this URL, with the resolved upstream `Authorization: Bearer` header applied by
|
|
63
|
+
the token exchange policy.
|
|
64
|
+
|
|
65
|
+
Two value forms are supported:
|
|
66
|
+
|
|
67
|
+
- **A literal HTTPS or HTTP URL.** Used verbatim as the upstream target.
|
|
68
|
+
- **An environment-variable reference of the form `${env.X}`.** The variable
|
|
69
|
+
must resolve to a fully-qualified HTTP(S) URL.
|
|
70
|
+
|
|
71
|
+
```jsonc
|
|
72
|
+
// Literal URL
|
|
73
|
+
{ "rewritePattern": "https://mcp.linear.app/mcp" }
|
|
74
|
+
|
|
75
|
+
// Environment variable
|
|
76
|
+
{ "rewritePattern": "${env.UPSTREAM_MCP_URL}" }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Dynamic request-based patterns are explicitly rejected — MCP routes need a
|
|
80
|
+
stable upstream URL.
|
|
81
|
+
|
|
82
|
+
:::caution
|
|
83
|
+
|
|
84
|
+
The URL Rewrite handler's broader template syntax — `${params.x}`,
|
|
85
|
+
`${headers.get("x")}`, and so on — is **not** supported on `rewritePattern` for
|
|
86
|
+
MCP routes. Use a literal URL or an `${env.X}` reference.
|
|
87
|
+
|
|
88
|
+
:::
|
|
89
|
+
|
|
90
|
+
### `forwardSearch` (optional)
|
|
91
|
+
|
|
92
|
+
Type: `boolean`. Default: `true`.
|
|
93
|
+
|
|
94
|
+
When `true`, the inbound request's query string is appended to the upstream URL
|
|
95
|
+
before forwarding. Set to `false` to drop client query parameters.
|
|
96
|
+
|
|
97
|
+
### `followRedirects` (optional)
|
|
98
|
+
|
|
99
|
+
Type: `boolean`. Default: `false`.
|
|
100
|
+
|
|
101
|
+
When `false`, redirects from the upstream return as-is to the client (status
|
|
102
|
+
code and `Location` header passed through). Set to `true` to have the runtime
|
|
103
|
+
follow them transparently.
|
|
104
|
+
|
|
105
|
+
### `mtlsCertificate` (optional)
|
|
106
|
+
|
|
107
|
+
Type: `string`. The id of an mTLS certificate registered with the Zuplo project.
|
|
108
|
+
When set, the upstream fetch uses mutual TLS with the specified client
|
|
109
|
+
certificate. Most MCP upstreams don't require mTLS; leave this unset unless you
|
|
110
|
+
specifically need it.
|
|
111
|
+
|
|
112
|
+
## Behavior
|
|
113
|
+
|
|
114
|
+
### GET returns 405
|
|
115
|
+
|
|
116
|
+
The gateway only speaks stateless Streamable HTTP, and the MCP authorization
|
|
117
|
+
spec uses POST for every JSON-RPC call. A `GET` to an MCP route returns:
|
|
118
|
+
|
|
119
|
+
```http
|
|
120
|
+
HTTP/1.1 405 Method Not Allowed
|
|
121
|
+
Allow: POST
|
|
122
|
+
Content-Type: application/problem+json
|
|
123
|
+
|
|
124
|
+
{
|
|
125
|
+
"type": "https://httpproblems.com/http-status/405",
|
|
126
|
+
"status": 405,
|
|
127
|
+
"detail": "MCP Gateway routes support stateless Streamable HTTP requests over POST. Server-sent event GET streams are not supported."
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
If you've seen an MCP server that exposes a GET endpoint for SSE event streams,
|
|
132
|
+
that's a different transport. The Zuplo MCP Gateway is Streamable HTTP,
|
|
133
|
+
POST-only.
|
|
134
|
+
|
|
135
|
+
### POST forwards to the upstream
|
|
136
|
+
|
|
137
|
+
A POST request runs through the inbound policy chain, then the handler emits
|
|
138
|
+
capability analytics events, forwards to the upstream URL, and emits a
|
|
139
|
+
completion event with `outcome`, `mcpStatus`, `latencyMs`, and any JSON-RPC
|
|
140
|
+
error details.
|
|
141
|
+
|
|
142
|
+
Inbound auth headers don't leak to the upstream — the gateway-issued bearer
|
|
143
|
+
token is stripped, and the token exchange policy sets the upstream's own
|
|
144
|
+
`Authorization: Bearer <upstream-token>` header.
|
|
145
|
+
|
|
146
|
+
## Route requirements
|
|
147
|
+
|
|
148
|
+
Every route that uses `McpProxyHandler` must:
|
|
149
|
+
|
|
150
|
+
- **Set `operationId`.** It's used to identify the MCP route.
|
|
151
|
+
- **Include an MCP OAuth policy** in the inbound chain — one of the
|
|
152
|
+
[IdP-specific wrappers](../auth/overview.mdx#identity-providers) (Auth0,
|
|
153
|
+
Cognito, Clerk, Entra, Google, Keycloak, Logto, Okta, OneLogin, Ping, WorkOS)
|
|
154
|
+
or the generic `mcp-oauth-inbound`.
|
|
155
|
+
- **Include at most one `mcp-token-exchange-inbound` policy.**
|
|
156
|
+
|
|
157
|
+
Across the project:
|
|
158
|
+
|
|
159
|
+
- No two MCP routes can share an `operationId`.
|
|
160
|
+
- No two MCP routes can share a path.
|
|
161
|
+
- No two `mcp-token-exchange-*` policies can share an upstream `id`.
|
|
162
|
+
|
|
163
|
+
## Analytics
|
|
164
|
+
|
|
165
|
+
Every POST emits two analytics events when the request body parses as a JSON-RPC
|
|
166
|
+
call:
|
|
167
|
+
|
|
168
|
+
- A **`capability_invocation_started`** event fired before the upstream fetch,
|
|
169
|
+
carrying the parsed `mcpMethod` and `capabilityName`.
|
|
170
|
+
- A **`capability_invocation_completed`** event fired after the response,
|
|
171
|
+
carrying `outcome`, `mcpStatus`, `latencyMs`, and any JSON-RPC error details.
|
|
172
|
+
|
|
173
|
+
Each event also includes the route's `operationId` (as `virtualServerName`), the
|
|
174
|
+
upstream `id` (as `upstreamServerName`), the authenticated `subjectId`, the
|
|
175
|
+
`authProfileId`, and the `upstreamAuthMode`. See
|
|
176
|
+
[Analytics](../observability/analytics.mdx) for the dashboard view and
|
|
177
|
+
[Logging](../observability/logging.mdx) for the structured-log counterpart.
|
|
178
|
+
|
|
179
|
+
## Related
|
|
180
|
+
|
|
181
|
+
- `mcp-token-exchange-inbound` — resolves the upstream credential and handles
|
|
182
|
+
upstream 401 refresh and retry.
|
|
183
|
+
- `mcp-capability-filter-inbound` — curates the upstream surface area on a
|
|
184
|
+
per-route basis.
|
|
185
|
+
- [Multi-upstream pattern](./multi-upstream.mdx) — pair one `McpProxyHandler`
|
|
186
|
+
route with each upstream MCP server in one project.
|