@bevel-software/platform-mcp-core 0.25.2 → 0.27.0

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 (56) hide show
  1. package/dist/call-guards.d.ts +47 -0
  2. package/dist/call-guards.d.ts.map +1 -0
  3. package/dist/call-guards.js +215 -0
  4. package/dist/call-guards.js.map +1 -0
  5. package/dist/dispatch.d.ts +5 -1
  6. package/dist/dispatch.d.ts.map +1 -1
  7. package/dist/dispatch.js +19 -2
  8. package/dist/dispatch.js.map +1 -1
  9. package/dist/get-has-no-body.d.ts +49 -0
  10. package/dist/get-has-no-body.d.ts.map +1 -0
  11. package/dist/get-has-no-body.js +104 -0
  12. package/dist/get-has-no-body.js.map +1 -0
  13. package/dist/index.d.ts +7 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +7 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/mcp-app.d.ts +118 -0
  18. package/dist/mcp-app.d.ts.map +1 -0
  19. package/dist/mcp-app.js +126 -0
  20. package/dist/mcp-app.js.map +1 -0
  21. package/dist/meta-tools.d.ts +1 -1
  22. package/dist/meta-tools.d.ts.map +1 -1
  23. package/dist/meta-tools.js +28 -6
  24. package/dist/meta-tools.js.map +1 -1
  25. package/dist/proxied-tool.d.ts +33 -3
  26. package/dist/proxied-tool.d.ts.map +1 -1
  27. package/dist/proxied-tool.js +234 -21
  28. package/dist/proxied-tool.js.map +1 -1
  29. package/dist/results.d.ts +20 -1
  30. package/dist/results.d.ts.map +1 -1
  31. package/dist/results.js +117 -3
  32. package/dist/results.js.map +1 -1
  33. package/dist/schema-validity.d.ts +50 -0
  34. package/dist/schema-validity.d.ts.map +1 -0
  35. package/dist/schema-validity.js +254 -0
  36. package/dist/schema-validity.js.map +1 -0
  37. package/dist/tool-interface.d.ts +137 -0
  38. package/dist/tool-interface.d.ts.map +1 -0
  39. package/dist/tool-interface.js +640 -0
  40. package/dist/tool-interface.js.map +1 -0
  41. package/dist/utcp-namespace.d.ts +14 -0
  42. package/dist/utcp-namespace.d.ts.map +1 -1
  43. package/dist/utcp-namespace.js +7 -2
  44. package/dist/utcp-namespace.js.map +1 -1
  45. package/package.json +2 -1
  46. package/src/call-guards.ts +237 -0
  47. package/src/dispatch.ts +18 -1
  48. package/src/get-has-no-body.ts +110 -0
  49. package/src/index.ts +45 -0
  50. package/src/mcp-app.ts +194 -0
  51. package/src/meta-tools.ts +31 -6
  52. package/src/proxied-tool.ts +237 -19
  53. package/src/results.ts +129 -3
  54. package/src/schema-validity.ts +264 -0
  55. package/src/tool-interface.ts +673 -0
  56. package/src/utcp-namespace.ts +7 -2
