zuplo 6.71.9 → 6.71.10

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.
@@ -140,6 +140,7 @@ const config = {
140
140
  graphqlPlugin({
141
141
  schema: "./schema.graphql", // Also accepts a URL, e.g. https://...
142
142
  path: graphqlPath,
143
+ // endpoint: "/my-graphql", // Optional, defaults to "/graphql"
143
144
  }),
144
145
  ],
145
146
  };
@@ -0,0 +1,409 @@
1
+ ---
2
+ title: Proxying Between Zuplo Gateways
3
+ sidebar_label: Proxying Between Gateways
4
+ description:
5
+ Learn how to proxy requests from one Zuplo project to another using the URL
6
+ Forward Handler or a custom fetch handler, propagate authentication, surface
7
+ upstream errors, and troubleshoot 522 timeouts.
8
+ tags:
9
+ - backends
10
+ - deployment
11
+ - custom-code
12
+ - authentication
13
+ ---
14
+
15
+ Some architectures call for one Zuplo gateway to sit in front of one or more
16
+ other Zuplo gateways. A "product" gateway might aggregate several team-owned
17
+ "member" gateways, a BFF might fan out to multiple internal APIs, or a migration
18
+ might route a subset of traffic through a new project while the old one still
19
+ serves the rest.
20
+
21
+ This guide covers the patterns, auth propagation strategies, error-handling
22
+ pitfalls, and troubleshooting steps you need when the upstream is another Zuplo
23
+ project.
24
+
25
+ ## When this pattern makes sense
26
+
27
+ - **Product-of-products** — A single public API endpoint forwards different path
28
+ prefixes to separate Zuplo projects, each owned by a different team.
29
+ - **Backend for frontend (BFF)** — A gateway aggregates data from multiple
30
+ downstream Zuplo-managed APIs into a single response.
31
+ - **Tenant routing** — Requests are routed to different Zuplo projects based on
32
+ tenant identity or API key metadata. See
33
+ [User-Based Backend Routing](./user-based-backend-routing.mdx) for a detailed
34
+ walkthrough of this approach.
35
+ - **Gradual migration** — During a migration, a new gateway forwards unhandled
36
+ routes to the old gateway.
37
+
38
+ If you use a Managed Dedicated deployment with an enterprise plan, consider
39
+ [Federated Gateways](../dedicated/federated-gateways.mdx) instead. Federated
40
+ Gateways is an enterprise add-on that uses the `local://` protocol for
41
+ inter-environment communication within the same dedicated instance, avoiding the
42
+ public internet and providing lower latency.
43
+
44
+ ## Choosing an approach
45
+
46
+ ### Pattern A: URL Forward Handler
47
+
48
+ The [URL Forward Handler](../handlers/url-forward.mdx) is the simplest option.
49
+ It proxies the request — method, headers, and body — to the downstream Zuplo
50
+ project without writing any code.
51
+
52
+ ```json
53
+ {
54
+ "handler": {
55
+ "export": "urlForwardHandler",
56
+ "module": "$import(@zuplo/runtime)",
57
+ "options": {
58
+ "baseUrl": "${env.DOWNSTREAM_GATEWAY_URL}"
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ Store the downstream URL (for example
65
+ `https://member-api-main-abc123.zuplo.app`) in an
66
+ [environment variable](../articles/environment-variables.mdx) so you can change
67
+ it per environment without modifying route configuration.
68
+
69
+ The URL Forward Handler appends the incoming path to the `baseUrl`. If the outer
70
+ gateway receives `GET /orders/123` and the `baseUrl` is
71
+ `https://member-api-main-abc123.zuplo.app`, the forwarded request goes to
72
+ `https://member-api-main-abc123.zuplo.app/orders/123`.
73
+
74
+ **When to use this pattern:**
75
+
76
+ - You want zero-code proxying and are happy forwarding the request as-is.
77
+ - You do not need to inspect or transform the upstream response before returning
78
+ it to the caller.
79
+
80
+ ### Pattern B: Custom fetch handler
81
+
82
+ A [Function Handler](../handlers/custom-handler.mdx) gives you full control over
83
+ the outbound request and lets you inspect the upstream response before returning
84
+ it to the caller. This is the recommended pattern when you need to propagate
85
+ authentication credentials, transform the response, or surface upstream error
86
+ details.
87
+
88
+ ```typescript
89
+ import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime";
90
+
91
+ export default async function (
92
+ request: ZuploRequest,
93
+ context: ZuploContext,
94
+ ): Promise<Response> {
95
+ const url = new URL(request.url);
96
+ const upstreamUrl = `${environment.DOWNSTREAM_GATEWAY_URL}${url.pathname}${url.search}`;
97
+
98
+ const upstreamResponse = await fetch(upstreamUrl, {
99
+ method: request.method,
100
+ headers: request.headers,
101
+ body: request.body,
102
+ });
103
+
104
+ // Return the upstream response directly, preserving status and headers
105
+ return new Response(upstreamResponse.body, {
106
+ status: upstreamResponse.status,
107
+ headers: upstreamResponse.headers,
108
+ });
109
+ }
110
+ ```
111
+
112
+ **When to use this pattern:**
113
+
114
+ - You need to add, remove, or transform headers before forwarding.
115
+ - You need to read the upstream response body (for example, to merge responses
116
+ from multiple downstreams).
117
+ - You want to return the exact upstream status code and body to the caller
118
+ instead of receiving an opaque 522. See
119
+ [Surfacing upstream errors](#surfacing-upstream-errors-instead-of-522) below.
120
+
121
+ ### Pattern C: Federated Gateways (Managed Dedicated)
122
+
123
+ On a [Managed Dedicated](../dedicated/federated-gateways.mdx) plan, use the
124
+ `local://` protocol to call other environments in the same instance:
125
+
126
+ ```json
127
+ {
128
+ "handler": {
129
+ "export": "urlForwardHandler",
130
+ "module": "$import(@zuplo/runtime)",
131
+ "options": {
132
+ "baseUrl": "local://member-api-main-abc123"
133
+ }
134
+ }
135
+ }
136
+ ```
137
+
138
+ This avoids the public internet entirely. The Lambda handler is not supported
139
+ for federated calls — use URL Forward, URL Rewrite, or a Function Handler
140
+ instead.
141
+
142
+ ## Propagating authentication
143
+
144
+ When the outer gateway authenticates a request (using
145
+ [API Key Authentication](../concepts/api-keys.md),
146
+ [JWT authentication](../concepts/authentication.mdx), or another method), the
147
+ inner gateway still needs to trust that request. There are several patterns for
148
+ propagating identity between gateways.
149
+
150
+ ### Forward the original credential
151
+
152
+ The simplest approach is to forward the caller's original `Authorization` header
153
+ (or API key header) to the downstream gateway. Both the URL Forward Handler and
154
+ the custom fetch handler forward request headers by default, so if the
155
+ downstream gateway accepts the same credentials, this works without extra
156
+ configuration.
157
+
158
+ :::caution
159
+
160
+ If the outer and inner gateways use different API key buckets or different JWT
161
+ issuers, forwarding the original credential does not work. Use one of the
162
+ patterns below instead.
163
+
164
+ :::
165
+
166
+ ### Shared secret header
167
+
168
+ Store a shared secret in an
169
+ [environment variable](../articles/environment-variables.mdx) on both projects.
170
+ On the outer gateway, use a
171
+ [Set Headers policy](../policies/set-headers-inbound.mdx) to add the secret as a
172
+ custom header. On the inner gateway, validate the header in an inbound policy or
173
+ use the same Set Headers policy to check the value.
174
+
175
+ ```json
176
+ {
177
+ "name": "set-backend-secret",
178
+ "policyType": "set-headers-inbound",
179
+ "handler": {
180
+ "export": "SetHeadersInboundPolicy",
181
+ "module": "$import(@zuplo/runtime)",
182
+ "options": {
183
+ "headers": [
184
+ {
185
+ "name": "x-gateway-secret",
186
+ "value": "$env(DOWNSTREAM_SECRET)"
187
+ }
188
+ ]
189
+ }
190
+ }
191
+ }
192
+ ```
193
+
194
+ See [Securing your backend](../articles/securing-your-backend.mdx) for a
195
+ complete walkthrough of this approach.
196
+
197
+ ### Upstream Zuplo JWT
198
+
199
+ The [Upstream Zuplo JWT policy](../policies/upstream-zuplo-jwt-auth-inbound.mdx)
200
+ generates a short-lived, self-signed JWT and attaches it to the outbound
201
+ request. Configure the inner gateway to validate this JWT using the
202
+ [OpenID JWT Authentication policy](../policies/open-id-jwt-auth-inbound.mdx)
203
+ with Zuplo's JWKS endpoint.
204
+
205
+ This is the most robust option for service-to-service authentication between
206
+ Zuplo projects because it does not require sharing static secrets and the token
207
+ includes claims you can use for authorization on the downstream side.
208
+
209
+ ## Surfacing upstream errors instead of 522
210
+
211
+ A common problem when proxying between Zuplo gateways: the downstream gateway
212
+ returns a `401 Unauthorized` (or another error), but the caller sees a `522`
213
+ instead.
214
+
215
+ ### Why this happens
216
+
217
+ Zuplo's managed edge environment uses connection-level timeouts between the
218
+ gateway and the origin server. A `522` status code means a connection-level
219
+ failure occurred between the gateway and the upstream. The
220
+ [Platform Limits](../articles/limits.mdx) documentation lists two scenarios that
221
+ produce a 522: a Complete TCP Connection timeout at 19 seconds and a TCP ACK
222
+ Timeout at 90 seconds.
223
+
224
+ A `522` can also appear when the upstream closes the connection unexpectedly —
225
+ for example, if the downstream gateway rejects the TLS handshake, returns a
226
+ connection reset, or takes too long to send the response headers.
227
+
228
+ When the downstream Zuplo project returns an HTTP error like `401` or `500`,
229
+ that is **not** a 522. The 522 means the connection itself failed before an HTTP
230
+ response was received. If you are seeing 522 instead of the expected upstream
231
+ error, the issue is at the network or TLS layer, not the HTTP layer.
232
+
233
+ ### Common causes of 522 between Zuplo projects
234
+
235
+ - **DNS resolution failure** — The downstream URL is incorrect or the
236
+ environment no longer exists.
237
+ - **TLS handshake failure** — Misconfigured custom domain or certificate issue
238
+ on the downstream project.
239
+ - **Connection timeout** — The downstream project takes longer than 19 seconds
240
+ to accept the TCP connection, usually because it is overloaded or
241
+ misconfigured.
242
+ - **Egress restrictions** — In some network configurations, outbound connections
243
+ from one Zuplo project to another may be restricted.
244
+
245
+ ### Returning the actual upstream error
246
+
247
+ If the TCP connection succeeds but the upstream returns an HTTP error (like 401
248
+ or 500), the URL Forward Handler already returns that status code to the caller.
249
+ You do not need to do anything extra — the upstream's status and body flow
250
+ through.
251
+
252
+ If you need more control (for example, to log the upstream error or transform it
253
+ before returning), use a custom fetch handler:
254
+
255
+ ```typescript
256
+ import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime";
257
+
258
+ export default async function (
259
+ request: ZuploRequest,
260
+ context: ZuploContext,
261
+ ): Promise<Response> {
262
+ const url = new URL(request.url);
263
+ const upstreamUrl = `${environment.DOWNSTREAM_GATEWAY_URL}${url.pathname}${url.search}`;
264
+
265
+ try {
266
+ const upstreamResponse = await fetch(upstreamUrl, {
267
+ method: request.method,
268
+ headers: request.headers,
269
+ body: request.body,
270
+ });
271
+
272
+ if (!upstreamResponse.ok) {
273
+ context.log.warn(
274
+ `Upstream returned ${upstreamResponse.status} for ${url.pathname}`,
275
+ );
276
+ }
277
+
278
+ return new Response(upstreamResponse.body, {
279
+ status: upstreamResponse.status,
280
+ headers: upstreamResponse.headers,
281
+ });
282
+ } catch (error) {
283
+ context.log.error(`Failed to reach upstream: ${error}`);
284
+ return new Response(
285
+ JSON.stringify({
286
+ type: "https://httpproblems.com/http-status/502",
287
+ title: "Bad Gateway",
288
+ status: 502,
289
+ detail: "The upstream service is unreachable.",
290
+ }),
291
+ {
292
+ status: 502,
293
+ headers: { "content-type": "application/problem+json" },
294
+ },
295
+ );
296
+ }
297
+ }
298
+ ```
299
+
300
+ This handler catches connection-level errors (which would otherwise surface as
301
+ a 522) and returns a structured `502 Bad Gateway` response. When the upstream
302
+ does return an HTTP response, the status and body pass through unchanged.
303
+
304
+ ## Custom domains across the fleet
305
+
306
+ When multiple Zuplo projects form a gateway chain, decide where to attach your
307
+ [custom domain](../articles/custom-domains.mdx):
308
+
309
+ - **Outer gateway only** — The most common setup. Attach your custom domain (for
310
+ example, `api.example.com`) to the outer gateway project and let the inner
311
+ gateways use their default `*.zuplo.app` URLs. Callers only see your custom
312
+ domain.
313
+ - **Every gateway** — Useful when internal teams also call the inner gateways
314
+ directly for testing or monitoring. Each project gets its own custom domain.
315
+
316
+ :::tip
317
+
318
+ Putting the custom domain only on the outer gateway simplifies DNS management
319
+ and certificate renewal. The inner gateways are implementation details that
320
+ callers do not need to know about.
321
+
322
+ :::
323
+
324
+ ## Cost considerations
325
+
326
+ Request traffic is metered at the account level, and every project in the chain
327
+ counts against the same shared allowance. A single client request that fans out
328
+ to three downstream Zuplo projects results in four billed requests: one on the
329
+ outer gateway and one on each downstream project.
330
+
331
+ Review [Platform Limits](../articles/limits.mdx) and your plan's monthly request
332
+ allowance before designing a fan-out architecture. If the request volume is
333
+ high, consider whether a single Zuplo project with path-based routing can
334
+ replace the multi-project topology.
335
+
336
+ ## Troubleshooting
337
+
338
+ ### 522 with no logs on the downstream project
339
+
340
+ The outer gateway's runtime could not establish a TCP connection to the
341
+ downstream project. The request never reached the inner gateway, so there are no
342
+ logs there.
343
+
344
+ **Checklist:**
345
+
346
+ 1. Verify the downstream URL is correct. Check the environment variable value in
347
+ the outer gateway project. A typo in the environment name (for example,
348
+ `main` instead of `main-abc123`) produces a DNS failure.
349
+ 2. Confirm the downstream project is deployed and its environment is active.
350
+ Open the downstream project in the [Zuplo Portal](https://portal.zuplo.com)
351
+ and check the environment status.
352
+ 3. If using a custom domain on the downstream project, verify the DNS CNAME
353
+ record points to `cname.zuplo.app` and the certificate is valid.
354
+
355
+ ### 522 only when forwarding to another Zuplo project
356
+
357
+ Requests to `httpbin.org` or other external services work fine, but requests to
358
+ `*.zuplo.app` return 522.
359
+
360
+ **Checklist:**
361
+
362
+ 1. Check that the downstream Zuplo project's environment is not a development
363
+ environment (ending in `.zuplo.dev`). Development environments have stricter
364
+ rate limits (1,000 requests per minute) and may reject connections under
365
+ load.
366
+ 2. Verify TLS is working — the outer gateway connects to the downstream over
367
+ HTTPS. If the downstream has a custom domain with certificate issues, the TLS
368
+ handshake fails and produces a 522.
369
+ 3. Look at the outer gateway's logs for connection error details. The Zuplo
370
+ runtime logs include the error message when an outbound `fetch` fails.
371
+
372
+ ### Caller receives the upstream 401 directly
373
+
374
+ This is the expected behavior. The URL Forward Handler and custom fetch handlers
375
+ both return the upstream's HTTP status and body as-is. If the caller sees `401`,
376
+ the downstream project rejected the request at the HTTP level (not a connection
377
+ failure).
378
+
379
+ If the downstream uses API key authentication and the caller's key is not valid
380
+ on the downstream project, the downstream returns `401`. Review the
381
+ [Propagating authentication](#propagating-authentication) section to choose the
382
+ right credential strategy.
383
+
384
+ ### Mismatched response content types
385
+
386
+ The downstream project returns JSON but the caller receives an unexpected
387
+ content type or an empty body.
388
+
389
+ **Checklist:**
390
+
391
+ 1. Verify the downstream route is configured to return the expected content
392
+ type. Check the handler and outbound policies on the downstream project.
393
+ 2. If using a custom fetch handler on the outer gateway, make sure you are
394
+ forwarding the upstream's `content-type` header. The example handler in
395
+ [Pattern B](#pattern-b-custom-fetch-handler) preserves all upstream headers.
396
+ 3. Check whether an outbound policy on the outer gateway transforms or strips
397
+ the response body.
398
+
399
+ ## Related resources
400
+
401
+ - [URL Forward Handler](../handlers/url-forward.mdx)
402
+ - [Function Handler](../handlers/custom-handler.mdx)
403
+ - [Federated Gateways (Managed Dedicated)](../dedicated/federated-gateways.mdx)
404
+ - [Securing your backend](../articles/securing-your-backend.mdx)
405
+ - [Platform Limits](../articles/limits.mdx)
406
+ - [Gateway Timeout error](../errors/gateway-timeout.mdx)
407
+ - [Request Lifecycle](../concepts/request-lifecycle.mdx)
408
+ - [Custom Domains](../articles/custom-domains.mdx)
409
+ - [Environment Variables](../articles/environment-variables.mdx)
@@ -121,6 +121,17 @@ client, ensure your `outputSchema` is a valid `type: object` JSON Schema and
121
121
  included as `structuredContent`. When `false`, only `text` content will be
122
122
  returned.
123
123
 
124
+ :::caution
125
+
126
+ Enable `includeOutputSchema` and `includeStructuredContent` together. Turning on
127
+ `includeOutputSchema` alone makes the server advertise an output schema while
128
+ returning only `text` content, which causes MCP clients to fail tool calls with
129
+ `Tool call failed: 500`. See
130
+ [Troubleshooting the MCP Server Handler](../mcp-server/troubleshooting.mdx) for
131
+ how to diagnose and fix this.
132
+
133
+ :::
134
+
124
135
  ### Operations
125
136
 
126
137
  Configure MCP tools, prompts, and resources using the `operations`
@@ -0,0 +1,195 @@
1
+ ---
2
+ title: "Troubleshooting the MCP Server Handler"
3
+ sidebar_label: "Troubleshooting"
4
+ description:
5
+ Diagnose MCP Server handler tool calls that fail in an AI client even though
6
+ the gateway route returns 200, using debug logging and the structured-content
7
+ configuration.
8
+ ---
9
+
10
+ When an AI client calls a tool exposed by the
11
+ [MCP Server handler](../handlers/mcp-server.mdx), a failure often surfaces as
12
+ nothing more than `Tool call failed: 500`, with no indication of _why_. The
13
+ underlying gateway route can return `200` while the MCP client still rejects the
14
+ response, which makes these failures hard to diagnose from the client alone.
15
+
16
+ This page shows how to turn on the handler's debug logging, read the log lines
17
+ that reveal a misconfiguration, and fix the most common cause: a tool that
18
+ advertises an output schema but returns no structured content.
19
+
20
+ :::tip
21
+
22
+ Most "200 on the gateway, error in the client" failures come from the
23
+ [`includeOutputSchema` and `includeStructuredContent` options](../handlers/mcp-server.mdx#mcp-2025-06-18-global-options).
24
+ If you enabled one without the other, jump straight to
25
+ [Tool call failed: 500](#tool-call-failed-500--has-an-output-schema-but-did-not-return-structured-content).
26
+
27
+ :::
28
+
29
+ ## Enable debug logging
30
+
31
+ The MCP Server handler can emit verbose logs covering server startup, tool
32
+ registration, and each tool call. These logs are the fastest way to confirm what
33
+ the server actually advertised to the client.
34
+
35
+ <Stepper>
36
+
37
+ 1. **Turn on `debugMode`.** Set `debugMode: true` in the handler options for
38
+ your MCP route in `routes.oas.json`:
39
+
40
+ ```json
41
+ "post": {
42
+ "x-zuplo-route": {
43
+ "handler": {
44
+ "export": "mcpServerHandler",
45
+ "module": "$import(@zuplo/runtime)",
46
+ "options": {
47
+ "name": "example-mcp-server",
48
+ "version": "1.0.0",
49
+ "debugMode": true
50
+ }
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ 2. **Redeploy so the server cold-starts.** Some of the most useful log lines —
57
+ server startup and tool registration — are written only when the MCP server
58
+ initializes (a cold start). An already-running worker won't re-emit them.
59
+ Deploy the working copy or environment again so a fresh worker boots with
60
+ `debugMode` enabled.
61
+
62
+ 3. **View the logs.** Open your project in the Portal and go to **Observability
63
+ → Logs**. Trigger a tool call from your MCP client, then read the entries for
64
+ the MCP route. Filter on the request's `zuplo-request-id` to isolate a single
65
+ call.
66
+
67
+ </Stepper>
68
+
69
+ :::caution
70
+
71
+ Leave `debugMode` off in production. It logs verbose per-call detail that adds
72
+ noise and overhead. Turn it on to diagnose an issue, then turn it back off and
73
+ redeploy.
74
+
75
+ :::
76
+
77
+ ### Key log lines to read
78
+
79
+ With `debugMode: true`, the handler writes lines at each stage of its lifecycle.
80
+ The exact format may change between runtime versions, but the fields below are
81
+ what you're looking for:
82
+
83
+ | Log line | When it's written | What to check |
84
+ | ------------------------------ | ----------------------- | ---------------------------------------------------------------------- |
85
+ | `MCP Server cold start` | A fresh worker boots | Confirms `debugMode` is active and a new worker started |
86
+ | `MCP tool registered` | Each tool is registered | The `includeStructuredContent` and `hasOutputSchema` fields for a tool |
87
+ | `MCP Server response complete` | A tool call finishes | The response was produced and sent back to the client |
88
+
89
+ The `MCP tool registered` line is the important one. For a tool whose calls fail
90
+ in the client, you'll typically see a line resembling:
91
+
92
+ ```
93
+ MCP tool registered { name: "get_current_weather", hasOutputSchema: true, includeStructuredContent: false }
94
+ ```
95
+
96
+ `hasOutputSchema: true` with `includeStructuredContent: false` is the exact
97
+ misconfiguration described in the next section.
98
+
99
+ ## Tool call failed: 500 / "has an output schema but did not return structured content"
100
+
101
+ **Symptom.** An AI client (such as Claude) reports `Tool call failed: 500` when
102
+ it invokes a tool. With more verbose client logging, the underlying error reads:
103
+
104
+ ```
105
+ Tool <tool_name> has an output schema but did not return structured content
106
+ ```
107
+
108
+ Meanwhile, the gateway's own logs show the route that backs the tool returned
109
+ `200`.
110
+
111
+ **Likely cause.** The handler is configured with `includeOutputSchema: true` but
112
+ not `includeStructuredContent: true`. Under the
113
+ [MCP `2025-06-18`](https://modelcontextprotocol.io/specification/2025-06-18)
114
+ structured-content behavior, when a tool advertises an `outputSchema`, the
115
+ client expects every successful result to include a matching `structuredContent`
116
+ object. With `includeOutputSchema` on and `includeStructuredContent` off, the
117
+ server advertises the schema but returns only `text` content, so a
118
+ spec-compliant client rejects an otherwise-successful `200` response.
119
+
120
+ The `MCP tool registered` debug line confirms it:
121
+ `hasOutputSchema: true, includeStructuredContent: false`.
122
+
123
+ **Fix.** Enable both options together on the MCP Server handler:
124
+
125
+ ```json
126
+ "options": {
127
+ "name": "example-mcp-server",
128
+ "version": "1.0.0",
129
+ "includeOutputSchema": true,
130
+ "includeStructuredContent": true
131
+ }
132
+ ```
133
+
134
+ Then redeploy. The next `MCP tool registered` line should read
135
+ `hasOutputSchema: true, includeStructuredContent: true`, and the tool call
136
+ succeeds.
137
+
138
+ :::note
139
+
140
+ If you don't need output schemas at all, the alternative fix is to turn both off
141
+ (the defaults). The two options are designed to move together: enable
142
+ `includeOutputSchema` _only_ alongside `includeStructuredContent`.
143
+
144
+ :::
145
+
146
+ For the full definitions of both options, see the
147
+ [MCP `2025-06-18` Global Options](../handlers/mcp-server.mdx#mcp-2025-06-18-global-options)
148
+ in the handler reference.
149
+
150
+ ## Client-side debugging tools
151
+
152
+ When the gateway looks healthy, reproduce the failure against the client to see
153
+ the protocol-level error the AI client hides:
154
+
155
+ - **MCP Inspector** — the
156
+ [official inspector](https://github.com/modelcontextprotocol/inspector)
157
+ (`npx @modelcontextprotocol/inspector`) connects to your remote server, lists
158
+ tools, and calls them while showing the raw JSON-RPC messages. See
159
+ [Testing](./testing.mdx) for connection steps.
160
+ - **mcpjam** — the [mcpjam inspector](https://github.com/mcpjam/inspector) is an
161
+ open-source MCP testing client that's useful for exercising tool calls and
162
+ inspecting responses outside of an AI client.
163
+ - **Claude Code** — run with the `--debug` flag (for example, `claude --debug`)
164
+ to print MCP protocol traffic, including the full tool-call error that the
165
+ chat UI summarizes as `Tool call failed: 500`.
166
+
167
+ ## Checklist: the gateway returns 200 but the client errors
168
+
169
+ When a tool call fails in the client but the gateway reports success, verify
170
+ each of these in order:
171
+
172
+ 1. **`debugMode` is on and a fresh worker booted.** Confirm a
173
+ `MCP Server cold start` line appears after your latest deploy. Without a cold
174
+ start, you're reading stale logs.
175
+ 2. **Schema and structured content move together.** In the `MCP tool registered`
176
+ line, `hasOutputSchema` and `includeStructuredContent` should either both be
177
+ `true` or both be `false` — never `hasOutputSchema: true` with
178
+ `includeStructuredContent: false`.
179
+ 3. **The output schema is a valid `type: object` JSON Schema.** Some clients
180
+ reject schemas that aren't `type: object`. The same applies to the
181
+ `structuredContent` the server returns. See the
182
+ [compatibility caveat](../handlers/mcp-server.mdx#mcp-2025-06-18-global-options).
183
+ 4. **The route really returns JSON.** `includeStructuredContent` parses the
184
+ response body as JSON to build `structuredContent`. A non-JSON `200` body
185
+ can't be parsed into the advertised schema.
186
+ 5. **Reproduce in a raw client.** Call the tool through the
187
+ [MCP Inspector](./testing.mdx) or `curl` to read the JSON-RPC error directly,
188
+ rather than the client's summarized `500`.
189
+
190
+ ## Next steps
191
+
192
+ - [MCP Server handler reference](../handlers/mcp-server.mdx) — every handler
193
+ configuration option.
194
+ - [Testing your MCP server](./testing.mdx) — MCP Inspector and `curl` recipes.
195
+ - [MCP Server tools](./tools.mdx) — editing tool definitions and `outputSchema`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "6.71.9",
3
+ "version": "6.71.10",
4
4
  "type": "module",
5
5
  "description": "The programmable API Gateway",
6
6
  "author": "Zuplo, Inc.",
@@ -19,9 +19,9 @@
19
19
  "zuplo": "zuplo.js"
20
20
  },
21
21
  "dependencies": {
22
- "@zuplo/cli": "6.71.9",
23
- "@zuplo/core": "6.71.9",
24
- "@zuplo/runtime": "6.71.9",
22
+ "@zuplo/cli": "6.71.10",
23
+ "@zuplo/core": "6.71.10",
24
+ "@zuplo/runtime": "6.71.10",
25
25
  "@zuplo/test": "1.4.0"
26
26
  }
27
27
  }