zuplo 7.4.4 → 7.4.6

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.
Files changed (40) hide show
  1. package/docs/articles/ci-cd-azure/local-testing.mdx +22 -4
  2. package/docs/articles/ci-cd-bitbucket/local-testing.mdx +22 -4
  3. package/docs/articles/ci-cd-circleci/local-testing.mdx +21 -4
  4. package/docs/articles/ci-cd-github/deploy-and-test.mdx +28 -13
  5. package/docs/articles/ci-cd-github/local-testing.mdx +36 -16
  6. package/docs/articles/ci-cd-gitlab/local-testing.mdx +22 -4
  7. package/docs/articles/github-deployment-testing.mdx +118 -31
  8. package/docs/articles/testing-getting-started.mdx +220 -0
  9. package/docs/articles/testing-preview-environments.mdx +148 -0
  10. package/docs/articles/testing-recipes.mdx +429 -0
  11. package/docs/articles/testing.mdx +138 -408
  12. package/docs/mcp-gateway/auth/configuring-auth0.mdx +6 -5
  13. package/docs/mcp-gateway/auth/configuring-clerk.mdx +4 -4
  14. package/docs/mcp-gateway/auth/configuring-cognito.mdx +5 -4
  15. package/docs/mcp-gateway/auth/configuring-entra.mdx +5 -4
  16. package/docs/mcp-gateway/auth/configuring-generic-oidc.mdx +8 -8
  17. package/docs/mcp-gateway/auth/configuring-google.mdx +4 -4
  18. package/docs/mcp-gateway/auth/configuring-keycloak.mdx +4 -3
  19. package/docs/mcp-gateway/auth/configuring-logto.mdx +4 -4
  20. package/docs/mcp-gateway/auth/configuring-okta.mdx +3 -3
  21. package/docs/mcp-gateway/auth/configuring-onelogin.mdx +3 -3
  22. package/docs/mcp-gateway/auth/configuring-ping.mdx +3 -3
  23. package/docs/mcp-gateway/auth/configuring-workos.mdx +5 -4
  24. package/docs/mcp-gateway/auth/manual-oauth-testing.mdx +8 -8
  25. package/docs/mcp-gateway/auth/overview.mdx +17 -17
  26. package/docs/mcp-gateway/auth/upstream-oauth.mdx +5 -5
  27. package/docs/mcp-gateway/code-config/local-development.mdx +14 -12
  28. package/docs/mcp-gateway/code-config/overview.mdx +9 -4
  29. package/docs/mcp-gateway/connect-clients/chatgpt.mdx +114 -56
  30. package/docs/mcp-gateway/how-it-works.mdx +11 -9
  31. package/docs/mcp-gateway/introduction.mdx +3 -1
  32. package/docs/mcp-gateway/quickstart-local.mdx +7 -7
  33. package/docs/mcp-gateway/reference.mdx +58 -25
  34. package/docs/mcp-gateway/server-registry.mdx +179 -0
  35. package/docs/mcp-gateway/test-clients.mdx +2 -2
  36. package/docs/mcp-server/custom-tools.mdx +32 -0
  37. package/docs/programmable-api/mcp-gateway-plugin.mdx +137 -0
  38. package/docs/programmable-api/mcp-sdk.mdx +240 -0
  39. package/docs/self-hosted/overview.md +2 -0
  40. package/package.json +5 -5