@@ -0,0 +1,640 @@
1
+ import { utcpNameToTsInterfaceName } from './code-mode-names.js';
2
+ /**
3
+ * A tool's INTERFACE, in the two forms an agent needs it: the one-line call
4
+ * example that opens every description, and the argument list a refusal shows
5
+ * when a call did not match.
6
+ *
7
+ * Both are derived from the tool's own input schema and nothing else. A
8
+ * hand-written example drifts from the schema the moment either changes, and a
9
+ * connected server's tools would have none at all — so there is one generator
10
+ * here, used by the platform's own tools, by a deployment's, and by every
11
+ * connected server's alike.
12
+ *
13
+ * Pure: no IO, no clock, no client. The surfaces that list tools call
14
+ * {@link withCallExample}; the argument check beside it (`call-guards.ts`)
15
+ * calls {@link argumentsDoNotMatchMessage}.
16
+ */
17
+ /** What every tool description starts with, on a line of its own. */
18
+ export const CALL_LINE_PREFIX = 'Call: ';
19
+ /** The `kind` an arguments-do-not-match refusal carries, for a client that branches on it. */
20
+ export const ARGUMENTS_DO_NOT_MATCH_KIND = 'arguments-do-not-match';
21
+ /**
22
+ * The sentence that leads the mismatches when every argument was wrapped in a
23
+ * `body` the tool does not have — the single most common wrong call, because
24
+ * the platform's own tools DO take their arguments that way and a connector
25
+ * tool's arguments are flat.
26
+ */
27
+ export const BODY_AT_TOP_LEVEL_LINE = 'This tool takes its arguments at the top level, not under "body".';
28
+ /**
29
+ * The mirror of {@link BODY_AT_TOP_LEVEL_LINE}: the same mistake the other way
30
+ * round, by an agent that learned the flat shape and used it on a tool whose
31
+ * arguments ride a `body` envelope. The platform's route-hosted tools take that
32
+ * envelope, and the arguments of a flat call reach their route as query
33
+ * parameters instead of a body — which is how the route tells this apart from a
34
+ * call that simply left everything out, and can say which it was.
35
+ */
36
+ export const ARGS_UNDER_BODY_LINE = 'This tool takes its arguments under "body", not at the top level.';
37
+ /**
38
+ * How deep the interface and the check go: the top level and one level below
39
+ * it. That is exactly far enough for the `{ body: { ... } }` envelope every
40
+ * platform tool wears, and keeps a refusal readable for a connected server's
41
+ * deeply nested schema instead of printing its whole tree.
42
+ */
43
+ const MAX_DEPTH = 2;
44
+ /**
45
+ * How many placeholders an array example holds at most. `minItems` comes from
46
+ * a schema someone else wrote; one declaring a million would otherwise have
47
+ * the listing allocate a million placeholders for one line of description.
48
+ * An example cut short is still a call that shows the shape, which is its job.
49
+ */
50
+ const EXAMPLE_ITEMS_MAX = 8;
51
+ /** An argument description is cut to this, so one verbose argument can't crowd out the rest. */
52
+ const DESCRIPTION_MAX = 160;
53
+ function isDict(value) {
54
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
55
+ }
56
+ /** The JSON-Schema keywords this module cannot reason about; their presence switches checking off. */
57
+ const UNSUPPORTED_KEYWORDS = ['anyOf', 'oneOf', 'allOf', 'not', '$ref', 'if', 'then', 'else'];
58
+ function unsupportedKeyword(schema) {
59
+ return UNSUPPORTED_KEYWORDS.find((k) => schema[k] !== undefined);
60
+ }
61
+ /** The declared types of a (sub)schema as a list, or undefined when it declares none we know. */
62
+ function declaredTypes(schema) {
63
+ const raw = schema.type;
64
+ const list = typeof raw === 'string' ? [raw] : Array.isArray(raw) ? raw.filter((t) => typeof t === 'string') : [];
65
+ const known = list.filter((t) => ['string', 'number', 'integer', 'boolean', 'array', 'object', 'null'].includes(t));
66
+ return known.length > 0 ? known : undefined;
67
+ }
68
+ /** The placeholder value a type takes in the call example. */
69
+ function placeholder(schema, depth) {
70
+ // A value the schema fixes is shown as that value: `"..."` would be a call
71
+ // the check refuses.
72
+ if (schema.const !== undefined)
73
+ return schema.const;
74
+ // The first member the rest of the schema admits, so a sibling constraint
75
+ // (`minLength`, a range) cannot make the example one the check refuses.
76
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
77
+ return schema.enum.find((option) => valueMismatch('', schema, option) === null) ?? schema.enum[0];
78
+ }
79
+ const type = declaredTypes(schema)?.[0];
80
+ if (type === 'number' || type === 'integer')
81
+ return numberPlaceholder(schema, type === 'integer');
82
+ if (type === 'boolean')
83
+ return true;
84
+ if (type === 'array') {
85
+ // An array that must not be empty is shown with one element, so the
86
+ // example satisfies its own schema (`tools_info` takes `tool_names`
87
+ // with at least one name). Otherwise empty, the shortest array that works.
88
+ const minItems = typeof schema.minItems === 'number' ? schema.minItems : 0;
89
+ if (minItems < 1)
90
+ return [];
91
+ const items = isDict(schema.items) ? schema.items : {};
92
+ return Array.from({ length: Math.min(minItems, EXAMPLE_ITEMS_MAX) }, () => placeholder(items, depth));
93
+ }
94
+ if (type === 'object') {
95
+ // An object whose own required arguments are known is shown with them, so
96
+ // the `{ body: { branch, path } }` envelope is spelled out rather than
97
+ // handed over as an empty `{}` the agent has to guess the inside of. The
98
+ // check stops at the same depth (see `compileCheck`), so an example cut
99
+ // off here is still one the check accepts.
100
+ return depth < MAX_DEPTH ? exampleArguments(schema, depth + 1) : {};
101
+ }
102
+ // A value that must be null is shown as `null`: `"..."` would be a call the
103
+ // check refuses.
104
+ if (type === 'null')
105
+ return null;
106
+ // A string long enough for its `minLength`: `"..."` fails a schema that
107
+ // wants more. (A `pattern` is not synthesised — a placeholder cannot be
108
+ // made to match an arbitrary expression — so the example stays `"..."`.)
109
+ if (type === 'string')
110
+ return stringPlaceholder(schema);
111
+ // No declared type is shown as a string placeholder: the overwhelming
112
+ // majority of such arguments are strings, and a `"..."` reads as "put a
113
+ // value here" in a way `null` does not.
114
+ return '...';
115
+ }
116
+ /** `"..."`, padded with dots to the schema's `minLength` (capped: a bound is a bound, not a size). */
117
+ function stringPlaceholder(schema) {
118
+ const minLength = typeof schema.minLength === 'number' && Number.isFinite(schema.minLength) ? schema.minLength : 0;
119
+ return '.'.repeat(Math.min(64, Math.max(3, Math.ceil(minLength))));
120
+ }
121
+ /**
122
+ * A number the schema's range admits: `0`, unless a bound rules it out —
123
+ * then the nearest value at the lower bound, the upper bound, or between the
124
+ * two, whichever satisfies EVERY bound (`exclusiveMinimum: 1, maximum: 1.5`
125
+ * gives `1.5` and `exclusiveMaximum: 1.5` in its place
126
+ * gives `1.25`, not a `2` the check would refuse). A range nothing satisfies
127
+ * keeps `0`: the schema is at fault, and no example can fix it.
128
+ */
129
+ function numberPlaceholder(schema, integer) {
130
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);
131
+ const minimum = num(schema.minimum);
132
+ const maximum = num(schema.maximum);
133
+ const exclusiveMinimum = num(schema.exclusiveMinimum);
134
+ const exclusiveMaximum = num(schema.exclusiveMaximum);
135
+ const lows = [minimum, exclusiveMinimum].filter((v) => v !== undefined);
136
+ const highs = [maximum, exclusiveMaximum].filter((v) => v !== undefined);
137
+ const low = lows.length > 0 ? Math.max(...lows) : undefined;
138
+ const high = highs.length > 0 ? Math.min(...highs) : undefined;
139
+ const candidates = [
140
+ 0,
141
+ minimum,
142
+ exclusiveMinimum === undefined ? undefined : exclusiveMinimum + 1,
143
+ maximum,
144
+ exclusiveMaximum === undefined ? undefined : exclusiveMaximum - 1,
145
+ low !== undefined && high !== undefined ? (low + high) / 2 : undefined,
146
+ ].flatMap((c) => (c === undefined ? [] : integer ? [Math.ceil(c), Math.floor(c)] : [c]));
147
+ return candidates.find((c) => numericBoundBroken(schema, c) === null) ?? 0;
148
+ }
149
+ /**
150
+ * The arguments the call example passes: one placeholder per REQUIRED
151
+ * argument, in the order the schema requires them, and nothing else.
152
+ *
153
+ * This is the example as a VALUE, which is what makes the example testable —
154
+ * every tool the platform declares is checked with it against its own schema,
155
+ * so an example an agent copies is one the check accepts.
156
+ */
157
+ export function exampleArguments(inputs, depth = 1) {
158
+ const schema = isDict(inputs) ? inputs : {};
159
+ const properties = isDict(schema.properties) ? schema.properties : {};
160
+ const required = Array.isArray(schema.required) ? schema.required.filter((r) => typeof r === 'string') : [];
161
+ const args = {};
162
+ for (const name of required) {
163
+ const prop = properties[name];
164
+ // An own property by definition: a bare `args.__proto__ = …` would set
165
+ // the prototype and lose the argument.
166
+ Object.defineProperty(args, name, {
167
+ value: placeholder(isDict(prop) ? prop : {}, depth),
168
+ enumerable: true,
169
+ writable: true,
170
+ configurable: true,
171
+ });
172
+ }
173
+ return args;
174
+ }
175
+ /**
176
+ * A key as JavaScript source: bare when it is an identifier, quoted (and
177
+ * escaped) when it is not, e.g. `"odd-key"`. `__proto__` is computed
178
+ * (`["__proto__"]`): bare or quoted in an object literal it sets the prototype.
179
+ */
180
+ function renderKey(key) {
181
+ if (key === '__proto__')
182
+ return '["__proto__"]';
183
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
184
+ }
185
+ /**
186
+ * The placeholder tree as source an agent can paste: object keys bare, strings
187
+ * double-quoted, one space inside the braces. Every value here was produced by
188
+ * {@link placeholder}; strings and keys that are not identifiers are quoted as
189
+ * JSON, so a value taken from an `enum` is escaped like any other.
190
+ */
191
+ function render(value) {
192
+ if (typeof value === 'string')
193
+ return JSON.stringify(value);
194
+ if (Array.isArray(value))
195
+ return value.length === 0 ? '[]' : `[${value.map(render).join(', ')}]`;
196
+ if (isDict(value)) {
197
+ const entries = Object.entries(value).map(([k, v]) => `${renderKey(k)}: ${render(v)}`);
198
+ return entries.length === 0 ? '{}' : `{ ${entries.join(', ')} }`;
199
+ }
200
+ return String(value);
201
+ }
202
+ /**
203
+ * The call example for one tool: the namespace this connection exposes, the
204
+ * tool's name, and its required arguments with a placeholder each, in the shape
205
+ * the tool really takes.
206
+ *
207
+ * `utcpName` is the tool's registered UTCP name (`<manual>.<tool>`); the
208
+ * namespace and name come from the same mapping the chain runtime uses, so the
209
+ * example is literally callable inside `call_tool_chain`. Optional arguments
210
+ * are left out — the example is the shortest call that can work, not a catalog.
211
+ */
212
+ export function callExample(utcpName, inputs) {
213
+ return `${utcpNameToTsInterfaceName(utcpName)}(${render(exampleArguments(inputs))})`;
214
+ }
215
+ /** The `Call:` line as it appears at the top of a description. */
216
+ export function callLine(utcpName, inputs) {
217
+ return `${CALL_LINE_PREFIX}${callExample(utcpName, inputs)}`;
218
+ }
219
+ /**
220
+ * A description with its call example ahead of it. Idempotent: a description
221
+ * that already opens with a `Call:` line keeps the one it has, so a surface
222
+ * that lists the same tool through two layers cannot stack two examples.
223
+ */
224
+ export function withCallExample(description, utcpName, inputs) {
225
+ const body = description ?? '';
226
+ if (body.startsWith(CALL_LINE_PREFIX))
227
+ return body;
228
+ return body === '' ? callLine(utcpName, inputs) : `${callLine(utcpName, inputs)}\n\n${body}`;
229
+ }
230
+ /**
231
+ * Split a description into its `Call:` line and the rest, so a caller that
232
+ * prepends text of its own (the knowledge-base tools' purpose prefix) can keep
233
+ * the example first — the line only does its job if it is the first thing read.
234
+ */
235
+ export function splitCallLine(description) {
236
+ if (!description.startsWith(CALL_LINE_PREFIX))
237
+ return { call: null, rest: description };
238
+ const end = description.indexOf('\n');
239
+ if (end < 0)
240
+ return { call: description, rest: '' };
241
+ return { call: description.slice(0, end), rest: description.slice(end + 1).replace(/^\n+/, '') };
242
+ }
243
+ function shortDescription(schema) {
244
+ const raw = typeof schema.description === 'string' ? schema.description.replace(/\s+/g, ' ').trim() : '';
245
+ if (raw.length <= DESCRIPTION_MAX)
246
+ return raw;
247
+ return `${raw.slice(0, DESCRIPTION_MAX - 1)}…`;
248
+ }
249
+ function typeLabel(schema) {
250
+ return declaredTypes(schema)?.join(' or ') ?? 'any';
251
+ }
252
+ /**
253
+ * The tool's interface as lines: every argument with its type, whether it is
254
+ * required, and its description — the top level, then one level below it,
255
+ * indented. This is what an agent needs in order to correct its call, and it
256
+ * is the schema's own content, never prose about it.
257
+ */
258
+ export function describeInterface(inputs, depth = 1, indent = '') {
259
+ const schema = isDict(inputs) ? inputs : {};
260
+ const properties = isDict(schema.properties) ? schema.properties : {};
261
+ const required = new Set((Array.isArray(schema.required) ? schema.required : []).filter((r) => typeof r === 'string'));
262
+ const lines = [];
263
+ for (const [name, raw] of Object.entries(properties)) {
264
+ const prop = isDict(raw) ? raw : {};
265
+ const description = shortDescription(prop);
266
+ lines.push(`${indent}${name} (${typeLabel(prop)}, ${required.has(name) ? 'required' : 'optional'})` +
267
+ (description === '' ? '' : ` — ${description}`));
268
+ if (depth < MAX_DEPTH && declaredTypes(prop)?.includes('object') && isDict(prop.properties)) {
269
+ lines.push(...describeInterface(prop, depth + 1, `${indent} `));
270
+ }
271
+ }
272
+ return lines;
273
+ }
274
+ /**
275
+ * The whole refusal: one sentence that the arguments do not match, the
276
+ * mismatches one per line, the interface, and the call example last — the order
277
+ * an agent reads it in, ending with the line it can copy.
278
+ */
279
+ export function argumentsDoNotMatchMessage(toolName, utcpName, inputs, mismatches, options = {}) {
280
+ const interfaceLines = describeInterface(inputs);
281
+ return [
282
+ `The arguments do not match the "${toolName}" tool.`,
283
+ ...mismatches,
284
+ `Interface of "${toolName}":`,
285
+ ...(interfaceLines.length > 0 ? interfaceLines : ['(this tool takes no arguments)']),
286
+ callLine(utcpName, options.exampleInputs ?? inputs),
287
+ ].join('\n');
288
+ }
289
+ /** A value's JSON type, as the schema's vocabulary names it. */
290
+ function jsonTypeOf(value) {
291
+ if (value === null)
292
+ return 'null';
293
+ if (Array.isArray(value))
294
+ return 'array';
295
+ if (typeof value === 'number')
296
+ return Number.isInteger(value) ? 'integer' : 'number';
297
+ if (typeof value === 'object')
298
+ return 'object';
299
+ return typeof value;
300
+ }
301
+ function typeMatches(types, value) {
302
+ const actual = jsonTypeOf(value);
303
+ // An integer satisfies `number`, and any number satisfies `integer` only
304
+ // when it has no fractional part — which `jsonTypeOf` has already decided.
305
+ return types.some((t) => t === actual || (t === 'number' && actual === 'integer'));
306
+ }
307
+ /**
308
+ * Compile one input schema into a check, or decide it cannot be checked.
309
+ *
310
+ * Deliberately conservative: anything this module cannot reason about with
311
+ * certainty — a combinator, a `$ref`, a schema that is not an object — switches
312
+ * the check OFF rather than guessing. A checker that refuses valid calls would
313
+ * take tools away from every agent at once, so every rule here is one a call
314
+ * cannot satisfy by any reading of the schema.
315
+ */
316
+ export function compileCheck(inputs, depth = 1) {
317
+ if (!isDict(inputs))
318
+ return { checkable: false, reason: 'the input schema is not an object' };
319
+ const unsupported = unsupportedKeyword(inputs);
320
+ if (unsupported)
321
+ return { checkable: false, reason: `the input schema uses "${unsupported}"` };
322
+ // A boolean subschema (`false`: nothing is valid; `true`: anything is) is a
323
+ // rule this module does not apply — read as `{}` it would pass what the
324
+ // schema forbids — so it switches the check off like a combinator does.
325
+ if (hasBooleanSubschema(inputs))
326
+ return { checkable: false, reason: 'the input schema uses a boolean subschema' };
327
+ const types = declaredTypes(inputs);
328
+ if (types && !types.includes('object')) {
329
+ return { checkable: false, reason: `the input schema declares type "${types.join(' or ')}", not an object` };
330
+ }
331
+ if (inputs.properties !== undefined && !isDict(inputs.properties)) {
332
+ return { checkable: false, reason: 'the input schema\'s "properties" is not an object' };
333
+ }
334
+ const properties = isDict(inputs.properties) ? inputs.properties : {};
335
+ const required = (Array.isArray(inputs.required) ? inputs.required : []).filter((r) => typeof r === 'string');
336
+ const closed = inputs.additionalProperties === false;
337
+ const hasBodyProperty = Object.prototype.hasOwnProperty.call(properties, 'body');
338
+ // Sub-checks for the one level below the top: compiled here, with the rest,
339
+ // so a call pays nothing for them. No deeper than the call example goes
340
+ // (`MAX_DEPTH`): an example cut off at `{}` must be a call the check accepts.
341
+ const nested = new Map();
342
+ if (depth < MAX_DEPTH) {
343
+ for (const [name, raw] of Object.entries(properties)) {
344
+ if (!isDict(raw) || !declaredTypes(raw)?.includes('object') || !isDict(raw.properties))
345
+ continue;
346
+ const inner = compileCheck(raw, depth + 1);
347
+ if (inner.checkable)
348
+ nested.set(name, inner);
349
+ }
350
+ }
351
+ const check = (args) => {
352
+ const mismatches = [];
353
+ // The `body` wrapper first: when every argument sits under a `body` key the
354
+ // tool does not have, THAT is what went wrong, and the lines below (a
355
+ // missing required argument, an argument the tool lacks) are its symptoms.
356
+ // Only when the wrapper IS wrong: an open schema with nothing required
357
+ // accepts `{ body: {} }` as it accepts any extra key.
358
+ const keys = Object.keys(args);
359
+ if (!hasBodyProperty &&
360
+ (closed || required.length > 0) &&
361
+ keys.length === 1 &&
362
+ keys[0] === 'body' &&
363
+ isDict(args.body)) {
364
+ mismatches.push(BODY_AT_TOP_LEVEL_LINE);
365
+ }
366
+ // Own properties only, here and below: `constructor` or `toString` read
367
+ // through the prototype would otherwise count as given — or be checked
368
+ // as a value the caller never sent.
369
+ const given = (name) => Object.prototype.hasOwnProperty.call(args, name);
370
+ for (const name of required) {
371
+ if (!given(name))
372
+ mismatches.push(`"${name}" is required, and was not given.`);
373
+ }
374
+ if (closed) {
375
+ for (const key of keys) {
376
+ if (!Object.prototype.hasOwnProperty.call(properties, key)) {
377
+ mismatches.push(`"${key}" is not an argument of this tool.`);
378
+ }
379
+ }
380
+ }
381
+ for (const [name, raw] of Object.entries(properties)) {
382
+ if (!given(name))
383
+ continue;
384
+ const value = args[name];
385
+ if (value === undefined)
386
+ continue;
387
+ const prop = isDict(raw) ? raw : {};
388
+ const wrong = valueMismatch(name, prop, value);
389
+ if (wrong) {
390
+ mismatches.push(wrong);
391
+ continue; // a wrong value cannot also be walked for its own arguments
392
+ }
393
+ const inner = nested.get(name);
394
+ if (inner?.checkable && isDict(value)) {
395
+ mismatches.push(...inner.check(value).map((m) => qualify(name, m)));
396
+ }
397
+ }
398
+ return mismatches;
399
+ };
400
+ return { checkable: true, check };
401
+ }
402
+ /**
403
+ * What is wrong with one argument's VALUE, or `null`: its type first, then
404
+ * the constraints the schema puts on a value of that type — `enum`/`const`,
405
+ * a string's length and `pattern`, a number's range, an array's length and
406
+ * each of its `items`. A call that breaks a declared constraint does not match
407
+ * the schema any more than one of the wrong type does, and is refused the same
408
+ * way rather than reaching the tool.
409
+ *
410
+ * `format` is an annotation (JSON Schema does not require it to be asserted)
411
+ * and is not checked; neither is a keyword this module does not know. A
412
+ * subschema using a combinator is not ours to judge and passes as it is.
413
+ */
414
+ function valueMismatch(name, schema, value) {
415
+ if (unsupportedKeyword(schema))
416
+ return null;
417
+ const expected = declaredTypes(schema);
418
+ if (expected && !typeMatches(expected, value)) {
419
+ return `"${name}" must be ${expected.join(' or ')}, but ${jsonTypeOf(value)} was given.`;
420
+ }
421
+ if (Array.isArray(schema.enum) && !schema.enum.some((option) => sameJson(option, value))) {
422
+ return `"${name}" must be one of ${schema.enum.map((o) => JSON.stringify(o)).join(', ')}, but ${shortJson(value)} was given.`;
423
+ }
424
+ if (schema.const !== undefined && !sameJson(schema.const, value)) {
425
+ return `"${name}" must be ${JSON.stringify(schema.const)}, but ${shortJson(value)} was given.`;
426
+ }
427
+ if (typeof value === 'string') {
428
+ // Length in code points, as JSON Schema counts it, not UTF-16 units.
429
+ const length = [...value].length;
430
+ if (typeof schema.minLength === 'number' && length < schema.minLength) {
431
+ return `"${name}" must be at least ${schema.minLength} character(s) long, but ${length} was given.`;
432
+ }
433
+ if (typeof schema.maxLength === 'number' && length > schema.maxLength) {
434
+ return `"${name}" must be at most ${schema.maxLength} character(s) long, but ${length} was given.`;
435
+ }
436
+ const pattern = typeof schema.pattern === 'string' ? compiledPattern(schema.pattern) : null;
437
+ if (pattern && !pattern.test(value)) {
438
+ return `"${name}" must match the pattern ${schema.pattern}, but ${shortJson(value)} was given.`;
439
+ }
440
+ }
441
+ if (typeof value === 'number') {
442
+ const bound = numericBoundBroken(schema, value);
443
+ if (bound)
444
+ return `"${name}" must be ${bound}, but ${value} was given.`;
445
+ }
446
+ if (Array.isArray(value)) {
447
+ if (typeof schema.minItems === 'number' && value.length < schema.minItems) {
448
+ return `"${name}" must hold at least ${schema.minItems} item(s), but ${value.length} was given.`;
449
+ }
450
+ if (typeof schema.maxItems === 'number' && value.length > schema.maxItems) {
451
+ return `"${name}" must hold at most ${schema.maxItems} item(s), but ${value.length} was given.`;
452
+ }
453
+ if (isDict(schema.items)) {
454
+ for (let i = 0; i < value.length; i++) {
455
+ const wrong = valueMismatch(`${name}[${i}]`, schema.items, value[i]);
456
+ if (wrong)
457
+ return wrong; // the first bad item is enough to correct the call
458
+ }
459
+ }
460
+ }
461
+ return null;
462
+ }
463
+ /**
464
+ * Whether a property or item schema anywhere the check would look (`MAX_DEPTH`
465
+ * levels, items included) is a boolean rather than an object.
466
+ */
467
+ function hasBooleanSubschema(schema, depth = 1) {
468
+ // `additionalProperties: false` is the ordinary way to close an object and
469
+ // is applied as such; a boolean PROPERTY or ITEM schema is the case here.
470
+ const subschemas = isDict(schema.properties) ? Object.values(schema.properties) : [];
471
+ if (schema.items !== undefined)
472
+ subschemas.push(schema.items);
473
+ for (const sub of subschemas) {
474
+ if (typeof sub === 'boolean')
475
+ return true;
476
+ if (isDict(sub) && depth < MAX_DEPTH && hasBooleanSubschema(sub, depth + 1))
477
+ return true;
478
+ }
479
+ return false;
480
+ }
481
+ /** The numeric bound `value` breaks, phrased for a refusal, or `null`. */
482
+ function numericBoundBroken(schema, value) {
483
+ const { minimum, maximum, exclusiveMinimum, exclusiveMaximum } = schema;
484
+ if (typeof minimum === 'number' && value < minimum)
485
+ return `at least ${minimum}`;
486
+ if (typeof maximum === 'number' && value > maximum)
487
+ return `at most ${maximum}`;
488
+ if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum)
489
+ return `greater than ${exclusiveMinimum}`;
490
+ if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum)
491
+ return `less than ${exclusiveMaximum}`;
492
+ return null;
493
+ }
494
+ /**
495
+ * Equality as JSON sees it, for `enum` and `const`: arrays in order, objects
496
+ * by their properties whatever order their keys were written in.
497
+ */
498
+ function sameJson(a, b) {
499
+ if (a === b)
500
+ return true;
501
+ if (Array.isArray(a) || Array.isArray(b)) {
502
+ return (Array.isArray(a) &&
503
+ Array.isArray(b) &&
504
+ a.length === b.length &&
505
+ a.every((value, index) => sameJson(value, b[index])));
506
+ }
507
+ if (!isDict(a) || !isDict(b))
508
+ return false;
509
+ const keys = Object.keys(a);
510
+ return (keys.length === Object.keys(b).length &&
511
+ keys.every((key) => Object.prototype.hasOwnProperty.call(b, key) && sameJson(a[key], b[key])));
512
+ }
513
+ /** A value as it appears in a refusal: JSON, cut short so one long argument cannot fill the message. */
514
+ function shortJson(value) {
515
+ const text = JSON.stringify(value) ?? String(value);
516
+ return text.length <= 60 ? text : `${text.slice(0, 59)}…`;
517
+ }
518
+ /**
519
+ * Could this pattern backtrack catastrophically? True for a group that repeats
520
+ * (`*`, `+`, `{…}`) and itself contains a quantifier or an
521
+ * alternation — `(a+)+`, `(a|a)*`, `(\w+\s?)*` — and for a backreference.
522
+ *
523
+ * JavaScript's matcher backtracks, so such a pattern, which comes from a
524
+ * schema someone else wrote, could hold the event loop for seconds on one
525
+ * caller's string. The test is a conservative over-approximation (it flags
526
+ * some patterns that would in fact be fast); a flagged pattern is simply not
527
+ * asserted, which is what this module does with any keyword it cannot judge
528
+ * safely.
529
+ */
530
+ export function patternMayBacktrack(source) {
531
+ if (/\\[1-9]|\\k</.test(source))
532
+ return true;
533
+ // One frame per open group: did anything inside it repeat or branch?
534
+ const stack = [];
535
+ let risky = false; // what the group that just closed held
536
+ let previousClosedGroup = false;
537
+ for (let i = 0; i < source.length; i++) {
538
+ const ch = source[i];
539
+ const afterGroup = previousClosedGroup;
540
+ previousClosedGroup = false;
541
+ if (ch === '\\') {
542
+ i++;
543
+ continue;
544
+ }
545
+ if (ch === '[') {
546
+ // A character class is one atom: skip to its closing bracket.
547
+ for (i++; i < source.length && source[i] !== ']'; i++)
548
+ if (source[i] === '\\')
549
+ i++;
550
+ continue;
551
+ }
552
+ if (ch === '(') {
553
+ stack.push(false);
554
+ if (source[i + 1] === '?')
555
+ i++; // `(?:`, `(?=`, `(?<name>`: the `?` is syntax, not a quantifier
556
+ continue;
557
+ }
558
+ if (ch === ')') {
559
+ risky = stack.pop() ?? false;
560
+ // What a group holds, the group around it holds too.
561
+ if (risky && stack.length > 0)
562
+ stack[stack.length - 1] = true;
563
+ previousClosedGroup = true;
564
+ continue;
565
+ }
566
+ const quantifier = ch === '*' || ch === '+' || ch === '?' || ch === '{';
567
+ if (quantifier || ch === '|') {
568
+ if (stack.length > 0)
569
+ stack[stack.length - 1] = true;
570
+ // `?` repeats nothing; any `{…}` count is treated as a repeat, a long
571
+ // fixed count compounding the backtracking just as `+` does.
572
+ const repeats = ch === '*' || ch === '+' || ch === '{';
573
+ if (afterGroup && repeats && risky)
574
+ return true;
575
+ }
576
+ if (ch === '{') {
577
+ const close = source.indexOf('}', i);
578
+ if (close > i)
579
+ i = close;
580
+ }
581
+ }
582
+ return false;
583
+ }
584
+ /**
585
+ * A `pattern` compiled once. One this runtime cannot compile, or one that may
586
+ * backtrack catastrophically (see {@link patternMayBacktrack}), is not checked
587
+ * rather than refused.
588
+ */
589
+ const patterns = new Map();
590
+ /** How many compiled patterns are kept: past this the cache starts over, so a process listing ever-new schemas does not grow without bound. */
591
+ const PATTERN_CACHE_MAX = 512;
592
+ function compiledPattern(source) {
593
+ if (!patterns.has(source)) {
594
+ if (patterns.size >= PATTERN_CACHE_MAX)
595
+ patterns.clear();
596
+ let re = null;
597
+ if (patternMayBacktrack(source)) {
598
+ patterns.set(source, null);
599
+ return null;
600
+ }
601
+ try {
602
+ re = new RegExp(source, 'u');
603
+ }
604
+ catch {
605
+ try {
606
+ re = new RegExp(source);
607
+ }
608
+ catch {
609
+ re = null;
610
+ }
611
+ }
612
+ patterns.set(source, re);
613
+ }
614
+ return patterns.get(source) ?? null;
615
+ }
616
+ /** A nested mismatch, named by its path (`"body.path" is required…`). */
617
+ function qualify(parent, mismatch) {
618
+ return mismatch.replace(/^"([^"]+)"/, (_m, name) => `"${parent}.${name}"`);
619
+ }
620
+ /**
621
+ * One compiled check per distinct input schema, kept for as long as the schema
622
+ * object lives. Both surfaces that check a call hand in the SAME schema object
623
+ * every time — the tool repository hands out tools whose `inputs` is the stored
624
+ * object, and a route's schema is the one its `toolDef` was given — so the
625
+ * check is compiled once per tool rather than once per call, and a call pays
626
+ * for a walk of its arguments and nothing else.
627
+ */
628
+ const compiled = new WeakMap();
629
+ /** The compiled check for one input schema, compiled at most once per schema. */
630
+ export function checkFor(inputs) {
631
+ if (typeof inputs !== 'object' || inputs === null)
632
+ return compileCheck(inputs);
633
+ const hit = compiled.get(inputs);
634
+ if (hit)
635
+ return hit;
636
+ const built = compileCheck(inputs);
637
+ compiled.set(inputs, built);
638
+ return built;
639
+ }
640
+ //# sourceMappingURL=tool-interface.js.map