zuplo 7.6.0 → 7.6.1

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.
@@ -98,6 +98,13 @@ The MCP Server handler supports the following configuration options:
98
98
  - `operations` - An array of operation references to register with the MCP
99
99
  server. Each operation can be a tool, prompt, or resource based on its
100
100
  `x-zuplo-route.mcp` configuration.
101
+ - `toolInputStyle` (optional, default `flat`) - How generated tools advertise an
102
+ operation's inputs. With `flat`, a request body's properties and the
103
+ operation's path, query, and header parameters all become top-level tool
104
+ arguments. With `nested`, each input channel stays its own object (`body`,
105
+ `queryParams`, `pathParams`, `headers`). Flat tools accept the nested form
106
+ too, so switching styles doesn't break existing clients. See
107
+ [Tool input schemas](../mcp-server/tools.mdx#tool-input-schemas).
101
108
 
102
109
  ### MCP `2025-06-18` Global Options
103
110
 
@@ -140,6 +140,176 @@ array:
140
140
  See further details in the
141
141
  [MCP Server Handler documentation](../handlers/mcp-server.mdx).
142
142
 
143
+ ## Tool input schemas
144
+
145
+ Zuplo derives each tool's `inputSchema` from the operation's OpenAPI definition
146
+ and advertises it as a single flat object. The request body's properties and the
147
+ operation's path, query, and header parameters all become top-level tool
148
+ arguments, which is the shape MCP clients send on a first call.
149
+
150
+ When a client calls the tool, the gateway routes each argument back to the
151
+ channel it came from, so a body field lands in the request body and a query
152
+ parameter lands in the URL.
153
+
154
+ The weather route above declares one required query parameter, so its tool takes
155
+ one required argument:
156
+
157
+ ```json
158
+ {
159
+ "type": "object",
160
+ "properties": {
161
+ "location": {
162
+ "type": "string"
163
+ }
164
+ },
165
+ "required": ["location"],
166
+ "additionalProperties": false
167
+ }
168
+ ```
169
+
170
+ A request body works the same way — its fields are the tool's arguments. Given
171
+ this operation:
172
+
173
+ ```json
174
+ {
175
+ "/orders": {
176
+ "post": {
177
+ "operationId": "createOrder",
178
+ "requestBody": {
179
+ "required": true,
180
+ "content": {
181
+ "application/json": {
182
+ "schema": {
183
+ "type": "object",
184
+ "properties": {
185
+ "productId": { "type": "string" },
186
+ "quantity": { "type": "number" }
187
+ },
188
+ "required": ["productId"],
189
+ "additionalProperties": false
190
+ }
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+ ```
198
+
199
+ The tool advertises the body's fields directly:
200
+
201
+ ```json
202
+ {
203
+ "type": "object",
204
+ "properties": {
205
+ "productId": { "type": "string" },
206
+ "quantity": { "type": "number" }
207
+ },
208
+ "required": ["productId"],
209
+ "additionalProperties": false
210
+ }
211
+ ```
212
+
213
+ And a call passes them at the top level:
214
+
215
+ ```json
216
+ {
217
+ "name": "createOrder",
218
+ "arguments": {
219
+ "productId": "sku-123",
220
+ "quantity": 2
221
+ }
222
+ }
223
+ ```
224
+
225
+ :::note
226
+
227
+ Parameters declared with `in: cookie` aren't exposed as tool arguments.
228
+
229
+ :::
230
+
231
+ ### Undeclared arguments
232
+
233
+ Your own OpenAPI schema decides whether an argument the tool doesn't declare is
234
+ rejected:
235
+
236
+ - **The schema sets `additionalProperties: false`.** The tool's flat schema is
237
+ closed too, and an undeclared argument fails with JSON-RPC error `-32602`.
238
+ - **The schema omits `additionalProperties`.** JSON Schema permits extras, so
239
+ the tool accepts the argument and forwards it in the request body.
240
+
241
+ Set `additionalProperties: false` on a request body schema when you want the
242
+ gateway to reject fields a model invented rather than pass them upstream.
243
+
244
+ An argument routes to a request header only when a header parameter declares
245
+ that exact name. An undeclared argument never becomes an upstream header.
246
+
247
+ ### When arguments stay nested
248
+
249
+ Some operations can't be flattened without changing what the tool accepts. Those
250
+ tools keep the nested shape, and the MCP server logs the reason when it
251
+ registers the tool:
252
+
253
+ - **Two channels declare the same name.** A `limit` query parameter alongside a
254
+ `limit` body field, for example. The whole schema stays nested and the server
255
+ logs a warning. Rename one of them to get flat arguments.
256
+ - **The request body isn't a plain object.** An array, a string, or a body built
257
+ from `oneOf`, `anyOf`, or `allOf` has no property names to hoist. Neither does
258
+ one that constrains the object as a whole, through `minProperties`,
259
+ `patternProperties`, or `dependentRequired`. The parameters still flatten and
260
+ the body keeps its own `body` argument. Enable
261
+ [`debugMode`](../handlers/mcp-server.mdx#configuration) to see which tools
262
+ this applies to.
263
+
264
+ A nested schema wraps each channel the operation declares in its own object:
265
+
266
+ ```json
267
+ {
268
+ "type": "object",
269
+ "properties": {
270
+ "body": {
271
+ "type": "object",
272
+ "properties": { "productId": { "type": "string" } },
273
+ "required": ["productId"],
274
+ "additionalProperties": false
275
+ }
276
+ },
277
+ "required": ["body"],
278
+ "additionalProperties": false
279
+ }
280
+ ```
281
+
282
+ The channel names are `body`, `queryParams`, `pathParams`, and `headers`. Only
283
+ the channels the operation declares appear.
284
+
285
+ ### Calling a tool with nested arguments
286
+
287
+ Flat tools also accept the nested form, so a client written against the nested
288
+ schema keeps working:
289
+
290
+ ```json
291
+ {
292
+ "name": "createOrder",
293
+ "arguments": {
294
+ "body": { "productId": "sku-123", "quantity": 2 }
295
+ }
296
+ }
297
+ ```
298
+
299
+ Send flat arguments in new clients — that's what the tool advertises.
300
+
301
+ To make every generated tool advertise the nested shape instead, set
302
+ [`toolInputStyle: "nested"`](../handlers/mcp-server.mdx#configuration) in the
303
+ handler options.
304
+
305
+ :::note
306
+
307
+ [Prompts](./prompts.mdx) don't use input channels. A prompt's arguments come
308
+ from its request body schema directly, so `prompts/get` always takes them at the
309
+ top level.
310
+
311
+ :::
312
+
143
313
  ## Testing MCP Tools
144
314
 
145
315
  ### List Available Tools
@@ -175,7 +345,8 @@ Response:
175
345
  "type": "string"
176
346
  }
177
347
  },
178
- "required": ["location"]
348
+ "required": ["location"],
349
+ "additionalProperties": false
179
350
  }
