zuplo 7.4.4 → 7.4.5

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 (38) 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/how-it-works.mdx +11 -9
  30. package/docs/mcp-gateway/introduction.mdx +3 -1
  31. package/docs/mcp-gateway/quickstart-local.mdx +7 -7
  32. package/docs/mcp-gateway/reference.mdx +58 -25
  33. package/docs/mcp-gateway/server-registry.mdx +179 -0
  34. package/docs/mcp-gateway/test-clients.mdx +2 -2
  35. package/docs/mcp-server/custom-tools.mdx +32 -0
  36. package/docs/programmable-api/mcp-gateway-plugin.mdx +137 -0
  37. package/docs/programmable-api/mcp-sdk.mdx +240 -0
  38. package/package.json +5 -5
@@ -32,7 +32,9 @@ prompt injection or token misuse. Each gateway feature maps to one of five
32
32
  problems that show up the moment a team uses more than one or two MCP servers:
33
33
 
34
34
  1. **Discovery.** A single catalog of approved MCP servers your developers and
35
- agents can connect to. No more sharing OAuth client IDs in Slack.
35
+ agents can connect to. The gateway serves a read-only
36
+ [MCP Server Registry](./server-registry.mdx) API that advertises every
37
+ registered MCP route — no more sharing OAuth client IDs in Slack.
36
38
  2. **Authentication.** Translation from your corporate SSO to whatever each
37
39
  upstream MCP requires. MCP client config files no longer hold raw upstream
38
40
  credentials.
@@ -91,7 +91,7 @@ older project and the build complains about the compatibility date, see
91
91
  "jwksUrl": "http://127.0.0.1:9000/.well-known/jwks.json"
92
92
  },
93
93
  "browserLogin": {
94
- "url": "http://127.0.0.1:9000/oauth/dev-login"
94
+ "url": "http://127.0.0.1:9000/__zuplo/oauth/dev-login"
95
95
  }
96
96
  }
97
97
  }
@@ -100,12 +100,12 @@ older project and the build complains about the compatibility date, see
100
100
 
101
101
  :::caution
102
102
 
