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.
- package/docs/articles/ci-cd-azure/local-testing.mdx +22 -4
- package/docs/articles/ci-cd-bitbucket/local-testing.mdx +22 -4
- package/docs/articles/ci-cd-circleci/local-testing.mdx +21 -4
- package/docs/articles/ci-cd-github/deploy-and-test.mdx +28 -13
- package/docs/articles/ci-cd-github/local-testing.mdx +36 -16
- package/docs/articles/ci-cd-gitlab/local-testing.mdx +22 -4
- package/docs/articles/github-deployment-testing.mdx +118 -31
- package/docs/articles/testing-getting-started.mdx +220 -0
- package/docs/articles/testing-preview-environments.mdx +148 -0
- package/docs/articles/testing-recipes.mdx +429 -0
- package/docs/articles/testing.mdx +138 -408
- package/docs/mcp-gateway/auth/configuring-auth0.mdx +6 -5
- package/docs/mcp-gateway/auth/configuring-clerk.mdx +4 -4
- package/docs/mcp-gateway/auth/configuring-cognito.mdx +5 -4
- package/docs/mcp-gateway/auth/configuring-entra.mdx +5 -4
- package/docs/mcp-gateway/auth/configuring-generic-oidc.mdx +8 -8
- package/docs/mcp-gateway/auth/configuring-google.mdx +4 -4
- package/docs/mcp-gateway/auth/configuring-keycloak.mdx +4 -3
- package/docs/mcp-gateway/auth/configuring-logto.mdx +4 -4
- package/docs/mcp-gateway/auth/configuring-okta.mdx +3 -3
- package/docs/mcp-gateway/auth/configuring-onelogin.mdx +3 -3
- package/docs/mcp-gateway/auth/configuring-ping.mdx +3 -3
- package/docs/mcp-gateway/auth/configuring-workos.mdx +5 -4
- package/docs/mcp-gateway/auth/manual-oauth-testing.mdx +8 -8
- package/docs/mcp-gateway/auth/overview.mdx +17 -17
- package/docs/mcp-gateway/auth/upstream-oauth.mdx +5 -5
- package/docs/mcp-gateway/code-config/local-development.mdx +14 -12
- package/docs/mcp-gateway/code-config/overview.mdx +9 -4
- package/docs/mcp-gateway/connect-clients/chatgpt.mdx +114 -56
- package/docs/mcp-gateway/how-it-works.mdx +11 -9
- package/docs/mcp-gateway/introduction.mdx +3 -1
- package/docs/mcp-gateway/quickstart-local.mdx +7 -7
- package/docs/mcp-gateway/reference.mdx +58 -25
- package/docs/mcp-gateway/server-registry.mdx +179 -0
- package/docs/mcp-gateway/test-clients.mdx +2 -2
- package/docs/mcp-server/custom-tools.mdx +32 -0
- package/docs/programmable-api/mcp-gateway-plugin.mdx +137 -0
- package/docs/programmable-api/mcp-sdk.mdx +240 -0
- package/docs/self-hosted/overview.md +2 -0
- 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
|
|
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.
|
|
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.
|
|
23
|
-
"@zuplo/core": "7.4.
|
|
24
|
-
"@zuplo/runtime": "7.4.
|
|
25
|
-
"@zuplo/test": "7.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
|
}
|