180
351
  }
181
352
  ]
@@ -2,9 +2,10 @@
2
2
  title: "Troubleshooting the MCP Server Handler"
3
3
  sidebar_label: "Troubleshooting"
4
4
  description:
5
- Diagnose MCP Server handler tool calls that fail in an AI client even though
6
- the gateway route returns 200, using debug logging and the structured-content
7
- configuration.
5
+ Diagnose MCP Server handler tool calls that fail in an AI client a route
6
+ that returns 200 while the client errors, and invalid-argument rejections —
7
+ using debug logging, the structured-content configuration, and the tool's
8
+ advertised input schema.
8
9
  ---
9
10
 
10
11
  When an AI client calls a tool exposed by the
@@ -14,8 +15,9 @@ underlying gateway route can return `200` while the MCP client still rejects the
14
15
  response, which makes these failures hard to diagnose from the client alone.
15
16
 
16
17
  This page shows how to turn on the handler's debug logging, read the log lines
17
- that reveal a misconfiguration, and fix the most common cause: a tool that
18
- advertises an output schema but returns no structured content.
18
+ that reveal a misconfiguration, and fix the most common causes: a tool that
19
+ advertises an output schema but returns no structured content, and arguments
20
+ that don't match the schema the tool advertises.
19
21
 
20
22
  :::tip
21
23
 