@@ -0,0 +1,179 @@
1
+ ---
2
+ title: "MCP Server Registry"
3
+ sidebar_label: "Server Registry"
4
+ description:
5
+ The read-only MCP Server Registry API (v0.1) the gateway serves by default —
6
+ advertises every MCP route so clients and subregistries can discover them
7
+ without authentication.
8
+ ---
9
+
10
+ The MCP Gateway serves a read-only
11
+ [MCP Server Registry API](https://github.com/modelcontextprotocol/registry)
12
+ (v0.1 freeze) that advertises every MCP route on the gateway. MCP clients and
13
+ mirroring subregistries can discover the gateway's registered servers without
14
+ authentication, per the registry specification.
15
+
16
+ The registry is enabled by default and mounted at `/__zuplo/mcp-registry` (with
17
+ the default `basePath`). Configure or disable it through the
18
+ [`registry` option](../programmable-api/mcp-gateway-plugin.mdx#registry) on
19
+ `McpGatewayPlugin`.
20
+
21
+ ## What gets listed
22
+
23
+ The registry projects one entry per MCP-serving route — both:
24
+
25
+ - **Gateway virtual servers** — routes that carry MCP gateway policies
26
+ (`mcp-oauth-inbound` / `mcp-token-exchange-inbound`) and use
27
+ [`McpProxyHandler`](./code-config/mcp-proxy-handler.mdx) to proxy to an
28
+ upstream MCP server.
29
+ - **Standalone MCP servers** — routes that use the built-in `mcpServerHandler`
30
+ or a bare `McpProxyHandler` without gateway policies.
31
+
32
+ Each entry includes:
33
+
34
+ | Field | Source |
35
+ | ------------- | ----------------------------------------------------------------------------------------------------------------- |
36
+ | `name` | `<reverse-DNS of request host>/<operationId>` (for example, `com.example.gateway/linear-mcp-server`) |
37
+ | `remotes` | The gateway route URL, using the `streamable-http` transport. |
38
+ | `description` | OpenAPI operation `description` or `summary`, clamped to 100 code points per the spec. |
39
+ | `title` | `mcpServerHandler` options or the OpenAPI operation `summary`. |
40
+ | `version` | `mcpServerHandler` options or upstream connection config. Range-like values (`^1.2.3`, `1.x`) project as `0.0.0`. |
41
+ | `websiteUrl` | Upstream connection config, canonicalized to a valid URI. |
42
+ | `icons` | Upstream connection config. Only HTTPS URLs are included; `src` is clamped to 255 characters. |
43
+
44
+ :::note
45
+
46
+ Server names are derived from the request host — the same posture as the OAuth
47
+ issuer and Protected Resource Metadata. Names vary with the hostname the gateway
48
+ is reached on, so a custom domain produces different names than the default
49
+ `*.zuplo.dev` origin.
50
+
51
+ :::
52
+
53
+ The projection never throws. Every spec constraint is enforced by sanitizing or
54
+ omitting: names longer than 200 characters are truncated with a hash suffix,
55
+ descriptions are clamped, non-HTTPS icons are dropped, and version ranges are
56
+ replaced with `0.0.0`. One malformed route can't take down discovery for the
57
+ rest.
58
+
59
+ Parameterized routes (for example, `/mcp/{server}`) are omitted from the
60
+ directory — a templated path is not a concretely connectable remote.
61
+
62
+ ## Endpoints
63
+
64
+ All paths are relative to the registry mount (default: `/__zuplo/mcp-registry`).
65
+ Discovery GETs are unauthenticated and CORS-permissive
66
+ (`Access-Control-Allow-Origin: *`).
67
+
68
+ ### `GET {path}/v0.1/servers`
69
+
70
+ Lists all MCP servers registered on the gateway.
71
+
72
+ | Query parameter | Type | Default | Description |
73
+ | --------------- | ------- | ------- | ----------------------------------------------------------------------------------------------- |
74
+ | `cursor` | string | — | Opaque pagination cursor from a previous response. |
75
+ | `limit` | integer | `30` | Maximum servers to return. Clamped to `100`. |
76
+ | `search` | string | — | Case-insensitive substring filter on server name. |
77
+ | `version` | string | — | Filter to servers matching a specific version. |
78
+ | `updated_since` | string | — | RFC 3339 date-time. Validated but never filters — every entry conservatively counts as changed. |
79
+
80
+ Response:
81
+
82
+ ```json
83
+ {
84
+ "servers": [
85
+ {
86
+ "name": "com.example.gateway/linear-mcp-server",
87
+ "description": "Linear MCP proxy",
88
+ "title": "Linear",
89
+ "version": "1.0.0",
90
+ "remotes": [
91
+ {
92
+ "type": "streamable-http",
93
+ "url": "https://gateway.example.com/mcp/linear-v1"
94
+ }
95
+ ],
96
+ "websiteUrl": "https://linear.app",
97
+ "icons": []
98
+ }
99
+ ],
100
+ "metadata": { "count": 1 }
101
+ }
102
+ ```
103
+
104
+ When `cursor` is present in the response, more results are available. Pass it
105
+ back as the `cursor` query parameter on the next request.
106
+
107
+ ### `GET {path}/v0.1/servers/{serverName}/versions`
108
+
109
+ Lists versions for a single server. With the current read-only projection, every
110
+ server has exactly one version entry.
111
+
112
+ `{serverName}` is URL-encoded — the slash in the reverse-DNS name is sent as
113
+ `%2F` (for example, `com.example.gateway%2Flinear-mcp-server`).
114
+
115
+ ### `GET {path}/v0.1/servers/{serverName}/versions/{version}`
116
+
117
+ Returns a single server version. The special alias `latest` resolves to the
118
+ server's current version.
119
+
120
+ ### Write endpoints (refused)
121
+
122
+ The registry is read-only. Write endpoints are refused per the spec:
123
+
124
+ | Endpoint | Method | Status | Reason |
125
+ | ------------------------------------------------------------ | -------- | ------ | -------------------------------------------------------------------------------------------- |
126
+ | `{path}/v0.1/publish` | `POST` | `501` | Spec marks publishing optional and documents `501`. |
127
+ | `{path}/v0.1/servers/{serverName}/versions/{version}` | `PUT` | `501` | Spec marks version update optional. |
128
+ | `{path}/v0.1/servers/{serverName}/versions/{version}` | `DELETE` | `501` | Spec marks version deletion optional. |
129
+ | `{path}/v0.1/servers/{serverName}/versions/{version}/status` | `PATCH` | `403` | Spec defines no `501` for status operations; returns `403` with a permissions error message. |
130
+ | `{path}/v0.1/servers/{serverName}/status` | `PATCH` | `403` | Same as above. |
131
+
132
+ All error bodies use the spec's `{"error": "..."}` shape — not RFC 7807 problem
133
+ details — so off-the-shelf registry clients can parse them. A catch-all under
134
+ the mount ensures even unrecognized requests get the spec-shaped `404`.
135
+
136
+ ## Configuration
137
+
138
+ The registry is configured through the `McpGatewayPlugin` constructor. See the
139
+ [`registry` option](../programmable-api/mcp-gateway-plugin.mdx#registry) for the
140
+ full options reference.
141
+
142
+ ```ts title="modules/zuplo.runtime.ts"
143
+ import { RuntimeExtensions } from "@zuplo/runtime";
144
+ import { McpGatewayPlugin } from "@zuplo/runtime/mcp-gateway";
145
+
146
+ export function runtimeInit(runtime: RuntimeExtensions) {
147
+ runtime.addPlugin(
148
+ new McpGatewayPlugin({
149
+ registry: {
150
+ enabled: true, // default
151
+ path: "/mcp-registry", // default: "{basePath}/mcp-registry"
152
+ },
153
+ }),
154
+ );
155
+ }
156
+ ```
157
+
158
+ ## Limitations
159
+
160
+ - **No publishing.** The registry is a read-only projection of the gateway's
161
+ live route configuration. There is no write path, no namespace-ownership
162
+ model, and no stored `publishedAt` or `updatedAt` timestamps.
163
+ - **`updated_since` validates but never filters.** Because the registry is
164
+ derived from route config with no stored timestamps, every entry
165
+ conservatively counts as changed. This keeps mirroring subregistries correct
166
+ at the cost of re-reading everything.
167
+ - **No deployment-pinned server identity.** Server names are derived from the
168
+ request host, not from a stable deployment-level identifier.
169
+
170
+ ## Related
171
+
172
+ - [`McpGatewayPlugin` reference](../programmable-api/mcp-gateway-plugin.mdx) —
173
+ the full plugin options, including `basePath` and `registry`.
174
+ - [Gateway reference](./reference.mdx) — the full URL catalog, including the
175
+ registry mount.
176
+ - [How the MCP Gateway works](./how-it-works.mdx) — architecture and request
177
+ lifecycle.
178
+ - [MCP Server Registry specification](https://github.com/modelcontextprotocol/registry)
179
+ — the v0.1 freeze the gateway implements.
@@ -102,8 +102,8 @@ Whichever tool you pick, exercise these gateway behaviors during a smoke test:
102
102
  `WWW-Authenticate: Bearer resource_metadata=...`. If it doesn't, the route is
103
103
  missing an MCP OAuth policy.
104
104
  2. **OAuth handshake completes.** Confirm the inspector lands on the gateway's
105
- `/oauth/setup` consent page (rendered HTML), that the upstream's **Connect**
106
- button works, and that the inspector receives an access token.
105
+ `/__zuplo/oauth/setup` consent page (rendered HTML), that the upstream's
106
+ **Connect** button works, and that the inspector receives an access token.
107
107
  3. **`tools/list` returns the expected curated set.** If you've attached
108
108
  `mcp-capability-filter-inbound`, verify the filter is working — only
109
109
  allow-listed tools should appear.
@@ -432,6 +432,36 @@ export default async function (request: ZuploRequest, context: ZuploContext) {
432
432
  }
433
433
  ```
434
434
 
435
+ ## Controlling tool results with ZuploMcpSdk
436
+
437
+ By default, the MCP Server handler serializes the downstream response body into
438
+ a single text content block and, when `includeStructuredContent` is enabled,
439
+ derives `structuredContent` from it. The `ZuploMcpSdk` class gives you
440
+ programmatic control over what the AI client receives — without changing the
441
+ response your handler returns.
442
+
443
+ The most common use case is a **compact summary**: replace the full serialized
444
+ body in `content` with a short human-readable summary, while the gateway still
445
+ auto-derives `structuredContent` from the downstream response. This reduces the
446
+ token count in the model's context without losing data.
447
+
448
+ ```typescript
449
+ import { ZuploContext, ZuploMcpSdk, ZuploRequest } from "@zuplo/runtime";
450
+
451
+ export default async function (request: ZuploRequest, context: ZuploContext) {
452
+ const response = await context.invokeRoute("/todos");
453
+ new ZuploMcpSdk(context).setRawCallToolResult({
454
+ content: [{ type: "text", text: "Fetched the todo list" }],
455
+ });
456
+ return response;
457
+ }
458
+ ```
459
+
460
+ You can also read metadata from the incoming `tools/call` request using
461
+ `getRawCallToolRequest()`, and override `structuredContent` and `_meta`
462
+ independently. For the full API reference, see
463
+ [ZuploMcpSdk](../programmable-api/mcp-sdk.mdx).
464
+
435
465
  ## Best Practices
436
466
 
437
467
  ### Tool Design
@@ -479,6 +509,8 @@ export default async function (request: ZuploRequest, context: ZuploContext) {
479
509
 
480
510
  ## Learn More
481
511
 
512
+ - [ZuploMcpSdk](../programmable-api/mcp-sdk.mdx) - Programmatic control over MCP
513
+ tool results
482
514
  - [MCP Server Handler](../handlers/mcp-server.mdx) - For simple route-to-tool
483
515
  mapping
484
516
  - [Model Context Protocol Overview](../mcp-server/introduction.mdx) -
@@ -0,0 +1,137 @@
1
+ ---
2
+ title: "MCP Gateway Plugin"
3
+ sidebar_label: "MCP Gateway Plugin"
4
+ description:
5
+ Reference for the McpGatewayPlugin runtime plugin — options for the base path,
6
+ OAuth endpoints, and the read-only MCP Server Registry.
7
+ ---
8
+
9
+ `McpGatewayPlugin` activates the MCP Gateway internal routes on the runtime
10
+ router: the OAuth authorization server, upstream connection management,
11
+ well-known metadata endpoints, and the read-only
12
+ [MCP Server Registry](../mcp-gateway/server-registry.mdx). When no MCP-related
13
+ policy is present, the plugin registers no OAuth routes — it still records
14
+ `plugin.mcp-gateway` feature usage on construction so gateway adoption is
15
+ visible in telemetry.
16
+
17
+ Importing from `@zuplo/runtime/mcp-gateway` is the opt-in: the runtime core
18
+ doesn't depend on MCP Gateway code until the plugin is added.
19
+
20
+ ## Registration
21
+
22
+ Register the plugin in `modules/zuplo.runtime.ts`:
23
+
24
+ ```ts title="modules/zuplo.runtime.ts"
25
+ import { RuntimeExtensions } from "@zuplo/runtime";
26
+ import { McpGatewayPlugin } from "@zuplo/runtime/mcp-gateway";
27
+
28
+ export function runtimeInit(runtime: RuntimeExtensions) {
29
+ runtime.addPlugin(new McpGatewayPlugin());
30
+ }
31
+ ```
32
+
33
+ The plugin accepts an optional configuration object. All options have defaults,
34
+ so the no-argument form works for most projects.
35
+
36
+ ```ts title="modules/zuplo.runtime.ts"
37
+ import { RuntimeExtensions } from "@zuplo/runtime";
38
+ import { McpGatewayPlugin } from "@zuplo/runtime/mcp-gateway";
39
+
40
+ export function runtimeInit(runtime: RuntimeExtensions) {
41
+ runtime.addPlugin(
42
+ new McpGatewayPlugin({
43
+ basePath: "/__zuplo",
44
+ registry: {
45
+ enabled: true,
46
+ path: "/mcp-registry",
47
+ },
48
+ }),
49
+ );
50
+ }
51
+ ```
52
+
53
+ ## Options
54
+
55
+ | Option | Type | Default | Description |
56
+ | ---------- | --------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
57
+ | `basePath` | `string` | `"/__zuplo"` | Base path for all gateway internal routes (OAuth, well-known, registry). Must start with `/` and can't collide with route paths. |
58
+ | `registry` | `McpGatewayRegistryOptions` | See below | Configures the read-only [MCP Server Registry](../mcp-gateway/server-registry.mdx). |
59
+
60
+ ### `basePath`
61
+
62
+ The base path prefixes every internal URL the gateway serves — OAuth endpoints
63
+ (`/oauth/*`), well-known metadata (`/.well-known/*`), and the registry mount
64
+ (`/mcp-registry`). With the default `"/__zuplo"`, the token endpoint lives at
65
+ `/__zuplo/oauth/token` and the registry at `/__zuplo/mcp-registry`.
66
+
67
+ ```ts
68
+ new McpGatewayPlugin({ basePath: "/internal" });
69
+ // OAuth token: /internal/oauth/token
70
+ // Registry: /internal/mcp-registry
71
+ ```
72
+
73
+ ### `registry`
74
+
75
+ Configures the read-only MCP Server Registry API (v0.1) that advertises the
76
+ gateway's registered MCP routes. See
77
+ [MCP Server Registry](../mcp-gateway/server-registry.mdx) for the endpoint
78
+ reference and behavior.
79
+
80
+ | Option | Type | Default | Description |
81
+ | --------- | --------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
+ | `enabled` | `boolean` | `true` | Set to `false` to disable the registry entirely. No registry routes are registered when disabled. |
83
+ | `path` | `string` | `"{basePath}/mcp-registry"` | Absolute URL path where the registry API mounts. Registry endpoints live below this path (for example, `{path}/v0.1/servers`). Validated like `basePath`. |
84
+
85
+ The registry path is validated with the same schema as `basePath`, plus a
86
+ boot-time guard that rejects mounts shadowing the OAuth discovery
87
+ (`/.well-known`) or authorize wildcard routes.
88
+
89
+ :::note
90
+
91
+ When `registry.enabled` is `false`, the path collision guard is skipped — a
92
+ disabled registry registers no routes and must not fail boot over a mount that
93
+ will never exist.
94
+
95
+ :::
96
+
97
+ #### Default mount
98
+
99
+ With the default `basePath` of `"/__zuplo"` and no `registry` override, the
100
+ registry is served at:
101
+
102
+ ```text
103
+ /__zuplo/mcp-registry/v0.1/servers
104
+ ```
105
+
106
+ #### Custom registry path
107
+
108
+ Set `registry.path` to mount the registry independently of `basePath`:
109
+
110
+ ```ts
111
+ new McpGatewayPlugin({
112
+ registry: { path: "/registry" },
113
+ });
114
+ // Registry: /registry/v0.1/servers
115
+ // OAuth: /__zuplo/oauth/token (basePath unchanged)
116
+ ```
117
+
118
+ #### Disabled registry
119
+
120
+ Turn off the registry if you don't need server discovery:
121
+
122
+ ```ts
123
+ new McpGatewayPlugin({
124
+ registry: { enabled: false },
125
+ });
126
+ ```
127
+
128
+ ## Related
129
+
130
+ - [MCP Server Registry](../mcp-gateway/server-registry.mdx) — the read-only
131
+ registry API the plugin serves by default.
132
+ - [Set up an MCP Gateway](../mcp-gateway/code-config/overview.mdx) — the how-to
133
+ that puts this plugin into a project with policies and routes.
134
+ - [How the MCP Gateway works](../mcp-gateway/how-it-works.mdx) — the
135
+ architecture and request lifecycle.
136
+ - [Gateway reference](../mcp-gateway/reference.mdx) — the full URL catalog,
137
+ default TTLs, and configuration constants.
@@ -0,0 +1,240 @@
1
+ ---
2
+ title: ZuploMcpSdk
3
+ sidebar_label: MCP SDK
4
+ description:
5
+ Reference for the ZuploMcpSdk class, which gives custom MCP tool handlers
6
+ access to the incoming tool call request and control over the tool result sent
7
+ back to the AI client.
8
+ ---
9
+
10
+ The `ZuploMcpSdk` class provides a helper API for custom MCP tool handlers to
11
+ interact with the MCP runtime. Use it to read metadata from the incoming
12
+ `tools/call` request and to override fields of the tool result the gateway sends
13
+ back to the AI client.
14
+
15
+ ```ts
16
+ import { ZuploContext, ZuploMcpSdk, ZuploRequest } from "@zuplo/runtime";
17
+
18
+ export default async function (request: ZuploRequest, context: ZuploContext) {
19
+ const sdk = new ZuploMcpSdk(context);
20
+
21
+ // Read the incoming tool call request
22
+ const mcpRequest = sdk.getRawCallToolRequest();
23
+ context.log.info(`Tool called: ${mcpRequest?.params.name}`);
24
+
25
+ // Invoke a route on your gateway
26
+ const response = await context.invokeRoute("/todos");
27
+
28
+ // Override the content the model sees, keeping auto-derived structuredContent
29
+ sdk.setRawCallToolResult({
30
+ content: [{ type: "text", text: "Fetched the todo list" }],
31
+ });
32
+
33
+ return response;
34
+ }
35
+ ```
36
+
37
+ `ZuploMcpSdk` is available in custom tool handlers for both the
38
+ [MCP Server handler](../handlers/mcp-server.mdx) and the
39
+ [MCP Gateway](../mcp-gateway/introduction.mdx). It is exported from
40
+ `@zuplo/runtime`.
41
+
42
+ ## Methods
43
+
44
+ ### `setRawCallToolResult(result)`
45
+
46
+ Overrides fields of the MCP tool result the gateway is about to send to the AI
47
+ client. Every field is independently optional — an absent field keeps the value
48
+ the gateway derives from the downstream response.
49
+
50
+ ```ts
51
+ setRawCallToolResult(result: ZuploMcpToolResultOverride): void
52
+ ```
53
+
54
+ #### Parameters
55
+
56
+ The `result` argument is a `ZuploMcpToolResultOverride` object with the
57
+ following optional fields:
58
+
59
+ | Field | Type | Description |
60
+ | ------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
61
+ | `content` | `CallToolResult["content"]` | Content blocks the model reads. Replaces the gateway's default (the serialized downstream body in a text block). |
62
+ | `structuredContent` | `Record<string, unknown>` | The structured form of the tool's output. Must be a JSON object — the MCP 2025-11-25 wire format rejects arrays and scalars. |
63
+ | `_meta` | `Record<string, unknown>` | Per-call metadata. Replaces the gateway's default of `{}`. |
64
+
65
+ #### Compact summary example
66
+
67
+ The most useful combination is `content` alone: a human-readable summary
68
+ replaces the raw serialized body in the model's context, while
69
+ `structuredContent` is still auto-derived from the downstream response. This
70
+ reduces token consumption without losing data.
71
+
72
+ ```ts
73
+ import { ZuploContext, ZuploMcpSdk, ZuploRequest } from "@zuplo/runtime";
74
+
75
+ export default async function (request: ZuploRequest, context: ZuploContext) {
76
+ const response = await context.invokeRoute("/todos");
77
+ new ZuploMcpSdk(context).setRawCallToolResult({
78
+ content: [{ type: "text", text: "Fetched the todo list" }],
79
+ });
80
+ return response;
81
+ }
82
+ ```
83
+
84
+ The model sees `"Fetched the todo list"` instead of the full JSON body, but the
85
+ `structuredContent` field still carries the complete payload for spec-compliant
86
+ clients that read it.
87
+
88
+ #### Full override example
89
+
90
+ Override all available fields at once:
91
+
92
+ ```ts
93
+ import { ZuploContext, ZuploMcpSdk, ZuploRequest } from "@zuplo/runtime";
94
+
95
+ export default async function (request: ZuploRequest, context: ZuploContext) {
96
+ const response = await context.invokeRoute("/orders");
97
+ const orders = await response.json();
98
+
99
+ new ZuploMcpSdk(context).setRawCallToolResult({
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: `Found ${orders.length} orders totaling $${orders.total}`,
104
+ },
105
+ ],
106
+ structuredContent: { orders: orders.items, count: orders.length },
107
+ _meta: { source: "order-service", version: "2.0" },
108
+ });
109
+
110
+ return response;
111
+ }
112
+ ```
113
+
114
+ :::note
115
+
116
+ **`isError` is not overridable.** It follows the HTTP status of the response
117
+ your handler returns, so upstream failures cannot be masked. If the downstream
118
+ route returns a non-2xx status, the tool result carries `isError: true`
119
+ regardless of any override.
120
+
121
+ :::
122
+
123
+ :::caution{title="Single-use — consumed once"}
124
+
125
+ The override is **consumed** when the gateway assembles the tool result. It is
126
+ read and deleted from the context at that point, so calling
127
+ `setRawCallToolResult` twice in the same request replaces the first value before
128
+ the gateway reads it.
129
+
130
+ :::
131
+
132
+ #### How context lookup works
133
+
134
+ `setRawCallToolResult` writes the override to the context that carried the
135
+ matching `tools/call` request. The lookup travels exactly one
136
+ `context.invokeRoute` generation up from the context passed to the constructor.
137
+
138
+ This means:
139
+
140
+ - In a typical custom tool handler that calls `context.invokeRoute`, the
141
+ override is written to the parent context (the one carrying the `tools/call`
142
+ request), which is correct.
143
+ - If your handler calls `invokeRoute` to a route that **also** calls
144
+ `invokeRoute`, and you construct `ZuploMcpSdk` in that grandchild invocation,
145
+ the override is stored on the immediate parent — not the grandparent that
146
+ carries the tool call. The gateway never reads it. Construct `ZuploMcpSdk` in
147
+ the handler that is one level below the MCP route.
148
+
149
+ ### `getRawCallToolRequest()`
150
+
151
+ Retrieves the original MCP `tools/call` request object from the context. Use
152
+ this to access metadata like the `_meta` field from the incoming tool call.
153
+
154
+ ```ts
155
+ getRawCallToolRequest(): CallToolRequest | null
156
+ ```
157
+
158
+ Returns the `CallToolRequest` object, or `null` if no MCP tool call is in flight
159
+ on the current or parent context.
160
+
161
+ ```ts
162
+ import { ZuploContext, ZuploMcpSdk, ZuploRequest } from "@zuplo/runtime";
163
+
164
+ export default async function (request: ZuploRequest, context: ZuploContext) {
165
+ const sdk = new ZuploMcpSdk(context);
166
+ const mcpRequest = sdk.getRawCallToolRequest();
167
+
168
+ if (mcpRequest) {
169
+ const meta = mcpRequest.params._meta;
170
+ context.log.info(`Incoming _meta: ${JSON.stringify(meta)}`);
171
+ }
172
+
173
+ // ... handle the tool call
174
+ }
175
+ ```
176
+
177
+ Unlike `setRawCallToolResult`, this method can be called any number of times —
178
+ the request object is not consumed.
179
+
180
+ ## Output schema enforcement
181
+
182
+ When a tool advertises an `outputSchema` (via `includeOutputSchema` on the
183
+ handler or route config), the gateway validates the tool result's
184
+ `structuredContent` against that schema before sending it to the client. A
185
+ result whose `structuredContent` does not conform to the advertised schema fails
186
+ the call with a diagnostic `isError` result instead of a raw protocol error.
187
+
188
+ This enforcement is skipped on the error path (`isError: true`), matching the
189
+ MCP SDK's own behavior.
190
+
191
+ ### `includeOutputSchema` implies `includeStructuredContent`
192
+
193
+ Advertising an `outputSchema` while returning no `structuredContent` makes the
194
+ advertised schema inaccurate — spec-compliant clients reject the call. When
195
+ `includeOutputSchema` resolves to `true`, the gateway automatically forces
196
+ `includeStructuredContent` to `true` as well, and logs a warning naming the
197
+ route where the override kicked in.
198
+
199
+ You can set both explicitly to avoid the warning:
200
+
201
+ ```json
202
+ {
203
+ "x-zuplo-route": {
204
+ "handler": {
205
+ "export": "mcpServerHandler",
206
+ "module": "$import(@zuplo/runtime)",
207
+ "options": {
208
+ "includeOutputSchema": true,
209
+ "includeStructuredContent": true
210
+ }
211
+ }
212
+ }
213
+ }
214
+ ```
215
+
216
+ ## Type reference
217
+
218
+ ### `ZuploMcpToolResultOverride`
219
+
220
+ ```ts
221
+ interface ZuploMcpToolResultOverride {
222
+ content?: CallToolResult["content"];
223
+ structuredContent?: Record<string, unknown>;
224
+ _meta?: Record<string, unknown>;
225
+ }
226
+ ```
227
+
228
+ The subset of a `tools/call` result that a module author may override via
229
+ `setRawCallToolResult`. Every field is independently optional.
230
+
231
+ ## See also
232
+
233
+ - [MCP Server custom tools](../mcp-server/custom-tools.mdx) — how to build
234
+ custom MCP tool handlers with TypeScript
235
+ - [MCP Server handler](../handlers/mcp-server.mdx) — handler configuration
236
+ reference including `includeOutputSchema` and `includeStructuredContent`
237
+ - [MCP Gateway introduction](../mcp-gateway/introduction.mdx) — overview of the
238
+ MCP Gateway product
239
+ - [MCP specification](https://modelcontextprotocol.io/specification/) — the
240
+ canonical protocol reference
@@ -72,6 +72,8 @@ solutions architect walks through each item during onboarding.
72
72
 
73
73
  ### Kubernetes Cluster
74
74
 
75
+ - A cluster dedicated to Zuplo. If you want to run Zuplo in a multi-tenant
76
+ cluster, consult your Zuplo point of contact first.
75
77
  - A conformant Kubernetes cluster — managed offerings such as EKS, AKS, and GKE,
76
78
  or your own distribution.
77
79
  - Support for Services of type `LoadBalancer` to expose the ingress.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "7.4.4",
3
+ "version": "7.4.6",
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": "7.4.4",
23
- "@zuplo/core": "7.4.4",
24
- "@zuplo/runtime": "7.4.4",
25
- "@zuplo/test": "7.4.4"
22
+ "@zuplo/cli": "7.4.6",
23
+ "@zuplo/core": "7.4.6",
24
+ "@zuplo/runtime": "7.4.6",
25
+ "@zuplo/test": "7.4.6"
26
26
  }
27
27
  }