zuplo 7.13.1 → 7.13.3

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.
@@ -146,11 +146,10 @@ const { text } = await generateText({
146
146
 
147
147
  ### xAI
148
148
 
149
- Call `xai.chat(...)` rather than `xai(...)`. The bare callable targets xAI's
150
- Responses API, and xAI's models do not serve `/v1/responses` through the
151
- gateway—an xAI model sent there returns a `400`. The endpoint is
152
- capability-gated per model rather than restricted to one provider, so which
153
- models reach it is the matrix in [AI Providers](../providers.mdx).
149
+ Use either `xai(...)` or `xai.chat(...)`. The bare callable targets xAI's
150
+ Responses API (`/v1/responses`), and `xai.chat(...)` targets
151
+ `/v1/chat/completions`. Use `xai(...)` for xAI's server-side tools, such as
152
+ `web_search` and `x_search`, because xAI runs them only on the Responses API.
154
153
 
155
154
  ```typescript
156
155
  import { createXai } from "@ai-sdk/xai";
@@ -46,7 +46,7 @@ The following capabilities are supported across providers:
46
46
  | Anthropic | ✅ | ❌ | ❌ | ✅ |
47
47
  | Google | ✅ | ✅ | ❌ | ❌ |
48
48
  | Mistral | ✅ | ✅ | ❌ | ❌ |
49
- | xAI | ✅ | ✅ | ❌ | ❌ |
49
+ | xAI | ✅ | ✅ | ✅ | ❌ |
50
50
  | Azure AI | ✅ | ✅ | ✅ | ✅ |
51
51
  | Bedrock Mantle | ✅ | ❌ | ✅ | ✅ |
52
52
  | Bedrock Runtime | ❌ | ❌ | ❌ | ❌ |
@@ -70,7 +70,7 @@ clients that can't set a model still work.
70
70
  | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
71
71
  | `/v1/chat/completions` | Chat completions for every provider |
72
72
  | `/v1/embeddings` | Embeddings for the providers marked in the [capability matrix](./providers.mdx#supported-providers) |
73
- | `/v1/responses` | OpenAI Responses API: OpenAI, and the OpenAI-compatible models that serve it on [Azure AI](./azure-ai.mdx), [Bedrock Mantle](./bedrock-mantle.mdx) and [OpenRouter](./openrouter.mdx) |
73
+ | `/v1/responses` | OpenAI Responses API: OpenAI, xAI, and the OpenAI-compatible models that serve it on [Azure AI](./azure-ai.mdx), [Bedrock Mantle](./bedrock-mantle.mdx) and [OpenRouter](./openrouter.mdx) |
74
74
  | `/v1/messages` | Anthropic Messages API: Anthropic, and the Claude models of [Azure AI](./azure-ai.mdx), [Bedrock Mantle](./bedrock-mantle.mdx), [Vertex AI](./vertex-ai.mdx) and [OpenRouter](./openrouter.mdx) |
75
75
 
76
76
  Jev is the exception: it serves TypeSafe's System One API on `/v1/systemone`,
@@ -17,11 +17,75 @@ support window applies.
17
17
 
18
18
  ## At a glance
19
19
 
20
- | Surface | What controls compatibility | Support window |
21
- | -------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------- |
22
- | Gateway runtime | The `compatibilityDate` in your `zuplo.jsonc` | Indefinite. Existing compatibility dates keep working. |
23
- | Specification-based features (MCP, OAuth, OpenAPI) | The upstream specification's own lifecycle | Tracks the specification. Old revisions eventually drop. |
24
- | Developer Portal | The `zudoku` package version in your project | Current version only. Fixes aren't backported. |
20
+ | Surface | What controls compatibility | Support window |
21
+ | -------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------- |
22
+ | Gateway runtime | The `compatibilityDate` in your `zuplo.jsonc` | At least 12 months per compatibility date, likely longer. |
23
+ | Specification-based features (MCP, OAuth, OpenAPI) | The upstream specification's own lifecycle | Tracks the specification. Old revisions eventually drop. |
24
+ | Developer Portal | The `zudoku` package version in your project | Current version only. Fixes aren't backported. |
25
+
26
+ ## What counts as a breaking change
27
+
28
+ The compatibility promise focuses on the parts of Zuplo that a production API
29
+ depends on. Other parts of the platform, such as diagnostics and tooling, keep
30
+ improving between releases.
31
+
32
+ ### Breaking changes
33
+
34
+ Zuplo treats these as breaking and gates them behind a compatibility date:
35
+
36
+ - **Request handling.** A change that materially affects how the gateway handles
37
+ a request: which policies and handlers run and in what order, the status code,
38
+ headers, or body a client receives, how a request is routed, validated,
39
+ authenticated, rate limited, or forwarded.
40
+ - **Exported code APIs.** The documented, public exports of `@zuplo/runtime`
41
+ that custom code imports, such as
42
+ [`ZuploRequest`](../programmable-api/zuplo-request.mdx),
43
+ [`ZuploContext`](../programmable-api/zuplo-context.mdx), and
44
+ [`HttpProblems`](../programmable-api/http-problems.mdx). Removing an export,
45
+ changing a signature, or changing what a documented API returns is breaking.
46
+ - **Configuration.** A change that makes a valid `zuplo.jsonc`, route file, or
47
+ policy configuration fail to build, or that changes what an existing option
48
+ does. Stricter validation of invalid configuration is covered separately
49
+ below.
50
+
51
+ ### Not breaking changes
52
+
53
+ These can change from release to release without a compatibility date. Tests and
54
+ integrations hold up best when they don't depend on them:
55
+
56
+ - **Error messages.** The human-readable text of errors throughout Zuplo, in
57
+ runtime responses, build and deployment output, and the CLI, can be reworded,
58
+ expanded, or localized. For example, the `title` and `detail` fields of a
59
+ [Problem Details](../programmable-api/http-problems.mdx) response can change.
60
+ Match on stable identifiers such as the status code and the problem `type`
61
+ instead.
62
+ - **Additive changes.** New APIs, new policies, new optional configuration
63
+ properties, new response headers, and new fields in a response body or a
64
+ Problem Details object. Clients and tests should ignore fields and headers
65
+ they don't recognize.
66
+ - **Logs, traces, and metrics.** Log messages and their wording, which
67
+ operations emit logs or spans, span names and attributes, and which metrics
68
+ and analytics Zuplo collects all change over time. New log entries appear and
69
+ existing ones change, so avoid parsing log text to drive automation.
70
+ - **Local development.** The [local development](./local-development.mdx)
71
+ experience, including `zuplo dev`, the local Route Designer, and locally
72
+ mocked or emulated services, changes as the tooling improves. Behavior in
73
+ local development can differ from a deployed gateway. Treat a
74
+ [preview environment](./branch-based-deployments.mdx) as the source of truth
75
+ for production behavior.
76
+ - **Undocumented behavior and internal APIs.** Anything not described in the
77
+ documentation: exports not documented as public, imports from internal paths
78
+ of a Zuplo package, internal headers, and incidental behavior such as the
79
+ order of headers or the exact timing of a request.
80
+ - **Performance.** Latency, memory use, and throughput improve or shift between
81
+ releases. [Platform limits](./limits.mdx) are documented separately.
82
+ - **Stricter build validation.** Zuplo continues to add checks that catch
83
+ invalid configuration and code at build time instead of at request time. For
84
+ example, a policy that's misconfigured so that it fails on every request might
85
+ build and deploy today, then fail the build after a new check ships. Catching
86
+ these problems before deployment keeps them out of production, though it means
87
+ a project that built before can occasionally need a fix to build again. The
88
+ build error explains what's wrong and how to fix it.
25
89
 
26
90
  ## Gateway runtime
27
91
 
@@ -51,8 +115,9 @@ raising the date in `zuplo.jsonc`:
51
115
  ```
52
116
 
53
117
  New projects default to a recent compatibility date. Existing projects stay on
54
- the date they set until someone changes it, and Zuplo doesn't retire old
55
- compatibility dates on a schedule.
118
+ the date they set until someone changes it. Zuplo supports each compatibility
119
+ date for at least 12 months after it's released, and likely much longer. To
120
+ date, Zuplo hasn't retired any compatibility date.
56
121
 
57
122
  Every compatibility date and the behavior it changes is documented in
58
123
  [Compatibility Dates](../programmable-api/compatibility-dates.mdx). Each entry
@@ -66,18 +131,98 @@ that deployment, and merge once it passes.
66
131
 
67
132
  :::
68
133
 
69
- ### Security exceptions
134
+ ### Compatibility flags
70
135
 
71
- Security is the exception to the compatibility promise. To close a vulnerability
72
- or protect customer traffic, Zuplo may change runtime behavior without gating
73
- the change behind a compatibility date, including in ways that break existing
74
- projects.
136
+ Each behavior change in the runtime is implemented as a compatibility flag. A
137
+ compatibility date is a named set of flags: setting a date turns on every flag
138
+ that shipped with that date or an earlier one. Flags only ever turn new behavior
139
+ on. A project can't use a flag to turn off a behavior that its compatibility
140
+ date includes.
141
+
142
+ Zuplo occasionally ships a flag before it belongs to any compatibility date, to
143
+ resolve a specific customer issue or bug ahead of the next date. A project can
144
+ turn on such a flag with `compatibilityFlags` in `zuplo.jsonc`.
145
+
146
+ :::warning
147
+
148
+ Compatibility flags aren't documented or supported, and because they're
149
+ unstable, they aren't recommended for production projects.
150
+
151
+ :::
152
+
153
+ Until a flag becomes part of a compatibility date, it's considered unstable:
154
+
155
+ - **Its behavior can change** between releases while the change is refined.
156
+ - **It can be removed.** When a flag's behavior becomes standard or moves to
157
+ another flag, listing the old flag fails the build with a message explaining
158
+ what to use instead.
159
+ - **Stability guarantees apply to compatibility dates only.** While a project
160
+ sets any `compatibilityFlags`, the guarantees on this page don't apply to it,
161
+ even when the flags it sets are already part of its compatibility date.
162
+
163
+ When a flag becomes part of a compatibility date, raise the date and remove the
164
+ flag from `zuplo.jsonc`. Setting the date is the supported way to get the
165
+ behavior.
166
+
167
+ ## Exceptions to the compatibility promise
168
+
169
+ Zuplo works to avoid breaking changes, and gates every change it can behind a
170
+ compatibility date. In three situations, a change may need to ship outside a
171
+ compatibility date. Even then, Zuplo keeps the change as small as possible and
172
+ lets affected customers know.
173
+
174
+ ### Security
175
+
176
+ Protecting customer traffic comes first. To close a vulnerability, Zuplo may
177
+ change runtime behavior without a compatibility date, even when the fix affects
178
+ how some existing projects behave.
75
179
 
76
180
  Zuplo scopes these changes as narrowly as the vulnerability allows and
77
181
  communicates them as directly as responsible disclosure permits. For Zuplo's
78
182
  security practices and how to report a vulnerability, see
79
183
  [Security](./security.mdx).
80
184
 
185
+ ### Protocol correctness
186
+
187
+ Some parts of the gateway implement specifications Zuplo doesn't control — most
188
+ visibly the Model Context Protocol (MCP), along with OAuth, OpenID Connect,
189
+ OpenAPI, JSON Schema, and HTTP itself. Zuplo aims to implement these
190
+ specifications correctly, because correct behavior is what keeps the gateway
191
+ working with the clients and servers customers depend on.
192
+
193
+ These features follow the lifecycle of the specification:
194
+
195
+ - **Specification revisions.** When a specification deprecates or removes a
196
+ revision, Zuplo eventually ends support for that revision in line with the
197
+ specification's deprecation policy.
198
+ - **Specification changes.** When a specification changes in a way that can't be
199
+ both backward compatible and compliant, Zuplo follows the specification.
200
+ - **Conformance fixes.** From time to time, Zuplo finds a bug or behavior that's
201
+ inconsistent with a specification. Zuplo fixes these so the gateway behaves as
202
+ the specification describes.
203
+
204
+ Where a protocol change can be gated, Zuplo gates it. The MCP Gateway, for
205
+ example, requires a compatibility date of `2026-03-01` or later — see
206
+ [MCP Gateway compatibility dates](../mcp-gateway/code-config/compatibility-dates.mdx).
207
+
208
+ ### Official SDK dependencies
209
+
210
+ Some features are built on an official SDK for a protocol rather than a Zuplo
211
+ implementation. For example, the [MCP Server](../mcp-server/overview.mdx) and
212
+ [MCP Gateway](../mcp-gateway/overview.mdx) use the official MCP SDK for parts of
213
+ their protocol handling.
214
+
215
+ When Zuplo updates an SDK to pick up a new protocol revision, a security fix, or
216
+ a bug fix, behavior that comes from the SDK can change with it. SDK-driven
217
+ changes follow the SDK's own release cycle rather than Zuplo's compatibility
218
+ dates, and Zuplo calls out notable ones in the
219
+ [changelog](https://zuplo.com/changelog).
220
+
221
+ MCP is evolving quickly, so the MCP Gateway and MCP Server track new protocol
222
+ revisions and SDK releases as they publish. Testing MCP integrations against a
223
+ preview environment before each production deployment is the best way to catch a
224
+ change early.
225
+
81
226
  ## Changes take effect when you deploy
82
227
 
83
228
  Zuplo doesn't rebuild or redeploy a running gateway on your behalf. A new
@@ -99,31 +244,6 @@ recommends a comprehensive test suite that gates every deployment. The
99
244
  preview environments, and in CI/CD. See [Testing Your API](./testing.mdx) for
100
245
  the recommended setup.
101
246
 
102
- ## Specification-based features
103
-
104
- Some parts of the gateway implement specifications Zuplo doesn't control — most
105
- visibly the Model Context Protocol (MCP), along with OAuth, OpenAPI, and related
106
- standards. These features follow the lifecycle of the specification rather than
107
- Zuplo's own compatibility policy.
108
-
109
- Two consequences:
110
-
111
- - When a specification deprecates or removes a revision, Zuplo eventually ends
112
- support for that revision in line with the specification's deprecation policy.
113
- - When a specification changes such that remaining backward compatible would
114
- mean falling out of compliance, Zuplo implements the breaking change. A
115
- non-compliant implementation breaks interoperability with the clients and
116
- servers customers need to work with, which is the larger break.
117
-
118
- MCP moves fastest here. The [MCP Gateway](../mcp-gateway/overview.mdx) and
119
- [MCP Server](../mcp-server/overview.mdx) track protocol revisions as the
120
- specification publishes them, and support for older revisions ends on the
121
- specification's schedule.
122
-
123
- Where a spec-driven change can be gated, Zuplo gates it. The MCP Gateway, for
124
- example, requires a compatibility date of `2026-03-01` or later — see
125
- [MCP Gateway compatibility dates](../mcp-gateway/code-config/compatibility-dates.mdx).
126
-
127
247
  ## Developer Portal
128
248
 
129
249
  The Developer Portal is a web application built on [Zudoku](https://zudoku.dev),
@@ -158,8 +278,8 @@ that need more than a dependency bump.
158
278
 
159
279
  When Zuplo introduces a breaking change:
160
280
 
161
- - **Notice comes first.** Zuplo publishes breaking changes in advance through
162
- the [changelog](https://zuplo.com/changelog) and documentation, and contacts
281
+ - **Changes are communicated.** Zuplo announces breaking changes through the
282
+ [changelog](https://zuplo.com/changelog) and documentation, and contacts
163
283
  affected customers directly where the impact is identifiable.
164
284
  - **Compatibility dates carry the change where possible.** Gating a runtime
165
285
  change behind a compatibility date leaves the timing of adoption to you.
@@ -169,8 +289,9 @@ When Zuplo introduces a breaking change:
169
289
  plan work directly with the Zuplo team on upgrade planning, testing, and
170
290
  rollout. See [Support](./support.mdx).
171
291
 
172
- Security changes are the exception and may ship with short notice, or none, when
173
- advance disclosure would put customers at risk.
292
+ For security, protocol, and SDK changes, the timing of notice depends on the
293
+ situation, such as responsible disclosure for a vulnerability. See
294
+ [Exceptions to the compatibility promise](#exceptions-to-the-compatibility-promise).
174
295
 
175
296
  ## Related resources
176
297
 
@@ -179,6 +300,8 @@ advance disclosure would put customers at risk.
179
300
  - [Project Configuration (`zuplo.jsonc`)](../programmable-api/zuplo-json.mdx) —
180
301
  where the compatibility date is set
181
302
  - [Testing Your API](./testing.mdx) — local, preview, and CI/CD testing
303
+ - [Problem Details Helper](../programmable-api/http-problems.mdx) — the error
304
+ format to assert on in tests
182
305
  - [Branch-Based Deployments](./branch-based-deployments.mdx) — how branches map
183
306
  to environments
184
307
  - [Updating Versions](../dev-portal/updating.mdx) — upgrading the Developer
@@ -1,10 +1,47 @@
1
1
  ## Overview
2
2
 
3
3
  The `mcp-capability-filter-inbound` policy curates the MCP capabilities exposed
4
- by a proxied upstream server. It filters successful JSON-RPC list responses and
5
- blocks direct JSON-RPC access to hidden tools, prompts, and resources before the
4
+ by a proxied upstream server. It filters successful JSON-RPC list responses,
5
+ whether the upstream answers with JSON or with a `text/event-stream`, and blocks
6
+ direct JSON-RPC access to hidden tools, prompts, and resources before the
6
7
  request is forwarded upstream.
7
8
 
9
+ Direct access is checked on `tools/call` and `prompts/get` (by `params.name`),
10
+ on `resources/read`, `resources/subscribe` and `resources/unsubscribe` (by
11
+ `params.uri`), and on `completion/complete`: a `ref/prompt` reference's `name`
12
+ against `prompts`, a `ref/resource` reference's `uri` against `resources` and
13
+ `resourceTemplates` (either configured list may allow it, since the URI can be a
14
+ concrete resource or a template). A URI with a template expression (`{name}`) is
15
+ never a concrete resource, so it passes while `resourceTemplates` is omitted,
16
+ whatever `resources` lists. A completion whose `ref` has any other shape (an
17
+ unknown or missing `type`, no `ref`) is refused while `prompts`, `resources` or
18
+ `resourceTemplates` is configured.
19
+
20
+ A call to a hidden capability is not forwarded. It is answered exactly as MCP
21
+ answers a capability that does not exist, so a client cannot tell the two apart:
22
+
23
+ | Request | Answer |
24
+ | --------------------------------------------- | ----------------------------------------------------------------------------------- |
25
+ | `tools/call` of a hidden tool | `-32602`, `Tool "<name>" not found` |
26
+ | `prompts/get` of a hidden prompt | `-32602`, `Prompt "<name>" not found` |
27
+ | `resources/read`, `subscribe`, `unsubscribe` | `-32002`, `Resource not found`, with `data.uri` |
28
+ | `completion/complete` of a hidden reference | `-32602`, `Prompt "<name>" not found` or `Resource template "<uri>" not found` |
29
+ | Any of these without a string `name` or `uri` | `-32602`, for example `Invalid params for tools/call: params.name must be a string` |
30
+
31
+ A method the policy checks or filters spelled another way, in another case or
32
+ with surrounding spaces (`Tools/Call`, ` tools/list`), is answered `-32601`
33
+ (`Method not found`): it is no MCP method, and an upstream that matched it
34
+ leniently would run it unchecked.
35
+
36
+ A resource URI is allowed when `resources` lists it, or when it matches a
37
+ template that `resourceTemplates` allows, as the MCP SDK matches templates. With
38
+ the configuration below, `stripe://customers/cus_1` is readable from the
39
+ `stripe://customers/{id}` template, and a URI from any other template, such as
40
+ `stripe://invoices/in_1`, is refused. When you omit `resources`, a URI passes
41
+ unless it matches only templates hidden from the caller: the policy cannot tell
42
+ a concrete resource from a URI of a template it does not know, so configure
43
+ `resources` too when reads of unlisted templates must be refused.
44
+
8
45
  Omit a capability option to pass that capability type through unchanged. Set an
9
46
  option to an empty array to expose none of that capability type. Matching is
10
47
  case-sensitive and exact.
@@ -184,9 +221,143 @@ export default async function resolveCapabilities(
184
221
  }
185
222
  ```
186
223
 
224
+ ## Requests and responses the filter cannot read
225
+
226
+ The policy fails closed. It never forwards something it could not check:
227
+
228
+ | Case | Result |
229
+ | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
230
+ | Body is not JSON (`Content-Type` other than JSON), on any HTTP method | `415` problem+json, not forwarded |
231
+ | Body over 10 MiB | `413` problem+json, not forwarded |
232
+ | Body is JSON-typed but does not parse, or cannot be read (the upload failed) | `400`, `-32700`, not forwarded |
233
+ | Body is JSON but not a JSON-RPC message | `400`, `-32600` (its `id` when valid) |
234
+ | Batch with a member that is not a JSON-RPC 2.0 object (no or another `jsonrpc`, a nested array, a scalar) | `400`, one `-32600`, not forwarded |
235
+ | A `method` that is not a string (an array, number, `null` or object), alone or in a batch | `400`, one `-32600`, not forwarded |
236
+ | A non-ASCII key in a message, its `params` or `params.ref` (Go folds `paramſ` into `params`) | `400`, `-32600`, not forwarded |
237
+ | `jsonrpc`/`method`/`id`/`params`, `params.name`/`uri`/`ref` or `ref.type`/`name`/`uri` twice, in any case | `400`, `-32600`, not forwarded |
238
+ | A message key in another case (`Method`), or `params.Name`/`URI`/`Ref` or `ref.Name`/… on a direct call | `400`, `-32600`, not forwarded |
239
+ | A direct call whose `params` is not an object, or has no string name or URI | `-32602`, not forwarded |
240
+ | A method the policy checks or filters, in another case or with spaces (`Tools/Call`, ` tools/list`) | `-32601`, not forwarded |
241
+ | A refused call sent as a notification (no `id`) | `400`, `-32600`, no `id`, not forwarded |
242
+ | A refused call whose `id` is not a string or an integer (`1.5`, `null`) | `400`, `-32600`, no `id`, not forwarded |
243
+ | Access-control resolver throws or returns no value | `-32603` for each request that needed it |
244
+ | List reply the policy cannot parse, or with no or unknown media type | `-32603` instead of the reply |
245
+ | List reply in a content-coding the runtime does not decode (`deflate`, `zstd`) | `-32603` instead of the reply |
246
+ | List reply that is not a JSON-RPC result, error, notification or request | `-32603` instead of the reply |
247
+ | List result whose `result` is not an object, or whose list (`tools`, `prompts`, …) is missing or not an array | `-32603` instead of the reply |
248
+ | List result whose `nextCursor` is not a string (or `null`) | `-32603` instead of the reply |
249
+ | A JSON-RPC array answering a single (non-batch) request | `-32603` instead of the reply |
250
+ | Result holding a filtered list (`tools`, `prompts`, …) that its request did not list | `-32603` instead of the reply |
251
+ | Batch reply array: a member that is any of the `-32603` cases above, or whose `id` matches no request | Member dropped (see below) |
252
+ | SSE list reply: an event whose `data` is not JSON, or whose message is any of the `-32603` cases above | Event dropped |
253
+ | SSE list reply: an event with an unknown field, an `event` other than `message`, or a malformed `id`/`retry` | Event dropped |
254
+ | SSE list reply: an event over 10 MiB, or a final event without a closing blank line | Event dropped |
255
+ | SSE list reply: a comment with text | Text removed (`:` kept) |
256
+ | A list item without a string name or URI | Item dropped |
257
+
258
+ The key checks guard against an upstream that reads JSON keys case-insensitively
259
+ (as Go's `encoding/json` does) or keeps the first of two duplicates. Non-ASCII
260
+ keys and the repeated keys above are refused on every message, notifications
261
+ included, whatever its method: `{"params":{"name":1,"Name":2}}` is a `400` even
262
+ on `notifications/initialized`. A single case variant in `params` or
263
+ `params.ref` (`Name`, `URI`, `Ref`) is refused only on a direct call; keys
264
+ nested deeper, such as `params.arguments.Name`, are not checked.
265
+
266
+ A request without a body, or with an empty one, passes through whatever its HTTP
267
+ method (`GET`, a `DELETE` that ends a session, an empty `POST`). The policy
268
+ reads a body it cannot parse only as far as its answer needs: one that is not
269
+ JSON up to its first non-blank character, a JSON one up to the 10 MiB limit. A
270
+ non-2xx reply passes through with its status and headers (a problem details or
271
+ OAuth error body, an empty body, one that is not JSON or is still compressed),
272
+ unless it is a JSON reply with a `result` or a batch, which is filtered.
273
+
274
+ A list reply the policy cannot read is replaced by a JSON-RPC `-32603` error for
275
+ the list request, with its `id` (an array of errors for a batch), sent as
276
+ `application/json` with HTTP `200`; the error carries none of the reply. Every
277
+ reply the policy does not forward (a replaced reply, a dropped event or batch
278
+ member) is logged as a warning, without any of its content.
279
+
280
+ A request with a list method the policy filters is sent upstream with
281
+ `Accept-Encoding: gzip, br`, replacing the client's value, so the upstream
282
+ answers in a content-coding the runtime decodes. Other requests keep the
283
+ client's `Accept-Encoding`.
284
+
285
+ When the policy drops an SSE list reply, the client is not left waiting. Each
286
+ list request whose reply was dropped gets a JSON-RPC error event instead, with
287
+ none of the dropped content:
288
+
289
+ ```json
290
+ {
291
+ "jsonrpc": "2.0",
292
+ "id": 1,
293
+ "error": {
294
+ "code": -32603,
295
+ "message": "The MCP capability filter could not read the upstream's list response, so it was not forwarded."
296
+ }
297
+ }
298
+ ```
299
+
300
+ The error is sent at once when the dropped reply's `id` shows which request it
301
+ answered, and otherwise when the upstream stream ends, for each request still
302
+ unanswered. A request that is not a list request gets a different message (see
303
+ [Batch Requests](#batch-requests)). Once no request is waiting, the stream ends.
304
+ Other events in the stream (progress notifications, server requests, error
305
+ replies, events with only an `id` of letters, digits and `._~:/+=-`, `retry` or
306
+ `event: message`) pass through unchanged. So does an event without a payload (no
307
+ `data`, or an empty one), such as the priming event a 2025-11-25 server sends
308
+ first to make its stream resumable: its `id` is kept, and nothing is logged.
309
+
310
+ The policy adds an error only after it dropped something. When the upstream
311
+ stream ends cleanly without answering a request, and nothing was dropped, the
312
+ policy adds nothing: answering that request is the handler's job.
313
+ `McpProxyHandler` sends a `-32603` error for each unanswered request itself
314
+ (before this policy sees the stream), so behind it the client still gets an
315
+ answer.
316
+
187
317
  ## Batch Requests
188
318
 
189
- For JSON-RPC batch requests, list responses are filtered per response item when
190
- the item id can be matched to the original list request. If any batch item
191
- directly calls a hidden tool, prompt, or resource, the policy blocks the whole
192
- batch with a not-found JSON-RPC error response.
319
+ For JSON-RPC batch requests, list responses are filtered per response item,
320
+ matched to the original list request by id. A reply id matches a request of the
321
+ same type first (`1` to `1`), and otherwise a request with the same text (`"1"`
322
+ for `1`). A batch may hold ids that differ only in type (`1` and `"1"`): a reply
323
+ goes to the request whose id it matches exactly. A result whose id matches no
324
+ request in the batch fails closed, including one whose id is that of a client
325
+ result or error in the batch: that reply is dropped. Ids can be re-typed in more
326
+ ways than the policy can tell apart (`1.0`, `"01"`, `-0`, numbers past 2^53), so
327
+ a result that holds a list the policy filters (`tools`, `prompts`, `resources`,
328
+ `resourceTemplates`) fails closed the same way unless it answers the request
329
+ that listed it.
330
+
331
+ A batch reply array, as a JSON body or in an SSE event, is checked member by
332
+ member, so one bad member does not lose its siblings' replies (a tool call in
333
+ the batch may already have run). Each member that is safe is sent. A member the
334
+ policy cannot read is replaced by a JSON-RPC `-32603` error for the request it
335
+ answers, or dropped when its `id` matches no request still waiting. When a
336
+ member was dropped, every request left without a reply gets the error too: in a
337
+ JSON reply at once, appended to the array; on a stream when it ends. A JSON
338
+ batch reply that is not an array gets a `-32603` for every request.
339
+
340
+ When the stream drops an event that might have held a reply, every request still
341
+ waiting at the end of the stream gets a JSON-RPC `-32603` error. For a request
342
+ that is not a list request, the message says the filter did not forward the
343
+ reply and the request may already have run, so the client does not assume a
344
+ retry is safe:
345
+
346
+ ```json
347
+ {
348
+ "jsonrpc": "2.0",
349
+ "id": 2,
350
+ "error": {
351
+ "code": -32603,
352
+ "message": "The MCP capability filter did not forward the upstream's reply to this request. The request may already have run."
353
+ }
354
+ }
355
+ ```
356
+
357
+ If any batch item directly calls a hidden tool, prompt, or resource, the policy
358
+ refuses the whole batch and forwards none of it. It answers with an array that
359
+ has one error per request in the batch: the not-found error for the hidden call,
360
+ and for each other request a `-32603` that says it was not forwarded because
361
+ another request in its batch was refused. Notifications in the batch get
362
+ nothing. On protocol version 2025-06-18 and later, which have no batches, the
363
+ answer is one `400` with `-32600` and no `id`.
@@ -26,7 +26,7 @@
26
26
  "description": "The name of the exported type"
27
27
  },
28
28
  "module": {
29
- "const": "$import(@zuplo/runtime)",
29
+ "const": "$import(@zuplo/runtime/mcp-gateway)",
30
30
  "description": "The module containing the policy"
31
31
  },
32
32
  "options": {
@@ -330,7 +330,7 @@
330
330
  "examples": [
331
331
  {
332
332
  "export": "McpCapabilityFilterInboundPolicy",
333
- "module": "$import(@zuplo/runtime)",
333
+ "module": "$import(@zuplo/runtime/mcp-gateway)",
334
334
  "options": {
335
335
  "accessControl": {
336
336
  "groupClaim": "groups",
@@ -20,13 +20,14 @@ The policy does not perform the normal upstream fetch and does not pass hidden
20
20
  context to the handler. The route should use `McpProxyHandler` with a
21
21
  deterministic upstream MCP URL configured on the handler. `McpProxyHandler`
22
22
  handles GET stream probes and delegates POST forwarding to Zuplo's
23
- `urlRewriteHandler`. The policy only installs a response hook for MCP OAuth
24
- retry/connect-required cases.
23
+ `urlRewriteHandler`. When the upstream answers `401`, `McpProxyHandler`
24
+ refreshes the upstream credential and retries the request once, before any
25
+ outbound policy or response hook sees the response. If the refresh needs the
26
+ user to reconnect, the handler returns the connect-required JSON-RPC error
27
+ instead.
25
28
 
26
29
  Projects using this policy must run with a compatibility date that enables
27
- chained response hooks, currently `2026-03-01` or later. The retry hook must
28
- receive the latest response in the policy chain so later response hooks cannot
29
- accidentally replace an upstream OAuth retry or connect-required response.
30
+ chained response hooks, currently `2026-03-01` or later.
30
31
 
31
32
  ## Configuration
32
33
 
@@ -26,7 +26,7 @@
26
26
  "description": "The name of the exported type"
27
27
  },
28
28
  "module": {
29
- "const": "$import(@zuplo/runtime)",
29
+ "const": "$import(@zuplo/runtime/mcp-gateway)",
30
30
  "description": "The module containing the policy"
31
31
  },
32
32
  "options": {
@@ -424,7 +424,7 @@
424
424
  "examples": [
425
425
  {
426
426
  "export": "McpTokenExchangeInboundPolicy",
427
- "module": "$import(@zuplo/runtime)",
427
+ "module": "$import(@zuplo/runtime/mcp-gateway)",
428
428
  "options": {
429
429
  "displayName": "Linear",
430
430
  "id": "linear",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "7.13.1",
3
+ "version": "7.13.3",
4
4
  "type": "module",
5
5
  "description": "The official Zuplo CLI for local development and platform management",
6
6
  "homepage": "https://zuplo.com/docs/cli/overview",
@@ -32,9 +32,9 @@
32
32
  "zuplo": "zuplo.js"
33
33
  },
34
34
  "dependencies": {
35
- "@zuplo/cli": "7.13.1",
36
- "@zuplo/core": "7.13.1",
37
- "@zuplo/runtime": "7.13.1",
38
- "@zuplo/test": "7.13.1"
35
+ "@zuplo/cli": "7.13.3",
36
+ "@zuplo/core": "7.13.3",
37
+ "@zuplo/runtime": "7.13.3",
38
+ "@zuplo/test": "7.13.3"
39
39
  }
40
40
  }