103
- `/oauth/dev-login` returns `403 Forbidden` for any request that doesn't
104
- arrive over loopback, so it's safe to leave configured, but only useful in
105
- local dev. Production deployments should use a real OIDC provider through one
106
- of the [IdP wrappers](./auth/overview.mdx#identity-providers). A common
107
- pattern is keeping two OAuth policies (one for production, one for dev) and
108
- selecting between them in `routes.oas.json` by environment.
103
+ `/__zuplo/oauth/dev-login` returns `403 Forbidden` for any request that
104
+ doesn't arrive over loopback, so it's safe to leave configured, but only
105
+ useful in local dev. Production deployments should use a real OIDC provider
106
+ through one of the [IdP wrappers](./auth/overview.mdx#identity-providers). A
107
+ common pattern is keeping two OAuth policies (one for production, one for
108
+ dev) and selecting between them in `routes.oas.json` by environment.
109
109
 
110
110
  :::
111
111
 
@@ -16,37 +16,70 @@ The URLs below are all relative to the gateway origin. For a project deployed to
16
16
  `https://my-gateway.zuplo.dev` with an MCP route at `/mcp/linear-v1`, the public
17
17
  route is `https://my-gateway.zuplo.dev/mcp/linear-v1`.
18
18
 
19
+ The gateway mounts its internal OAuth and upstream-connection action routes
20
+ under the `basePath` configured on the `McpGatewayPlugin`. The default base path
21
+ is `/__zuplo`, which keeps gateway endpoints out of your API's route namespace.
22
+ Discovery documents (`/.well-known/…`) always stay at the origin root because
23
+ RFC 8414 / RFC 9728 locate them relative to the issuer. Pass `basePath: "/"` in
24
+ the plugin options to mount action routes at the origin root instead.
25
+
19
26
  ### Well-known metadata
20
27
 
21
28
  | Path | Methods | Purpose |
22
29
  | ------------------------------------------------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
30
  | `/.well-known/oauth-authorization-server` | `GET`, `OPTIONS` | RFC 8414 Authorization Server metadata for the gateway. Issuer is the gateway origin. |
24
- | `/.well-known/oauth-authorization-server/{routePath*}` | `GET`, `OPTIONS` | Per-route AS metadata. The issuer is rebound to the route's canonical URI, and `authorization_endpoint` points at `/oauth/authorize/{routePath}`. |
31
+ | `/.well-known/oauth-authorization-server/{routePath*}` | `GET`, `OPTIONS` | Per-route AS metadata. The issuer is rebound to the route's canonical URI, and `authorization_endpoint` points at `{basePath}/oauth/authorize/{routePath}`. |
25
32
  | `/.well-known/oauth-protected-resource/{routePath*}` | `GET`, `OPTIONS` | RFC 9728 Protected Resource Metadata for an MCP route. Lists `resource`, `resource_name`, `authorization_servers`, `bearer_methods_supported`, `scopes_supported`, and `mcp_protocol_version`. |
26
- | `/.well-known/oauth-client/{connection}` | `GET` | OAuth Client ID Metadata Document the gateway hosts to identify itself to an upstream provider. Requires the `?authProfileId=` query parameter. |
27
33
 
28
34
  These routes are CORS-permissive (`Access-Control-Allow-Origin: *`) because
29
35
  spec-compliant browser-resident MCP clients fetch them cross-origin.
30
36
 
31
37
  ### OAuth endpoints
32
38
 
33
- | Path | Methods | Purpose |
34
- | ------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
- | `/oauth/register` | `POST` | RFC 7591 Dynamic Client Registration. Supports `none`, `client_secret_basic`, `client_secret_post`, and `private_key_jwt` token-endpoint auth methods. DCR clients expire after 90 days. |
36
- | `/oauth/authorize` | `GET` | Gateway-wide authorization endpoint. Requires the `resource` parameter unless exactly one MCP route is configured. |
37
- | `/oauth/authorize/{routePath*}` | `GET` | Per-route authorization endpoint. The `resource` is implicit from the path. |
38
- | `/oauth/callback` | `GET` | Browser-login callback from the configured identity provider. Renders the consent page. |
39
- | `/oauth/setup` | `GET`, `POST` | Consent screen. Lists the upstream the requested MCP route depends on. `POST` accepts `decision=continue` / `approve` / `cancel`. |
40
- | `/oauth/token` | `POST` | RFC 6749 token endpoint. Supports `authorization_code` and `refresh_token` grants. |
41
- | `/oauth/revoke` | `POST` | RFC 7009 revocation endpoint. Accepts public-client revocations without authentication. |
42
- | `/oauth/dev-login` | `GET` | Loopback-only dev shortcut. Returns `403` over non-loopback addresses. |
39
+ Paths below use the default `basePath` of `/__zuplo`. Substitute your configured
40
+ `basePath` if you changed it.
41
+
42
+ | Path | Methods | Purpose |
43
+ | ------------------------------------------------ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `/__zuplo/oauth/register` | `POST` | RFC 7591 Dynamic Client Registration. Supports `none`, `client_secret_basic`, `client_secret_post`, and `private_key_jwt` token-endpoint auth methods. DCR clients expire after 90 days. |
45
+ | `/__zuplo/oauth/authorize` | `GET` | Gateway-wide authorization endpoint. Requires the `resource` parameter unless exactly one MCP route is configured. |
46
+ | `/__zuplo/oauth/authorize/{routePath*}` | `GET` | Per-route authorization endpoint. The `resource` is implicit from the path. |
47
+ | `/__zuplo/oauth/callback` | `GET` | Browser-login callback from the configured identity provider. Renders the consent page. |
48
+ | `/__zuplo/oauth/setup` | `GET`, `POST` | Consent screen. Lists the upstream the requested MCP route depends on. `POST` accepts `decision=continue` / `approve` / `cancel`. |
49
+ | `/__zuplo/oauth/token` | `POST` | RFC 6749 token endpoint. Supports `authorization_code` and `refresh_token` grants. |
50
+ | `/__zuplo/oauth/revoke` | `POST` | RFC 7009 revocation endpoint. Accepts public-client revocations without authentication. |
51
+ | `/__zuplo/oauth/dev-login` | `GET` | Loopback-only dev shortcut. Returns `403` over non-loopback addresses. |
52
+ | `/__zuplo/.well-known/oauth-client/{connection}` | `GET` | OAuth Client ID Metadata Document the gateway hosts to identify itself to an upstream provider. Requires the `?authProfileId=` query parameter. |
43
53
 
44
54
  ### Upstream connection endpoints
45
55
 
46
- | Path | Methods | Purpose |
47
- | ----------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
- | `/auth/connections/{connection}/connect` | `GET` | Browser entry to the upstream OAuth flow. With `redirect=true`, returns a 302 to the upstream `/authorize`; otherwise returns `428` with the connect-required payload. |
49
- | `/auth/connections/{connection}/callback` | `GET` | Upstream OAuth callback. Renders a success or failure page. |
56
+ | Path | Methods | Purpose |
57
+ | ------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58
+ | `/__zuplo/auth/connections/{connection}/connect` | `GET` | Browser entry to the upstream OAuth flow. With `redirect=true`, returns a 302 to the upstream `/authorize`; otherwise returns `428` with the connect-required payload. |
59
+ | `/__zuplo/auth/connections/{connection}/callback` | `GET` | Upstream OAuth callback. Renders a success or failure page. |
60
+
61
+ ### MCP Server Registry
62
+
63
+ The gateway serves a read-only
64
+ [MCP Server Registry API](https://github.com/modelcontextprotocol/registry)
65
+ (v0.1) that advertises every MCP route. Enabled by default; see
66
+ [Server Registry](./server-registry.mdx) for the endpoint reference and
67
+ [`McpGatewayPlugin`](../programmable-api/mcp-gateway-plugin.mdx#registry) for
68
+ configuration.
69
+
70
+ | Path | Methods | Purpose |
71
+ | -------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------- |
72
+ | `{registryPath}/v0.1/servers` | `GET`, `OPTIONS` | List servers with `cursor`/`limit`/`search`/`version`/`updated_since`. |
73
+ | `{registryPath}/v0.1/servers/{serverName}/versions` | `GET`, `OPTIONS` | List versions for a single server. |
74
+ | `{registryPath}/v0.1/servers/{serverName}/versions/{version}` | `GET`, `OPTIONS` | Get a specific version (supports `latest` alias). |
75
+ | `{registryPath}/v0.1/publish` | `POST` | `501` — publishing is optional in the spec and not supported. |
76
+ | `{registryPath}/v0.1/servers/{serverName}/versions/{version}` | `PUT`, `DELETE` | `501` — version update/delete is optional in the spec. |
77
+ | `{registryPath}/v0.1/servers/{serverName}/versions/{version}/status` | `PATCH` | `403` — no publish/edit permissions. |
78
+ | `{registryPath}/v0.1/servers/{serverName}/status` | `PATCH` | `403` — no publish/edit permissions. |
79
+
80
+ `{registryPath}` defaults to `/__zuplo/mcp-registry`. Discovery GETs are
81
+ unauthenticated and CORS-permissive. Error bodies use the spec's
82
+ `{"error": "..."}` shape.
50
83
 
51
84
  ### Customer-defined MCP routes
52
85
 
@@ -74,17 +107,17 @@ DCR requests that include any other scope value are rejected with
74
107
  | Refresh token / grant | ~10 years | `gateway.refreshTokenTtlSeconds` on the OAuth policy. | Downstream refresh grants are gateway client sessions, not upstream OAuth token lifetimes. The default is intentionally long so the gateway doesn't impose a shorter session bound than the upstream provider's refresh-token policy already does. |
75
108
  | DCR-registered client | 90 days | Not configurable. | Encourages clients to use CIMD where possible; stale DCR clients age out automatically. |
76
109
  | Authorization code | 60 seconds | Not configurable. | OAuth 2.1 recommendation. |
77
- | `oauth_authorize` state | 15 minutes | `browserLogin.stateTtlSeconds`. | Window between `/oauth/authorize` and `/oauth/callback`. |
110
+ | `oauth_authorize` state | 15 minutes | `browserLogin.stateTtlSeconds`. | Window between `/__zuplo/oauth/authorize` and `/__zuplo/oauth/callback`. |
78
111
 
79
112
  ## Headers
80
113
 
81
114
  ### Required on requests to MCP routes
82
115
 
83
- | Header | Required | Notes |
84
- | --------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------- |
85
- | `Authorization: Bearer <token>` | Yes (after initial 401) | Opaque access token issued by `/oauth/token`. Tokens in query strings are rejected. |
86
- | `Accept: application/json, text/event-stream` | Yes | Per the Streamable HTTP transport spec. The gateway forwards the body as-is. |
87
- | `MCP-Protocol-Version: 2025-11-25` | Yes after `initialize` | Per the MCP spec. The gateway tracks the current MCP protocol revision. |
116
+ | Header | Required | Notes |
117
+ | --------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------- |
118
+ | `Authorization: Bearer <token>` | Yes (after initial 401) | Opaque access token issued by `/__zuplo/oauth/token`. Tokens in query strings are rejected. |
119
+ | `Accept: application/json, text/event-stream` | Yes | Per the Streamable HTTP transport spec. The gateway forwards the body as-is. |
120
+ | `MCP-Protocol-Version: 2025-11-25` | Yes after `initialize` | Per the MCP spec. The gateway tracks the current MCP protocol revision. |
88
121
 
89
122
  ### Honored when present
90
123
 
@@ -119,9 +152,9 @@ See [Compatibility dates](./code-config/compatibility-dates.mdx).
119
152
  In addition to the standard RFC 8414 / OIDC discovery fields, the gateway
120
153
  publishes a vendor extension:
121
154
 
122
- | Field | Type | Values | Purpose |
123
- | ---------------------------- | ------ | --------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
124
- | `x-zuplo-browser-login-kind` | string | `"federated_oidc"`, `"local_dev"` | Lets client tooling special-case local development configurations (which use `/oauth/dev-login` and a loopback IdP). |
155
+ | Field | Type | Values | Purpose |
156
+ | ---------------------------- | ------ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
157
+ | `x-zuplo-browser-login-kind` | string | `"federated_oidc"`, `"local_dev"` | Lets client tooling special-case local development configurations (which use `/__zuplo/oauth/dev-login` and a loopback IdP). |
125
158
 
126
159
  ## Public route URL pattern
127
160
 
@@ -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.