zuplo 6.70.69 → 6.70.70
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/ai-gateway/getting-started.mdx +12 -8
- package/docs/ai-gateway/introduction.mdx +11 -9
- package/docs/articles/api-key-buckets.mdx +4 -2
- package/docs/articles/archiving-requests-to-storage.mdx +4 -4
- package/docs/articles/branch-based-deployments.mdx +10 -8
- package/docs/articles/ci-cd-github/cleanup-on-branch-delete.mdx +52 -31
- package/docs/articles/ci-cd-github/pr-preview-environments.mdx +17 -6
- package/docs/articles/custom-ci-cd-azure.mdx +1 -1
- package/docs/articles/custom-ci-cd-bitbucket.mdx +1 -1
- package/docs/articles/custom-ci-cd-circleci.mdx +1 -1
- package/docs/articles/custom-ci-cd-github.mdx +1 -1
- package/docs/articles/custom-ci-cd-gitlab.mdx +1 -1
- package/docs/articles/graphql.mdx +276 -0
- package/docs/articles/monorepo-deployment.mdx +17 -3
- package/docs/articles/opentelemetry.mdx +5 -2
- package/docs/articles/per-user-rate-limits-using-db.mdx +5 -6
- package/docs/articles/securing-the-gateway-with-client-mtls.mdx +68 -43
- package/docs/articles/step-1-setup-basic-gateway.mdx +1 -3
- package/docs/articles/step-2-add-rate-limiting.mdx +1 -1
- package/docs/articles/testing.mdx +1 -1
- package/docs/articles/troubleshooting.md +7 -3
- package/docs/articles/waf-ddos-akamai.md +35 -16
- package/docs/articles/waf-ddos-aws-waf-shield.mdx +35 -16
- package/docs/articles/waf-ddos-fastly.mdx +10 -7
- package/docs/cli/deploy.mdx +13 -10
- package/docs/cli/deploy.partial.mdx +13 -10
- package/docs/dev-portal/zudoku/components/sidecar-box.mdx +131 -0
- package/docs/dev-portal/zudoku/configuration/api-catalog.md +62 -42
- package/docs/dev-portal/zudoku/configuration/api-reference.md +5 -4
- package/docs/dev-portal/zudoku/configuration/navigation.mdx +70 -7
- package/docs/guides/canary-routing-for-employees.mdx +103 -39
- package/docs/guides/modify-openapi-paths.mdx +3 -3
- package/docs/handlers/legacy-dev-portal-handler.mdx +1 -1
- package/docs/handlers/mcp-server.mdx +13 -11
- package/docs/handlers/url-forward.mdx +5 -1
- package/docs/handlers/url-rewrite.mdx +7 -2
- package/docs/handlers/websocket-handler.mdx +5 -1
- package/docs/mcp-gateway/observability/logging.mdx +19 -12
- package/docs/mcp-server/resources.mdx +27 -15
- package/docs/mcp-server/testing.mdx +0 -2
- package/docs/policies/archive-request-azure-storage-inbound/doc.md +1 -1
- package/docs/policies/archive-response-azure-storage-outbound/doc.md +1 -1
- package/docs/policies/ip-restriction-inbound/policy.ts +1 -1
- package/docs/programmable-api/http-problems.mdx +0 -18
- package/docs/programmable-api/jwt-service-plugin.mdx +131 -109
- package/docs/programmable-api/runtime-behaviors.mdx +4 -2
- package/docs/programmable-api/streaming-zone-cache.mdx +4 -6
- package/docs/programmable-api/web-crypto-apis.mdx +10 -6
- package/package.json +4 -4
- package/docs/errors/get-head-body-error.mdx +0 -41
|
@@ -32,6 +32,10 @@ If any condition is met, the request routes to canary backends.
|
|
|
32
32
|
|
|
33
33
|
## Creating a Canary Routing Policy
|
|
34
34
|
|
|
35
|
+
The policy doesn't set the backend URL directly. Instead, it stores the chosen
|
|
36
|
+
backend on `context.custom`, and the route's handler reads that value to forward
|
|
37
|
+
the request (see "Adding the Policy to Routes" below).
|
|
38
|
+
|
|
35
39
|
Create a new policy file in your project:
|
|
36
40
|
|
|
37
41
|
```typescript
|
|
@@ -62,9 +66,9 @@ export const canaryRoutingPolicy: InboundPolicyHandler = async (
|
|
|
62
66
|
const isCanary =
|
|
63
67
|
stageParam === "canary" || stageHeader === "canary" || canaryUser;
|
|
64
68
|
|
|
65
|
-
//
|
|
69
|
+
// Store the backend URL for the route's handler to use
|
|
66
70
|
if (isCanary) {
|
|
67
|
-
context.
|
|
71
|
+
context.custom.backendUrl = environment.API_URL_CANARY;
|
|
68
72
|
|
|
69
73
|
// Log canary routing for debugging
|
|
70
74
|
context.log.info("Routing to canary backend", {
|
|
@@ -73,13 +77,19 @@ export const canaryRoutingPolicy: InboundPolicyHandler = async (
|
|
|
73
77
|
stage: stageParam || stageHeader || "canary",
|
|
74
78
|
});
|
|
75
79
|
} else {
|
|
76
|
-
context.
|
|
80
|
+
context.custom.backendUrl = environment.API_URL_PRODUCTION;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
// Remove stage query parameter to avoid passing it to backend
|
|
80
84
|
if (stageParam) {
|
|
81
85
|
url.searchParams.delete("stage");
|
|
82
|
-
return new ZuploRequest(url.toString(),
|
|
86
|
+
return new ZuploRequest(url.toString(), {
|
|
87
|
+
method: request.method,
|
|
88
|
+
headers: request.headers,
|
|
89
|
+
body: request.body,
|
|
90
|
+
user: request.user,
|
|
91
|
+
params: request.params,
|
|
92
|
+
});
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
return request;
|
|
@@ -139,24 +149,30 @@ export const multiServiceCanaryRoutingPolicy: InboundPolicyHandler = async (
|
|
|
139
149
|
// Clean up query parameter
|
|
140
150
|
if (stageParam) {
|
|
141
151
|
url.searchParams.delete("stage");
|
|
142
|
-
return new ZuploRequest(url.toString(),
|
|
152
|
+
return new ZuploRequest(url.toString(), {
|
|
153
|
+
method: request.method,
|
|
154
|
+
headers: request.headers,
|
|
155
|
+
body: request.body,
|
|
156
|
+
user: request.user,
|
|
157
|
+
params: request.params,
|
|
158
|
+
});
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
return request;
|
|
146
162
|
};
|
|
147
163
|
```
|
|
148
164
|
|
|
165
|
+
Each service's route then reads the matching value in its handler options. For
|
|
166
|
+
example, the user API route would use a URL Forward handler with
|
|
167
|
+
`"baseUrl": "${context.custom.userApiUrl}"`.
|
|
168
|
+
|
|
149
169
|
## Percentage-Based Canary Routing
|
|
150
170
|
|
|
151
171
|
Roll out canary deployments gradually with percentage-based routing:
|
|
152
172
|
|
|
153
173
|
```typescript
|
|
154
174
|
// policies/percentage-canary-routing.ts
|
|
155
|
-
import {
|
|
156
|
-
InboundPolicyHandler,
|
|
157
|
-
ZuploRequest,
|
|
158
|
-
environment,
|
|
159
|
-
} from "@zuplo/runtime";
|
|
175
|
+
import { InboundPolicyHandler, environment } from "@zuplo/runtime";
|
|
160
176
|
|
|
161
177
|
export const percentageCanaryRoutingPolicy: InboundPolicyHandler = async (
|
|
162
178
|
request,
|
|
@@ -168,7 +184,7 @@ export const percentageCanaryRoutingPolicy: InboundPolicyHandler = async (
|
|
|
168
184
|
: [];
|
|
169
185
|
|
|
170
186
|
if (request.user?.sub && CANARY_USERS.includes(request.user.sub)) {
|
|
171
|
-
context.
|
|
187
|
+
context.custom.backendUrl = environment.API_URL_CANARY;
|
|
172
188
|
return request;
|
|
173
189
|
}
|
|
174
190
|
|
|
@@ -176,8 +192,12 @@ export const percentageCanaryRoutingPolicy: InboundPolicyHandler = async (
|
|
|
176
192
|
const CANARY_PERCENTAGE = parseInt(environment.CANARY_PERCENTAGE || "0", 10);
|
|
177
193
|
|
|
178
194
|
if (CANARY_PERCENTAGE > 0) {
|
|
179
|
-
// Use a consistent hash for sticky sessions
|
|
180
|
-
|
|
195
|
+
// Use a consistent hash for sticky sessions. The client IP is
|
|
196
|
+
// available on the true-client-ip header.
|
|
197
|
+
const sessionId =
|
|
198
|
+
request.headers.get("x-session-id") ??
|
|
199
|
+
request.headers.get("true-client-ip") ??
|
|
200
|
+
"unknown";
|
|
181
201
|
const hash = await crypto.subtle.digest(
|
|
182
202
|
"SHA-256",
|
|
183
203
|
new TextEncoder().encode(sessionId),
|
|
@@ -186,13 +206,13 @@ export const percentageCanaryRoutingPolicy: InboundPolicyHandler = async (
|
|
|
186
206
|
const hashValue = hashArray[0] / 255; // Value between 0 and 1
|
|
187
207
|
|
|
188
208
|
if (hashValue * 100 < CANARY_PERCENTAGE) {
|
|
189
|
-
context.
|
|
209
|
+
context.custom.backendUrl = environment.API_URL_CANARY;
|
|
190
210
|
request.headers.set("X-Canary-Route", "percentage");
|
|
191
211
|
} else {
|
|
192
|
-
context.
|
|
212
|
+
context.custom.backendUrl = environment.API_URL_PRODUCTION;
|
|
193
213
|
}
|
|
194
214
|
} else {
|
|
195
|
-
context.
|
|
215
|
+
context.custom.backendUrl = environment.API_URL_PRODUCTION;
|
|
196
216
|
}
|
|
197
217
|
|
|
198
218
|
return request;
|
|
@@ -221,7 +241,9 @@ CANARY_PERCENTAGE=10 # Route 10% of traffic to canary
|
|
|
221
241
|
|
|
222
242
|
### Adding the Policy to Routes
|
|
223
243
|
|
|
224
|
-
Add the policy to your route configuration
|
|
244
|
+
Add the policy to your route configuration. Use the built-in
|
|
245
|
+
[URL Forward handler](../handlers/url-forward.mdx) and reference the value the
|
|
246
|
+
policy stored on `context.custom` in the `baseUrl` option:
|
|
225
247
|
|
|
226
248
|
```json
|
|
227
249
|
{
|
|
@@ -231,14 +253,14 @@ Add the policy to your route configuration:
|
|
|
231
253
|
"x-zuplo-route": {
|
|
232
254
|
"corsPolicy": "anything-goes",
|
|
233
255
|
"handler": {
|
|
234
|
-
"export": "
|
|
256
|
+
"export": "urlForwardHandler",
|
|
235
257
|
"module": "$import(@zuplo/runtime)",
|
|
236
258
|
"options": {
|
|
237
|
-
"
|
|
259
|
+
"baseUrl": "${context.custom.backendUrl}"
|
|
238
260
|
}
|
|
239
261
|
},
|
|
240
262
|
"policies": {
|
|
241
|
-
"inbound": ["
|
|
263
|
+
"inbound": ["auth-policy", "canary-routing"]
|
|
242
264
|
}
|
|
243
265
|
}
|
|
244
266
|
}
|
|
@@ -247,6 +269,9 @@ Add the policy to your route configuration:
|
|
|
247
269
|
}
|
|
248
270
|
```
|
|
249
271
|
|
|
272
|
+
The authentication policy runs first so that `request.user` is populated when
|
|
273
|
+
the canary routing policy checks the allow list.
|
|
274
|
+
|
|
250
275
|
## Testing Your Policy
|
|
251
276
|
|
|
252
277
|
### 1. Using Query Parameters
|
|
@@ -289,7 +314,9 @@ curl https://your-api.zuplo.app/api/v1/users \
|
|
|
289
314
|
|
|
290
315
|
## Monitoring and Observability
|
|
291
316
|
|
|
292
|
-
Add comprehensive logging to track canary routing
|
|
317
|
+
Add comprehensive logging to track canary routing. Inbound policies run before a
|
|
318
|
+
response exists, so set debug response headers from a
|
|
319
|
+
[response sending hook](../programmable-api/hooks.mdx):
|
|
293
320
|
|
|
294
321
|
```typescript
|
|
295
322
|
export const canaryRoutingPolicy: InboundPolicyHandler = async (
|
|
@@ -298,20 +325,26 @@ export const canaryRoutingPolicy: InboundPolicyHandler = async (
|
|
|
298
325
|
) => {
|
|
299
326
|
const startTime = Date.now();
|
|
300
327
|
|
|
328
|
+
const CANARY_USERS = environment.CANARY_USERS
|
|
329
|
+
? environment.CANARY_USERS.split(",").map((user) => user.trim())
|
|
330
|
+
: [];
|
|
331
|
+
|
|
301
332
|
// Check stage conditions
|
|
302
333
|
const url = new URL(request.url);
|
|
303
334
|
const stageParam = url.searchParams.get("stage");
|
|
304
335
|
const stageHeader = request.headers.get("x-stage");
|
|
305
|
-
const canaryUser =
|
|
336
|
+
const canaryUser =
|
|
337
|
+
request.user?.sub && CANARY_USERS.includes(request.user.sub);
|
|
306
338
|
|
|
307
339
|
const isCanary =
|
|
308
|
-
stageParam === "canary" ||
|
|
309
|
-
stageHeader === "canary" ||
|
|
310
|
-
canaryUser;
|
|
340
|
+
stageParam === "canary" || stageHeader === "canary" || canaryUser;
|
|
311
341
|
|
|
312
|
-
|
|
313
|
-
|
|
342
|
+
const backendType = isCanary ? "canary" : "release";
|
|
343
|
+
context.custom.backendUrl = isCanary
|
|
344
|
+
? environment.API_URL_CANARY
|
|
345
|
+
: environment.API_URL_PRODUCTION;
|
|
314
346
|
|
|
347
|
+
if (isCanary) {
|
|
315
348
|
// Log canary routing metrics
|
|
316
349
|
context.log.info("Canary route selected", {
|
|
317
350
|
userId: request.user?.sub,
|
|
@@ -320,14 +353,14 @@ export const canaryRoutingPolicy: InboundPolicyHandler = async (
|
|
|
320
353
|
stage: "canary",
|
|
321
354
|
duration: Date.now() - startTime,
|
|
322
355
|
});
|
|
323
|
-
|
|
324
|
-
// Add response header for debugging
|
|
325
|
-
context.response.headers.set("X-Backend-Type", "canary");
|
|
326
|
-
} else {
|
|
327
|
-
context.route.url = environment.API_URL_PRODUCTION;
|
|
328
|
-
context.response.headers.set("X-Backend-Type", "release");
|
|
329
356
|
}
|
|
330
357
|
|
|
358
|
+
// Add a response header for debugging
|
|
359
|
+
context.addResponseSendingHook((response) => {
|
|
360
|
+
response.headers.set("X-Backend-Type", backendType);
|
|
361
|
+
return response;
|
|
362
|
+
});
|
|
363
|
+
|
|
331
364
|
return request;
|
|
332
365
|
};
|
|
333
366
|
```
|
|
@@ -359,14 +392,45 @@ if (isCanaryUser) {
|
|
|
359
392
|
|
|
360
393
|
### 3. Fallback Handling
|
|
361
394
|
|
|
362
|
-
|
|
395
|
+
To fall back to production when the canary backend fails, replace the URL
|
|
396
|
+
Forward handler with a [custom handler](../handlers/custom-handler.mdx) that
|
|
397
|
+
retries against production on server errors:
|
|
363
398
|
|
|
364
399
|
```typescript
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
400
|
+
// modules/canary-fallback-handler.ts
|
|
401
|
+
import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime";
|
|
402
|
+
|
|
403
|
+
export default async function handler(
|
|
404
|
+
request: ZuploRequest,
|
|
405
|
+
context: ZuploContext,
|
|
406
|
+
) {
|
|
407
|
+
const url = new URL(request.url);
|
|
408
|
+
const backendUrl = context.custom.backendUrl;
|
|
409
|
+
const target = `${backendUrl}${url.pathname}${url.search}`;
|
|
410
|
+
|
|
411
|
+
// Buffer the body so the request can be retried
|
|
412
|
+
const body = request.body ? await request.arrayBuffer() : undefined;
|
|
413
|
+
|
|
414
|
+
const response = await fetch(target, {
|
|
415
|
+
method: request.method,
|
|
416
|
+
headers: request.headers,
|
|
417
|
+
body,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Retry against production if the canary backend returns a server error
|
|
421
|
+
if (backendUrl === environment.API_URL_CANARY && response.status >= 500) {
|
|
422
|
+
context.log.warn("Canary backend failed, falling back to production", {
|
|
423
|
+
status: response.status,
|
|
424
|
+
});
|
|
425
|
+
const fallbackBase = environment.API_URL_PRODUCTION;
|
|
426
|
+
return fetch(`${fallbackBase}${url.pathname}${url.search}`, {
|
|
427
|
+
method: request.method,
|
|
428
|
+
headers: request.headers,
|
|
429
|
+
body,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return response;
|
|
370
434
|
}
|
|
371
435
|
```
|
|
372
436
|
|
|
@@ -106,8 +106,8 @@ You can generate multiple versions as part of your build process:
|
|
|
106
106
|
```json title="package.json"
|
|
107
107
|
{
|
|
108
108
|
"scripts": {
|
|
109
|
-
"build:api:v1": "
|
|
110
|
-
"build:api:v2": "
|
|
109
|
+
"build:api:v1": "node add-path-prefix.mjs /v1 openapi.json dist/openapi-v1.json",
|
|
110
|
+
"build:api:v2": "node add-path-prefix.mjs /v2 openapi.json dist/openapi-v2.json",
|
|
111
111
|
"build:api:all": "npm run build:api:v1 && npm run build:api:v2"
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -128,7 +128,7 @@ mkdir -p $OUTPUT_DIR
|
|
|
128
128
|
for prefix in "${PREFIXES[@]}"; do
|
|
129
129
|
echo "Building with prefix: /$prefix"
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
node add-path-prefix.mjs \
|
|
132
132
|
"/$prefix" \
|
|
133
133
|
"$BASE_FILE" \
|
|
134
134
|
"$OUTPUT_DIR/openapi-$prefix.json"
|
|
@@ -35,14 +35,15 @@ Open the **Route Designer** by navigating to the **Code** tab, then click
|
|
|
35
35
|
**routes.oas.json**. For any route definition, select **MCP Server** from the
|
|
36
36
|
**Request Handlers** drop-down. Set the method to **POST**.
|
|
37
37
|
|
|
38
|
-
Configure the handler with the following
|
|
38
|
+
Configure the handler with the following options:
|
|
39
39
|
|
|
40
|
-
- **Server Name** - The name of the MCP server. AI MCP clients will
|
|
41
|
-
name when they initialize with the server.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
- **Server Name** (optional) - The name of the MCP server. AI MCP clients will
|
|
41
|
+
read this name when they initialize with the server. If not set, the server
|
|
42
|
+
advertises the default name `Zuplo MCP Server`.
|
|
43
|
+
- **Server Version** (optional) - The version of your MCP server. AI MCP clients
|
|
44
|
+
read this version when they initialize with the server and may make autonomous
|
|
45
|
+
decisions based on the versioning of your MCP server and the instructions
|
|
46
|
+
they've been given. If not set, the version defaults to `0.0.0`.
|
|
46
47
|
|
|
47
48
|
Next, configure your routes to be transformed into MCP tools or prompts (see
|
|
48
49
|
Configuration section below).
|
|
@@ -65,7 +66,7 @@ with the following route configuration:
|
|
|
65
66
|
"handler": {
|
|
66
67
|
|
|
67
68
|
// The MCP Server Handler handler
|
|
68
|
-
// and
|
|
69
|
+
// and example options
|
|
69
70
|
|
|
70
71
|
"export": "mcpServerHandler",
|
|
71
72
|
"module": "$import(@zuplo/runtime)",
|
|
@@ -86,10 +87,11 @@ with the following route configuration:
|
|
|
86
87
|
|
|
87
88
|
## Configuration
|
|
88
89
|
|
|
89
|
-
The MCP Server handler
|
|
90
|
+
The MCP Server handler supports the following configuration options:
|
|
90
91
|
|
|
91
|
-
- `name` (optional) - The name identifier of the MCP
|
|
92
|
-
|
|
92
|
+
- `name` (optional, default `Zuplo MCP Server`) - The name identifier of the MCP
|
|
93
|
+
server.
|
|
94
|
+
- `version` (optional, default `0.0.0`) - The version of the MCP server.
|
|
93
95
|
- `debugMode` (optional, default `false`) - Verbose logs on server startup,
|
|
94
96
|
initialization, tool listing, and tool calls. NOT recommended for production
|
|
95
97
|
environments.
|
|
@@ -53,6 +53,8 @@ The following objects are available for substitution:
|
|
|
53
53
|
`params.productId` would be the value of `:productId` in a route.
|
|
54
54
|
- `query: Record<string, string>` - The query parameters of the route. For
|
|
55
55
|
example, `query.filterBy` would be the value of `?filterBy=VALUE`.
|
|
56
|
+
- `method: string` - The HTTP method of the incoming request. For example, `GET`
|
|
57
|
+
or `POST`.
|
|
56
58
|
- `headers: Headers` - the incoming request's
|
|
57
59
|
[headers object](https://developer.mozilla.org/en-US/docs/Web/API/Headers)
|
|
58
60
|
- `url: string` - The full incoming request as a string
|
|
@@ -62,6 +64,9 @@ The following objects are available for substitution:
|
|
|
62
64
|
- `hostname: string` - The
|
|
63
65
|
[`hostname`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hostname)
|
|
64
66
|
portion of the incoming URL
|
|
67
|
+
- `origin: string` - The
|
|
68
|
+
[`origin`](https://developer.mozilla.org/en-US/docs/Web/API/URL/origin)
|
|
69
|
+
portion of the incoming URL
|
|
65
70
|
- `pathname: string` - The
|
|
66
71
|
[`pathname`](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname)
|
|
67
72
|
portion of the incoming URL
|
|
@@ -93,7 +98,6 @@ A few examples of the values of various substitutions.
|
|
|
93
98
|
- `${params.productId}` - `":productId"`
|
|
94
99
|
- `${pathname}` - `"/v1/products/:productId"`
|
|
95
100
|
- `${port}` - `"8080"`
|
|
96
|
-
- `${protocol}` - `"https:"`
|
|
97
101
|
- `${query.category}` - `"cars"`
|
|
98
102
|
- `${search}` - `"?category=cars"`
|
|
99
103
|
- `${url}` - `"https://example.com:8080/v1/products/:productId?category=cars"`
|
|
@@ -43,6 +43,8 @@ The following objects are available for substitution:
|
|
|
43
43
|
`params.productId` would be the value of `:productId` in a route.
|
|
44
44
|
- `query: Record<string, string>` - The query parameters of the route. For
|
|
45
45
|
example, `query.filterBy` would be the value of `?filterBy=VALUE`.
|
|
46
|
+
- `method: string` - The HTTP method of the incoming request. For example, `GET`
|
|
47
|
+
or `POST`.
|
|
46
48
|
- `headers: Headers` - the incoming request's
|
|
47
49
|
[headers object](https://developer.mozilla.org/en-US/docs/Web/API/Headers)
|
|
48
50
|
- `url: string` - The full incoming request as a string
|
|
@@ -52,6 +54,9 @@ The following objects are available for substitution:
|
|
|
52
54
|
- `hostname: string` - The
|
|
53
55
|
[`hostname`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hostname)
|
|
54
56
|
portion of the incoming URL
|
|
57
|
+
- `origin: string` - The
|
|
58
|
+
[`origin`](https://developer.mozilla.org/en-US/docs/Web/API/URL/origin)
|
|
59
|
+
portion of the incoming URL
|
|
55
60
|
- `pathname: string` - The
|
|
56
61
|
[`pathname`](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname)
|
|
57
62
|
portion of the incoming URL
|
|
@@ -83,7 +88,6 @@ A few examples of the values of various substitutions.
|
|
|
83
88
|
- `${params.productId}` - `":productId"`
|
|
84
89
|
- `${pathname}` - `"/v1/products/:productId"`
|
|
85
90
|
- `${port}` - `"8080"`
|
|
86
|
-
- `${protocol}` - `"https:"`
|
|
87
91
|
- `${query.category}` - `"cars"`
|
|
88
92
|
- `${search}` - `"?category=cars"`
|
|
89
93
|
- `${url}` - `"https://example.com:8080/v1/products/:productId?category=cars"`
|
|
@@ -132,7 +136,8 @@ use-cases.
|
|
|
132
136
|
- Type: `string`
|
|
133
137
|
- Supports JavaScript template interpolation with request context
|
|
134
138
|
- Available variables: `env`, `request`, `context`, `params`, `query`,
|
|
135
|
-
`headers`, `url`, `host`, `hostname`, `
|
|
139
|
+
`method`, `headers`, `url`, `host`, `hostname`, `origin`, `pathname`,
|
|
140
|
+
`port`, `search`
|
|
136
141
|
- Example: `"https://api-${params.version}.example.com/users/${params.id}"`
|
|
137
142
|
|
|
138
143
|
- **`forwardSearch`** (optional): Controls whether query parameters are
|
|
@@ -84,6 +84,8 @@ The following objects are available for substitution:
|
|
|
84
84
|
`params.productId` would be the value of `:productId` in a route.
|
|
85
85
|
- `query: Record<string, string>` - The query parameters of the route. For
|
|
86
86
|
example, `query.filterBy` would be the value of `?filterBy=VALUE`.
|
|
87
|
+
- `method: string` - The HTTP method of the incoming request. For example, `GET`
|
|
88
|
+
or `POST`.
|
|
87
89
|
- `headers: Headers` - the incoming request's
|
|
88
90
|
[headers object](https://developer.mozilla.org/en-US/docs/Web/API/Headers)
|
|
89
91
|
- `url: string` - The full incoming request as a string
|
|
@@ -93,6 +95,9 @@ The following objects are available for substitution:
|
|
|
93
95
|
- `hostname: string` - The
|
|
94
96
|
[`hostname`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hostname)
|
|
95
97
|
portion of the incoming URL
|
|
98
|
+
- `origin: string` - The
|
|
99
|
+
[`origin`](https://developer.mozilla.org/en-US/docs/Web/API/URL/origin)
|
|
100
|
+
portion of the incoming URL
|
|
96
101
|
- `pathname: string` - The
|
|
97
102
|
[`pathname`](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname)
|
|
98
103
|
portion of the incoming URL
|
|
@@ -124,7 +129,6 @@ A few examples of the values of various substitutions.
|
|
|
124
129
|
- `${params.productId}` - `":productId"`
|
|
125
130
|
- `${pathname}` - `"/v1/products/:productId"`
|
|
126
131
|
- `${port}` - `"8080"`
|
|
127
|
-
- `${protocol}` - `"https:"`
|
|
128
132
|
- `${query.category}` - `"cars"`
|
|
129
133
|
- `${search}` - `"?category=cars"`
|
|
130
134
|
- `${url}` - `"https://example.com:8080/v1/products/:productId?category=cars"`
|
|
@@ -141,33 +141,40 @@ structured entries described above with their `event` field preserved.
|
|
|
141
141
|
Registering both plugins is a small addition to `modules/zuplo.runtime.ts`:
|
|
142
142
|
|
|
143
143
|
```ts title="modules/zuplo.runtime.ts"
|
|
144
|
-
import {
|
|
144
|
+
import { OpenTelemetryPlugin } from "@zuplo/otel";
|
|
145
|
+
import { environment, RuntimeExtensions } from "@zuplo/runtime";
|
|
145
146
|
import { McpGatewayPlugin } from "@zuplo/runtime/mcp-gateway";
|
|
146
|
-
import { OpenTelemetryPlugin } from "@zuplo/runtime";
|
|
147
147
|
|
|
148
148
|
export function runtimeInit(runtime: RuntimeExtensions) {
|
|
149
149
|
runtime.addPlugin(new McpGatewayPlugin());
|
|
150
150
|
runtime.addPlugin(
|
|
151
151
|
new OpenTelemetryPlugin({
|
|
152
|
-
|
|
153
|
-
logUrl:
|
|
154
|
-
|
|
152
|
+
traceUrl: environment.OTLP_TRACES_ENDPOINT,
|
|
153
|
+
logUrl: environment.OTLP_LOGS_ENDPOINT,
|
|
154
|
+
headers: {
|
|
155
|
+
Authorization: environment.OTLP_AUTHORIZATION,
|
|
156
|
+
},
|
|
157
|
+
service: {
|
|
158
|
+
name: "mcp-gateway",
|
|
159
|
+
},
|
|
155
160
|
}),
|
|
156
161
|
);
|
|
157
162
|
}
|
|
158
163
|
```
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
|
|
165
|
+
Logs are only exported when `logUrl` is set alongside `traceUrl` — configuring
|
|
166
|
+
only `exporter.url` exports traces alone. Grafana Cloud is one common
|
|
167
|
+
destination — define the endpoints and credentials as
|
|
168
|
+
[environment variables](../../articles/environment-variables.mdx):
|
|
162
169
|
|
|
163
170
|
```bash
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
OTLP_TRACES_ENDPOINT=https://otlp-gateway-prod-<region>.grafana.net/otlp/v1/traces
|
|
172
|
+
OTLP_LOGS_ENDPOINT=https://otlp-gateway-prod-<region>.grafana.net/otlp/v1/logs
|
|
173
|
+
OTLP_AUTHORIZATION=Basic <base64 of instance-id:api-token>
|
|
167
174
|
```
|
|
168
175
|
|
|
169
|
-
|
|
170
|
-
`/v1/traces` and `/v1/logs`
|
|
176
|
+
The plugin sends OTLP data to the configured URLs exactly as given, so include
|
|
177
|
+
the full `/v1/traces` and `/v1/logs` paths. Other OTLP-compatible destinations
|
|
171
178
|
(Honeycomb, Dynatrace, New Relic, Tempo, self-hosted Jaeger) work the same way;
|
|
172
179
|
substitute the endpoint and credential headers.
|
|
173
180
|
|
|
@@ -191,27 +191,36 @@ Response:
|
|
|
191
191
|
"result": {
|
|
192
192
|
"resources": [
|
|
193
193
|
{
|
|
194
|
-
"name": "
|
|
195
|
-
"uri": "
|
|
196
|
-
"title": "
|
|
197
|
-
"description": "
|
|
198
|
-
"mimeType": "text/
|
|
194
|
+
"name": "html_doc",
|
|
195
|
+
"uri": "ui://html",
|
|
196
|
+
"title": "html_doc",
|
|
197
|
+
"description": "The HTML document for the AI applet",
|
|
198
|
+
"mimeType": "text/html"
|
|
199
199
|
},
|
|
200
200
|
{
|
|
201
|
-
"name": "
|
|
202
|
-
"uri": "
|
|
203
|
-
"title": "
|
|
204
|
-
"description": "
|
|
205
|
-
"mimeType": "text/
|
|
201
|
+
"name": "css",
|
|
202
|
+
"uri": "mcp://resources/css",
|
|
203
|
+
"title": "css",
|
|
204
|
+
"description": "Returns the AI applet's CSS",
|
|
205
|
+
"mimeType": "text/plain"
|
|
206
206
|
}
|
|
207
207
|
]
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
+
The `html_doc` resource uses the custom `name`, `uri`, `description`, and
|
|
213
|
+
`mimeType` from the route configuration shown earlier. The `css` resource sets
|
|
214
|
+
only `"mcp": { "type": "resource" }`, so the defaults apply: the name comes from
|
|
215
|
+
the `operationId`, the URI defaults to `mcp://resources/{name}`, the description
|
|
216
|
+
falls back to the operation's `description`, and the MIME type defaults to
|
|
217
|
+
`text/plain`.
|
|
218
|
+
|
|
212
219
|
### Read a Resource
|
|
213
220
|
|
|
214
|
-
Use the MCP `resources/read` method to read a resource by its URI
|
|
221
|
+
Use the MCP `resources/read` method to read a resource by its URI. Resources
|
|
222
|
+
configured without a custom `uri` use the default `mcp://resources/{name}`
|
|
223
|
+
format:
|
|
215
224
|
|
|
216
225
|
```bash
|
|
217
226
|
curl https://my-gateway.zuplo.dev/mcp \
|
|
@@ -222,7 +231,7 @@ curl https://my-gateway.zuplo.dev/mcp \
|
|
|
222
231
|
"id": "0",
|
|
223
232
|
"method": "resources/read",
|
|
224
233
|
"params": {
|
|
225
|
-
"uri": "mcp://resources/
|
|
234
|
+
"uri": "mcp://resources/css"
|
|
226
235
|
}
|
|
227
236
|
}'
|
|
228
237
|
```
|
|
@@ -236,9 +245,9 @@ Response:
|
|
|
236
245
|
"result": {
|
|
237
246
|
"contents": [
|
|
238
247
|
{
|
|
239
|
-
"uri": "mcp://resources/
|
|
248
|
+
"uri": "mcp://resources/css",
|
|
240
249
|
"mimeType": "text/plain",
|
|
241
|
-
"text": "
|
|
250
|
+
"text": "div { color: blue; }"
|
|
242
251
|
}
|
|
243
252
|
]
|
|
244
253
|
}
|
|
@@ -247,6 +256,9 @@ Response:
|
|
|
247
256
|
|
|
248
257
|
### Read a Resource with Custom URI
|
|
249
258
|
|
|
259
|
+
Resources configured with a custom `uri`, like the `html_doc` resource above,
|
|
260
|
+
are read using that URI:
|
|
261
|
+
|
|
250
262
|
```bash
|
|
251
263
|
curl https://my-gateway.zuplo.dev/mcp \
|
|
252
264
|
-X POST \
|
|
@@ -256,7 +268,7 @@ curl https://my-gateway.zuplo.dev/mcp \
|
|
|
256
268
|
"id": "0",
|
|
257
269
|
"method": "resources/read",
|
|
258
270
|
"params": {
|
|
259
|
-
"uri": "ui://
|
|
271
|
+
"uri": "ui://html"
|
|
260
272
|
}
|
|
261
273
|
}'
|
|
262
274
|
```
|
|
@@ -20,7 +20,7 @@ can generate one of these on the `Shared access tokens` tab.
|
|
|
20
20
|
Note, you should minimize the permissions - and select only the `Create`
|
|
21
21
|
permission. Choose a sensible start and expiration time for your token. Note, we
|
|
22
22
|
don't recommend restricting IP addresses because Zuplo runs at the edge in over
|
|
23
|
-
|
|
23
|
+
300 data centers worldwide.
|
|
24
24
|
|
|
25
25
|

|
|
26
26
|
|
|
@@ -20,7 +20,7 @@ can generate one of these on the `Shared access tokens` tab.
|
|
|
20
20
|
Note, you should minimize the permissions - and select only the `Create`
|
|
21
21
|
permission. Choose a sensible start and expiration time for your token. Note, we
|
|
22
22
|
don't recommend restricting IP addresses because Zuplo runs at the edge in over
|
|
23
|
-
|
|
23
|
+
300 data centers worldwide.
|
|
24
24
|
|
|
25
25
|

|
|
26
26
|
|
|
@@ -29,7 +29,7 @@ export default async function (
|
|
|
29
29
|
// If the blocked IP addresses are set, then the incoming IP
|
|
30
30
|
// can't be in that list
|
|
31
31
|
if (options.blockedIpAddresses) {
|
|
32
|
-
const blocked = ipRangeCheck(ip, options.
|
|
32
|
+
const blocked = ipRangeCheck(ip, options.blockedIpAddresses);
|
|
33
33
|
if (blocked) {
|
|
34
34
|
return HttpProblems.unauthorized(request, context);
|
|
35
35
|
}
|
|
@@ -116,24 +116,6 @@ return HttpProblems.unauthorized(
|
|
|
116
116
|
);
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
## HttpStatusCode Enum
|
|
120
|
-
|
|
121
|
-
The `HttpStatusCode` enum provides numeric constants for all HTTP status codes:
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
import { HttpStatusCode } from "@zuplo/runtime";
|
|
125
|
-
|
|
126
|
-
// Use in responses
|
|
127
|
-
return new Response("Created", {
|
|
128
|
-
status: HttpStatusCode.Created, // 201
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Use in comparisons
|
|
132
|
-
if (response.status === HttpStatusCode.NotFound) {
|
|
133
|
-
// Handle 404
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
119
|
## See Also
|
|
138
120
|
|
|
139
121
|
- [ProblemResponseFormatter](./problem-response-formatter.mdx)
|