@@ -147,6 +149,46 @@ For the full definitions of both options, see the
147
149
  [MCP `2025-06-18` Global Options](../handlers/mcp-server.mdx#mcp-2025-06-18-global-options)
148
150
  in the handler reference.
149
151
 
152
+ ## Invalid arguments for tool (-32602)
153
+
154
+ **Symptom.** A tool call fails with JSON-RPC error `-32602` and a message naming
155
+ an argument:
156
+
157
+ ```
158
+ -32602 Invalid arguments for tool 'create_order':
159
+ ✖ Unrecognized key: "sku"
160
+ ```
161
+
162
+ **Likely cause 1: the argument isn't in the schema.** The request body schema
163
+ backing the tool sets `additionalProperties: false`, so the tool's input schema
164
+ is closed and an argument it doesn't declare is invalid. Read the tool's
165
+ `inputSchema` from `tools/list` and compare the argument names. A model that
166
+ invents a field name lands here.
167
+
168
+ **Likely cause 2: the tool takes nested arguments.** Most tools take the request
169
+ body's fields and the operation's parameters as top-level arguments. A tool
170
+ whose OpenAPI can't be flattened keeps the nested shape instead, so its
171
+ arguments sit under `body`, `queryParams`, `pathParams`, or `headers`. When the
172
+ error reads `at body`, the caller sent flat arguments to a nested tool:
173
+
174
+ ```
175
+ -32602 Invalid arguments for tool 'create_order':
176
+ ✖ Invalid input: expected object, received undefined
177
+ → at body
178
+ ```
179
+
180
+ Two things cause a tool to stay nested, and the MCP server logs which one when
181
+ it registers the tool: a name collision between two input channels (logged as a
182
+ warning), or a request body that isn't a plain object (logged at debug level —
183
+ enable `debugMode` to see it). See
184
+ [When arguments stay nested](./tools.mdx#when-arguments-stay-nested) for both
185
+ cases and how to resolve the collision.
186
+
187
+ **Fix.** Always craft `arguments` against the `inputSchema` the tool advertises
188
+ in `tools/list` rather than assuming a shape. The
189
+ [MCP Inspector](#client-side-debugging-tools) shows both the schema and the
190
+ error for a call.
191
+
150
192
  ## Client-side debugging tools
151
193
 
152
194
  When the gateway looks healthy, reproduce the failure against the client to see
@@ -2,8 +2,8 @@
2
2
 
3
3
  AI Gateway Metering records application usage and configures the app's own
4
4
  budgets before the provider request runs. It meters spend, tokens, and requests.
5
- Each meter has independent daily and monthly periods and an optional warning
6
- threshold that notifies without blocking.
5
+ Budget rules can cover the whole application or each distinct value of an
6
+ expression.
7
7
 
8
8
  When a limit is exceeded, the policy activates the model selection's
9
9
  `quotaFallback` when AI Gateway Fallback Model supplied one. Otherwise it
@@ -20,31 +20,47 @@ Semantic Cache, after Metering so cache hits still count toward request limits.
20
20
  "name": "ai-gateway-metering-v2-inbound",
21
21
  "options": {
22
22
  "throwOnFailure": false,
23
- "limits": {
24
- "costs": {
25
- "monthly": {
26
- "enabled": true,
27
- "limit": 100,
28
- "warning": {
29
- "enabled": true,
30
- "threshold": 80
23
+ "budgetRules": [
24
+ {
25
+ "budgetBy": "app",
26
+ "meters": [
27
+ {
28
+ "meter": "cost",
29
+ "period": "monthly",
30
+ "value": 100,
31
+ "action": "block"
32
+ },
33
+ {
34
+ "meter": "cost",
35
+ "period": "monthly",
36
+ "value": 80,
37
+ "action": "warn"
31
38
  }
32
- }
39
+ ]
33
40
  },
34
- "requests": {
35
- "daily": {
36
- "enabled": true,
37
- "limit": 1000
38
- }
41
+ {
42
+ "budgetBy": "expression",
43
+ "expression": "request.headers.get(\"x-user\")",
44
+ "meters": [
45
+ {
46
+ "meter": "cost",
47
+ "period": "daily",
48
+ "value": 5,
49
+ "action": "block"
50
+ }
51
+ ]
39
52
  }
40
- }
53
+ ]
41
54
  }
42
55
  }
43
56
  ```
44
57
 
45
- The supported meters are `costs`, `tokens`, and `requests`. Each meter can have
46
- `daily` and `monthly` settings. A period is enforced only when `enabled` is
47
- `true` and `limit` is present. Usage equal to the limit is blocked.
58
+ The supported meters are `cost`, `requests`, and `tokens`. The supported periods
59
+ are `hourly`, `daily`, `weekly`, and `monthly`. An application can have at most
60
+ five rules. An expression rule creates a separate budget for every distinct
61
+ value of its expression. An action of `"warn"` notifies without blocking. An
62
+ action of `"block"` activates the configured quota fallback or returns
63
+ `429 Too Many Requests` when usage reaches the value.
48
64
 
49
65
  > **Budgets fail open by default.** When `throwOnFailure` is `false`, a metering
50
66
  > service failure lets the request proceed unmetered and no limit is checked.
@@ -52,8 +68,8 @@ The supported meters are `costs`, `tokens`, and `requests`. Each meter can have
52
68
 
53
69
  ## Team limits
54
70
 
55
- The `options.limits` settings govern only the app running this policy. Limits
56
- configured on a parent team or the gateway root are enforced centrally after the
71
+ Budgets configured on this application govern only this app. Limits configured
72
+ on a parent team or the gateway root are enforced centrally after the
57
73
  application's policy chain, whether or not this policy appears in that chain. An
58
74
  inherited limit activates the selected model's quota fallback when available and
59
75
  otherwise returns `429 Too Many Requests`.
@@ -62,83 +78,3 @@ An application cannot disable inherited enforcement through its policy chain. If
62
78
  the central hierarchical check is unavailable, the request proceeds. The
63
79
  policy's `throwOnFailure` option controls failures while checking or recording
64
80
  the app's own limits; it does not change inherited-limit behavior.
65
-
66
- ## Set limits from custom code
67
-
68
- Use a custom inbound policy to vary limits for individual requests. For example,
69
- the following policy gives callers with a `pro` plan a higher daily request
70
- limit:
71
-
72
- ```typescript
73
- import {
74
- AIGatewayMeteringV2InboundPolicy,
75
- type ZuploContext,
76
- type ZuploRequest,
77
- } from "@zuplo/runtime";
78
-
79
- export default function applyPlanLimit(
80
- request: ZuploRequest,
81
- context: ZuploContext
82
- ) {
83
- const dailyRequestLimit = request.user?.data.plan === "pro" ? 10_000 : 1_000;
84
-
85
- AIGatewayMeteringV2InboundPolicy.setLimits(context, {
86
- requests: {
87
- daily: {
88
- enabled: true,
89
- limit: dailyRequestLimit,
90
- },
91
- },
92
- });
93
-
94
- return request;
95
- }
96
- ```
97
-
98
- Register the module as a custom policy:
99
-
100
- ```json
101
- {
102
- "name": "plan-based-ai-limits",
103
- "policyType": "custom-code-inbound",
104
- "handler": {
105
- "export": "default",
106
- "module": "$import(./modules/plan-based-ai-limits)"
107
- }
108
- }
109
- ```
110
-
111
- Place it before metering in the application's inbound policy chain:
112
-
113
- ```json
114
- {
115
- "inboundPolicyChain": [
116
- {
117
- "name": "plan-based-ai-limits"
118
- },
119
- {
120
- "name": "ai-gateway-metering-v2-inbound"
121
- }
122
- ]
123
- }
124
- ```
125
-
126
- `setLimits` changes only the fields supplied for the current request. Other
127
- settings from the metering policy remain unchanged. For example, setting only
128
- `requests.daily.limit` preserves `requests.daily.enabled`, its warning settings,
129
- and all monthly limits. It does not update the policy configuration or affect
130
- later requests.
131
-
132
- The exported `AIGatewayMeteringV2LimitOverrides` interface can be used when a
133
- custom policy builds the override object separately. Its shape matches the
134
- `limits` option. These values are called overrides because they temporarily
135
- replace matching policy settings for the current request:
136
-
137
- - Choose a meter: `costs`, `tokens`, or `requests`.
138
- - Choose a period: `daily` or `monthly`.
139
- - Set one or more values: `enabled`, `limit`, or `warning`.
140
-
141
- If model filtering and fallback-model policies selected a `quotaFallback`, an
142
- exceeded limit activates that fallback instead of returning 429. The fallback
143
- request is still metered. Order the chain as model filtering, fallback model,
144
- metering, then policies that may short-circuit such as semantic cache.
@@ -1,7 +1,7 @@
1
1
  The AI Gateway Metering policy records each application's spend, tokens, and
2
- requests and enforces daily or monthly budgets. When a limit is exceeded, it
3
- uses the model selection's quota fallback when one is configured or returns
4
- `429 Too Many Requests`.
2
+ requests and enforces spend, token, and request budgets. When a limit is
3
+ exceeded, it uses the model selection's quota fallback when one is configured or
4
+ returns `429 Too Many Requests`.
5
5
 
6
6
  Place it after Model Filtering and Fallback Model, and before policies such as
7
7
  Semantic Cache that may answer without calling a provider. Metering fails open
@@ -38,14 +38,25 @@
38
38
  "required": [],
39
39
  "examples": [
40
40
  {
41
- "limits": {
42
- "requests": {
43
- "daily": {
44
- "enabled": true,
45
- "limit": 1000
46
- }
41
+ "budgetRules": [
42
+ {
43
+ "budgetBy": "app",
44
+ "meters": [
45
+ {
46
+ "meter": "cost",
47
+ "period": "monthly",
48
+ "value": 100,
49
+ "action": "block"
50
+ },
51
+ {
52
+ "meter": "cost",
53
+ "period": "monthly",
54
+ "value": 80,
55
+ "action": "warn"
56
+ }
57
+ ]
47
58
  }
48
- },
59
+ ],
49
60
  "throwOnFailure": false
50
61
  }
51
62
  ],
@@ -55,8 +66,61 @@
55
66
  "default": false,
56
67
  "description": "Throw when the metering service is unavailable instead of failing open."
57
68
  },
69
+ "budgetRules": {
70
+ "type": "array",
71
+ "maxItems": 5,
72
+ "description": "Budget rules for this application. Each rule budgets the whole app (\"app\") or each distinct value of an expression (\"expression\"), with warn and block thresholds per meter and period.",
73
+ "items": {
74
+ "type": "object",
75
+ "additionalProperties": false,
76
+ "required": ["budgetBy", "meters"],
77
+ "properties": {
78
+ "budgetBy": {
79
+ "type": "string",
80
+ "enum": ["app", "expression"]
81
+ },
82
+ "expression": {
83
+ "type": "string",
84
+ "minLength": 1,
85
+ "description": "Required when budgetBy is \"expression\"; forbidden for \"app\". Each distinct value gets its own budget."
86
+ },
87
+ "meters": {
88
+ "type": "array",
89
+ "minItems": 1,
90
+ "maxItems": 24,
91
+ "items": {
92
+ "type": "object",
93
+ "additionalProperties": false,
94
+ "required": ["meter", "period", "value", "action"],
95
+ "properties": {
96
+ "meter": {
97
+ "type": "string",
98
+ "enum": ["cost", "requests", "tokens"]
99
+ },
100
+ "period": {
101
+ "type": "string",
102
+ "enum": ["hourly", "daily", "weekly", "monthly"]
103
+ },
104
+ "value": {
105
+ "type": "number",
106
+ "minimum": 0
107
+ },
108
+ "action": {
109
+ "type": "string",
110
+ "enum": ["warn", "block"]
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ },
58
118
  "limits": {
59
119
  "type": "object",
120
+ "deprecated": true,
121
+ "doNotSuggest": true,
122
+ "x-show-example": false,
123
+ "x-advanced": true,
60
124
  "description": "Usage limits grouped by meter.",
61
125
  "additionalProperties": false,
62
126
  "properties": {
@@ -271,14 +335,25 @@
271
335
  "export": "AIGatewayMeteringV2InboundPolicy",
272
336
  "module": "$import(@zuplo/runtime)",
273
337
  "options": {
274
- "limits": {
275
- "requests": {
276
- "daily": {
277
- "enabled": true,
278
- "limit": 1000
279
- }
338
+ "budgetRules": [
339
+ {
340
+ "budgetBy": "app",
341
+ "meters": [
342
+ {
343
+ "meter": "cost",
344
+ "period": "monthly",
345
+ "value": 100,
346
+ "action": "block"
347
+ },
348
+ {
349
+ "meter": "cost",
350
+ "period": "monthly",
351
+ "value": 80,
352
+ "action": "warn"
353
+ }
354
+ ]
280
355
  }
281
- },
356
+ ],
282
357
  "throwOnFailure": false
283
358
  }
284
359
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuplo",
3
- "version": "7.6.0",
3
+ "version": "7.6.1",
4
4
  "type": "module",
5
5
  "description": "The official Zuplo CLI for local development and platform management",
6
6
  "homepage": "https://zuplo.com/docs/cli/overview",
@@ -32,9 +32,9 @@
32
32
  "zuplo": "zuplo.js"
33
33
  },
34
34
  "dependencies": {
35
- "@zuplo/cli": "7.6.0",
36
- "@zuplo/core": "7.6.0",
37
- "@zuplo/runtime": "7.6.0",
38
- "@zuplo/test": "7.6.0"
35
+ "@zuplo/cli": "7.6.1",
36
+ "@zuplo/core": "7.6.1",
37
+ "@zuplo/runtime": "7.6.1",
38
+ "@zuplo/test": "7.6.1"
39
39
  }
40
40
  }