stitchkit 0.55.0 → 0.56.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.
- package/dist/browser/socket-io.d.ts +14 -6
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/contract/errors.d.ts +7 -0
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/files/boundary.d.ts +8 -5
- package/dist/files/boundary.d.ts.map +1 -1
- package/dist/files.js +1 -1
- package/dist/{index-rgd18hx3.js → index-28cqssm3.js} +7 -0
- package/dist/index-557by2db.js +34 -0
- package/dist/index-5r13htq1.js +283 -0
- package/dist/index-6y759j86.js +1031 -0
- package/dist/{index-1k16zv57.js → index-bfcpjw20.js} +125 -31
- package/dist/{index-0gv7k0ra.js → index-escqg10p.js} +73 -666
- package/dist/{index-v4bbq3p4.js → index-j3dem06f.js} +1 -1
- package/dist/{index-qx84bqzk.js → index-jcc611vh.js} +1 -1
- package/dist/{index-npye5kt7.js → index-n4nfa7gh.js} +3 -3
- package/dist/{index-pzc32v75.js → index-sm2tjx06.js} +41 -4
- package/dist/{index-js7yexng.js → index-xy8fmh6w.js} +83 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -6
- package/dist/node.js +5 -4
- package/dist/observability/index.js +3 -3
- package/dist/remote.js +1 -1
- package/dist/server/context-contribution.d.ts +8 -0
- package/dist/server/context-contribution.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +8 -5
- package/dist/server/middleware/auth.d.ts +29 -2
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/testing/surface-conformance.d.ts +143 -10
- package/dist/testing/surface-conformance.d.ts.map +1 -1
- package/dist/testing/surface-manifest.d.ts +132 -33
- package/dist/testing/surface-manifest.d.ts.map +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +470 -122
- package/dist/tools/async-operation.d.ts +210 -18
- package/dist/tools/async-operation.d.ts.map +1 -1
- package/dist/tools/define-download-tool.d.ts.map +1 -1
- package/dist/tools/define-upload-tool.d.ts.map +1 -1
- package/dist/tools/internal/surface-projector.d.ts +101 -0
- package/dist/tools/internal/surface-projector.d.ts.map +1 -0
- package/dist/tools/managed-file-error.d.ts +6 -0
- package/dist/tools/managed-file-error.d.ts.map +1 -0
- package/dist/tools/mcp-prepare.d.ts +3 -21
- package/dist/tools/mcp-prepare.d.ts.map +1 -1
- package/dist/tools/mcp-round-policy.d.ts +9 -0
- package/dist/tools/mcp-round-policy.d.ts.map +1 -0
- package/dist/tools/mcp-round.d.ts +0 -4
- package/dist/tools/mcp-round.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/mount-upload.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +5 -9
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/runtime-tool.d.ts.map +1 -1
- package/dist/tools/surface.d.ts +7 -0
- package/dist/tools/surface.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +348 -383
- package/llms-full.txt +361 -58
- package/package.json +1 -1
- package/dist/index-h60df7rj.js +0 -110
- package/dist/index-k4dftwf7.js +0 -66
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import {
|
|
2
|
+
toJsonSchema
|
|
3
|
+
} from "./index-cby4ar3v.js";
|
|
4
|
+
import {
|
|
5
|
+
isRecord
|
|
6
|
+
} from "./index-7eq9va6e.js";
|
|
7
|
+
|
|
8
|
+
// src/tools/portable-formats.ts
|
|
9
|
+
var PORTABLE_JSON_SCHEMA_FORMATS = new Set([
|
|
10
|
+
"date",
|
|
11
|
+
"time",
|
|
12
|
+
"date-time",
|
|
13
|
+
"duration",
|
|
14
|
+
"uri",
|
|
15
|
+
"uri-reference",
|
|
16
|
+
"uri-template",
|
|
17
|
+
"email",
|
|
18
|
+
"hostname",
|
|
19
|
+
"ipv4",
|
|
20
|
+
"ipv6",
|
|
21
|
+
"regex",
|
|
22
|
+
"uuid",
|
|
23
|
+
"json-pointer",
|
|
24
|
+
"relative-json-pointer"
|
|
25
|
+
]);
|
|
26
|
+
function propertyPath(prefix, property) {
|
|
27
|
+
return prefix ? `${prefix}.${property}` : property;
|
|
28
|
+
}
|
|
29
|
+
function keywordPath(prefix, keyword) {
|
|
30
|
+
if (prefix && keyword.startsWith("["))
|
|
31
|
+
return `${prefix}${keyword}`;
|
|
32
|
+
return prefix ? `${prefix}.${keyword}` : keyword;
|
|
33
|
+
}
|
|
34
|
+
function findNonPortableFormats(schema, allowFormats = []) {
|
|
35
|
+
const allowed = new Set([...PORTABLE_JSON_SCHEMA_FORMATS, ...allowFormats]);
|
|
36
|
+
const found = [];
|
|
37
|
+
const pending = [{ value: schema, path: "" }];
|
|
38
|
+
while (pending.length > 0) {
|
|
39
|
+
const current = pending.pop();
|
|
40
|
+
if (!current || !isRecord(current.value))
|
|
41
|
+
continue;
|
|
42
|
+
const format = current.value.format;
|
|
43
|
+
if (typeof format === "string" && !allowed.has(format)) {
|
|
44
|
+
found.push({ path: current.path || "(root)", format });
|
|
45
|
+
}
|
|
46
|
+
const properties = current.value.properties;
|
|
47
|
+
if (isRecord(properties)) {
|
|
48
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
49
|
+
pending.push({ value, path: propertyPath(current.path, key) });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const mapKey of ["$defs", "definitions", "patternProperties", "dependentSchemas"]) {
|
|
53
|
+
const entries = current.value[mapKey];
|
|
54
|
+
if (!isRecord(entries))
|
|
55
|
+
continue;
|
|
56
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
57
|
+
pending.push({ value, path: keywordPath(current.path, `${mapKey}.${key}`) });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const directChildren = [
|
|
61
|
+
["items", "[]"],
|
|
62
|
+
["contains", "contains"],
|
|
63
|
+
["additionalProperties", "*"],
|
|
64
|
+
["unevaluatedProperties", "*"],
|
|
65
|
+
["propertyNames", "propertyNames"],
|
|
66
|
+
["not", "not"],
|
|
67
|
+
["if", "if"],
|
|
68
|
+
["then", "then"],
|
|
69
|
+
["else", "else"]
|
|
70
|
+
];
|
|
71
|
+
for (const [keyword, segment] of directChildren) {
|
|
72
|
+
const child = current.value[keyword];
|
|
73
|
+
if (isRecord(child)) {
|
|
74
|
+
pending.push({ value: child, path: keywordPath(current.path, segment) });
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(child)) {
|
|
77
|
+
child.forEach((value, index) => {
|
|
78
|
+
pending.push({ value, path: keywordPath(current.path, `${segment}[${index}]`) });
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const keyword of ["prefixItems", "allOf", "anyOf", "oneOf"]) {
|
|
83
|
+
const branches = current.value[keyword];
|
|
84
|
+
if (!Array.isArray(branches))
|
|
85
|
+
continue;
|
|
86
|
+
branches.forEach((value, index) => {
|
|
87
|
+
pending.push({ value, path: keywordPath(current.path, `${keyword}[${index}]`) });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return found.sort((left, right) => left.path.localeCompare(right.path) || left.format.localeCompare(right.format));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/tools/flatten-join.ts
|
|
95
|
+
var ANNOTATION_KEYS = new Set(["$schema", "default", "description", "examples", "title"]);
|
|
96
|
+
var TYPE_KEYWORDS = {
|
|
97
|
+
array: [
|
|
98
|
+
"contains",
|
|
99
|
+
"items",
|
|
100
|
+
"maxContains",
|
|
101
|
+
"maxItems",
|
|
102
|
+
"minContains",
|
|
103
|
+
"minItems",
|
|
104
|
+
"prefixItems",
|
|
105
|
+
"uniqueItems"
|
|
106
|
+
],
|
|
107
|
+
integer: ["exclusiveMaximum", "exclusiveMinimum", "maximum", "minimum", "multipleOf"],
|
|
108
|
+
number: ["exclusiveMaximum", "exclusiveMinimum", "maximum", "minimum", "multipleOf"],
|
|
109
|
+
object: [
|
|
110
|
+
"additionalProperties",
|
|
111
|
+
"dependentRequired",
|
|
112
|
+
"dependentSchemas",
|
|
113
|
+
"maxProperties",
|
|
114
|
+
"minProperties",
|
|
115
|
+
"patternProperties",
|
|
116
|
+
"properties",
|
|
117
|
+
"propertyNames",
|
|
118
|
+
"required"
|
|
119
|
+
],
|
|
120
|
+
string: [
|
|
121
|
+
"contentEncoding",
|
|
122
|
+
"contentMediaType",
|
|
123
|
+
"format",
|
|
124
|
+
"maxLength",
|
|
125
|
+
"minLength",
|
|
126
|
+
"pattern"
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
function nullableProjection(schema) {
|
|
130
|
+
if (!Array.isArray(schema.anyOf) || schema.anyOf.length !== 2)
|
|
131
|
+
return schema;
|
|
132
|
+
const branches = schema.anyOf.filter(isRecord);
|
|
133
|
+
if (branches.length !== 2)
|
|
134
|
+
return schema;
|
|
135
|
+
const nullBranch = branches.find((branch) => branch.type === "null");
|
|
136
|
+
const value = branches.find((branch) => typeof branch.type === "string" && branch.type !== "null");
|
|
137
|
+
if (!nullBranch || !value || Object.keys(nullBranch).some((key) => key !== "type")) {
|
|
138
|
+
return schema;
|
|
139
|
+
}
|
|
140
|
+
const type = value.type;
|
|
141
|
+
if (typeof type !== "string")
|
|
142
|
+
return schema;
|
|
143
|
+
const projected = { type: [type, "null"] };
|
|
144
|
+
for (const key of [...ANNOTATION_KEYS, ...TYPE_KEYWORDS[type] ?? []]) {
|
|
145
|
+
if (value[key] !== undefined)
|
|
146
|
+
projected[key] = value[key];
|
|
147
|
+
else if (schema[key] !== undefined)
|
|
148
|
+
projected[key] = schema[key];
|
|
149
|
+
}
|
|
150
|
+
if (value.const !== undefined)
|
|
151
|
+
projected.enum = [value.const, null];
|
|
152
|
+
else if (Array.isArray(value.enum))
|
|
153
|
+
projected.enum = [...value.enum, null];
|
|
154
|
+
return projected;
|
|
155
|
+
}
|
|
156
|
+
function withoutAnnotations(schema) {
|
|
157
|
+
const result = {};
|
|
158
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
159
|
+
if (!ANNOTATION_KEYS.has(key))
|
|
160
|
+
result[key] = value;
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
function stableJson(value) {
|
|
165
|
+
if (Array.isArray(value))
|
|
166
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
167
|
+
if (!isRecord(value))
|
|
168
|
+
return JSON.stringify(value) ?? "undefined";
|
|
169
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
170
|
+
}
|
|
171
|
+
function stringValues(schema) {
|
|
172
|
+
if (typeof schema.const === "string")
|
|
173
|
+
return [schema.const];
|
|
174
|
+
if (!Array.isArray(schema.enum))
|
|
175
|
+
return null;
|
|
176
|
+
const values = schema.enum.filter((value) => typeof value === "string");
|
|
177
|
+
return values.length === schema.enum.length ? values : null;
|
|
178
|
+
}
|
|
179
|
+
var JSON_SCHEMA_TYPE_ORDER = [
|
|
180
|
+
"string",
|
|
181
|
+
"number",
|
|
182
|
+
"integer",
|
|
183
|
+
"boolean",
|
|
184
|
+
"object",
|
|
185
|
+
"array",
|
|
186
|
+
"null"
|
|
187
|
+
];
|
|
188
|
+
function jsonSchemaType(value) {
|
|
189
|
+
switch (value) {
|
|
190
|
+
case "array":
|
|
191
|
+
case "boolean":
|
|
192
|
+
case "integer":
|
|
193
|
+
case "null":
|
|
194
|
+
case "number":
|
|
195
|
+
case "object":
|
|
196
|
+
case "string":
|
|
197
|
+
return value;
|
|
198
|
+
default:
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function valueType(value) {
|
|
203
|
+
if (value === null)
|
|
204
|
+
return "null";
|
|
205
|
+
if (Array.isArray(value))
|
|
206
|
+
return "array";
|
|
207
|
+
if (isRecord(value))
|
|
208
|
+
return "object";
|
|
209
|
+
if (typeof value === "string")
|
|
210
|
+
return "string";
|
|
211
|
+
if (typeof value === "number")
|
|
212
|
+
return "number";
|
|
213
|
+
if (typeof value === "boolean")
|
|
214
|
+
return "boolean";
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
function normalizeTypes(types) {
|
|
218
|
+
const normalized = new Set(types);
|
|
219
|
+
if (normalized.has("number"))
|
|
220
|
+
normalized.delete("integer");
|
|
221
|
+
return JSON_SCHEMA_TYPE_ORDER.filter((type) => normalized.has(type));
|
|
222
|
+
}
|
|
223
|
+
function combinedBranchTypes(schema) {
|
|
224
|
+
for (const keyword of ["oneOf", "anyOf"]) {
|
|
225
|
+
const value = schema[keyword];
|
|
226
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
227
|
+
continue;
|
|
228
|
+
const collected = [];
|
|
229
|
+
for (const branch of value) {
|
|
230
|
+
if (!isRecord(branch))
|
|
231
|
+
return null;
|
|
232
|
+
const types = typeNames(branch);
|
|
233
|
+
if (!types)
|
|
234
|
+
return null;
|
|
235
|
+
collected.push(...types);
|
|
236
|
+
}
|
|
237
|
+
return normalizeTypes(collected);
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
function typeNames(schema) {
|
|
242
|
+
const direct = jsonSchemaType(schema.type);
|
|
243
|
+
if (direct)
|
|
244
|
+
return [direct];
|
|
245
|
+
if (Array.isArray(schema.type)) {
|
|
246
|
+
const values = [];
|
|
247
|
+
for (const value of schema.type) {
|
|
248
|
+
const type = jsonSchemaType(value);
|
|
249
|
+
if (!type)
|
|
250
|
+
return null;
|
|
251
|
+
values.push(type);
|
|
252
|
+
}
|
|
253
|
+
return values.length > 0 ? normalizeTypes(values) : null;
|
|
254
|
+
}
|
|
255
|
+
if (schema.const !== undefined) {
|
|
256
|
+
const type = valueType(schema.const);
|
|
257
|
+
return type ? [type] : null;
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
260
|
+
const values = [];
|
|
261
|
+
for (const value of schema.enum) {
|
|
262
|
+
const type = valueType(value);
|
|
263
|
+
if (!type)
|
|
264
|
+
return null;
|
|
265
|
+
values.push(type);
|
|
266
|
+
}
|
|
267
|
+
return normalizeTypes(values);
|
|
268
|
+
}
|
|
269
|
+
return combinedBranchTypes(schema);
|
|
270
|
+
}
|
|
271
|
+
function typeSchema(type, nullable) {
|
|
272
|
+
const schema = { type: nullable ? [type, "null"] : type };
|
|
273
|
+
if (type === "array")
|
|
274
|
+
schema.items = {};
|
|
275
|
+
if (type === "object")
|
|
276
|
+
schema.additionalProperties = {};
|
|
277
|
+
return schema;
|
|
278
|
+
}
|
|
279
|
+
function commonBaseType(schemas) {
|
|
280
|
+
const names = schemas.map(typeNames);
|
|
281
|
+
if (names.some((value) => value === null))
|
|
282
|
+
return {};
|
|
283
|
+
let nullable = false;
|
|
284
|
+
const bases = new Set;
|
|
285
|
+
for (const branch of names) {
|
|
286
|
+
if (!branch)
|
|
287
|
+
return {};
|
|
288
|
+
for (const name of branch) {
|
|
289
|
+
if (name === "null")
|
|
290
|
+
nullable = true;
|
|
291
|
+
else
|
|
292
|
+
bases.add(name);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (bases.size === 0)
|
|
296
|
+
return nullable ? { type: "null" } : {};
|
|
297
|
+
if (bases.size === 1) {
|
|
298
|
+
const [only] = bases;
|
|
299
|
+
return only ? typeSchema(only, nullable) : {};
|
|
300
|
+
}
|
|
301
|
+
if ([...bases].every((name) => name === "integer" || name === "number")) {
|
|
302
|
+
return typeSchema("number", nullable);
|
|
303
|
+
}
|
|
304
|
+
const types = normalizeTypes(nullable ? [...bases, "null"] : bases);
|
|
305
|
+
return types.length > 0 ? { type: types } : {};
|
|
306
|
+
}
|
|
307
|
+
function mergePropertySchemas(properties) {
|
|
308
|
+
const schemas = properties.map((property) => nullableProjection(property.schema));
|
|
309
|
+
const structural = schemas.map(withoutAnnotations);
|
|
310
|
+
const [first] = structural;
|
|
311
|
+
let merged;
|
|
312
|
+
if (first && structural.every((schema) => stableJson(schema) === stableJson(first))) {
|
|
313
|
+
merged = structuredClone(first);
|
|
314
|
+
} else {
|
|
315
|
+
const values = schemas.map(stringValues);
|
|
316
|
+
const allStrings = values.every((value) => value !== null);
|
|
317
|
+
merged = allStrings ? { type: "string", enum: [...new Set(values.flatMap((value) => value ?? []))] } : commonBaseType(schemas);
|
|
318
|
+
}
|
|
319
|
+
const descriptions = [
|
|
320
|
+
...new Set(schemas.map((schema) => schema.description).filter((value) => typeof value === "string"))
|
|
321
|
+
];
|
|
322
|
+
if (descriptions.length === 1)
|
|
323
|
+
merged.description = descriptions[0];
|
|
324
|
+
return merged;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/tools/flatten.ts
|
|
328
|
+
function rebuild(value) {
|
|
329
|
+
if (Array.isArray(value))
|
|
330
|
+
return value.map(rebuild);
|
|
331
|
+
if (!isRecord(value))
|
|
332
|
+
return value;
|
|
333
|
+
const node = {};
|
|
334
|
+
for (const [key, child] of Object.entries(value))
|
|
335
|
+
node[key] = rebuild(child);
|
|
336
|
+
return flattenNode(node);
|
|
337
|
+
}
|
|
338
|
+
function stringValues2(schema) {
|
|
339
|
+
if (typeof schema.const === "string")
|
|
340
|
+
return [schema.const];
|
|
341
|
+
if (!Array.isArray(schema.enum))
|
|
342
|
+
return null;
|
|
343
|
+
const values = schema.enum.filter((value) => typeof value === "string");
|
|
344
|
+
return values.length === schema.enum.length ? values : null;
|
|
345
|
+
}
|
|
346
|
+
function objectVariants(node) {
|
|
347
|
+
if (!Array.isArray(node.oneOf) || node.oneOf.length < 2)
|
|
348
|
+
return null;
|
|
349
|
+
const variants = node.oneOf.filter(isRecord);
|
|
350
|
+
if (variants.length !== node.oneOf.length)
|
|
351
|
+
return null;
|
|
352
|
+
if (variants.some((variant) => variant.type !== "object" || !isRecord(variant.properties))) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
return variants;
|
|
356
|
+
}
|
|
357
|
+
function findDiscriminator(variants) {
|
|
358
|
+
const [first] = variants;
|
|
359
|
+
if (!first || !isRecord(first.properties))
|
|
360
|
+
return null;
|
|
361
|
+
for (const key of Object.keys(first.properties).sort()) {
|
|
362
|
+
const values = [];
|
|
363
|
+
const seen = new Set;
|
|
364
|
+
let valid = true;
|
|
365
|
+
for (const variant of variants) {
|
|
366
|
+
if (!requiredKeys(variant).has(key)) {
|
|
367
|
+
valid = false;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
const properties = variant.properties;
|
|
371
|
+
const field = isRecord(properties) ? properties[key] : undefined;
|
|
372
|
+
const branchValues = isRecord(field) ? stringValues2(field) : null;
|
|
373
|
+
if (!branchValues || branchValues.length === 0) {
|
|
374
|
+
valid = false;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
for (const value of branchValues) {
|
|
378
|
+
if (seen.has(value)) {
|
|
379
|
+
valid = false;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
seen.add(value);
|
|
383
|
+
}
|
|
384
|
+
if (!valid)
|
|
385
|
+
break;
|
|
386
|
+
values.push(branchValues);
|
|
387
|
+
}
|
|
388
|
+
if (valid)
|
|
389
|
+
return { key, values };
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
function requiredKeys(variant) {
|
|
394
|
+
if (!Array.isArray(variant.required))
|
|
395
|
+
return new Set;
|
|
396
|
+
return new Set(variant.required.filter((key) => typeof key === "string"));
|
|
397
|
+
}
|
|
398
|
+
function appendHint(schema, hint) {
|
|
399
|
+
const description = typeof schema.description === "string" ? schema.description : undefined;
|
|
400
|
+
return { ...schema, description: description ? `${description} ${hint}` : hint };
|
|
401
|
+
}
|
|
402
|
+
function flattenNode(node) {
|
|
403
|
+
const variants = objectVariants(node);
|
|
404
|
+
if (!variants)
|
|
405
|
+
return node;
|
|
406
|
+
const discriminator = findDiscriminator(variants);
|
|
407
|
+
if (!discriminator)
|
|
408
|
+
return node;
|
|
409
|
+
const perKey = new Map;
|
|
410
|
+
for (let index = 0;index < variants.length; index++) {
|
|
411
|
+
const variant = variants[index];
|
|
412
|
+
if (!variant)
|
|
413
|
+
continue;
|
|
414
|
+
const labels = discriminator.values[index] ?? [];
|
|
415
|
+
const required2 = requiredKeys(variant);
|
|
416
|
+
if (!isRecord(variant.properties))
|
|
417
|
+
continue;
|
|
418
|
+
for (const [key, rawSchema] of Object.entries(variant.properties)) {
|
|
419
|
+
if (key === discriminator.key || !isRecord(rawSchema))
|
|
420
|
+
continue;
|
|
421
|
+
const entries = perKey.get(key) ?? [];
|
|
422
|
+
entries.push({ schema: rawSchema, labels, required: required2.has(key) });
|
|
423
|
+
perKey.set(key, entries);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const discriminatorProperties = [];
|
|
427
|
+
for (let index = 0;index < variants.length; index++) {
|
|
428
|
+
const variant = variants[index];
|
|
429
|
+
if (!variant || !isRecord(variant.properties))
|
|
430
|
+
continue;
|
|
431
|
+
const schema = variant.properties[discriminator.key];
|
|
432
|
+
if (!isRecord(schema))
|
|
433
|
+
continue;
|
|
434
|
+
discriminatorProperties.push({
|
|
435
|
+
schema,
|
|
436
|
+
labels: discriminator.values[index] ?? [],
|
|
437
|
+
required: true
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const properties = {
|
|
441
|
+
[discriminator.key]: mergePropertySchemas(discriminatorProperties)
|
|
442
|
+
};
|
|
443
|
+
const required = [discriminator.key];
|
|
444
|
+
for (const [key, entries] of perKey) {
|
|
445
|
+
let merged = mergePropertySchemas(entries);
|
|
446
|
+
const requiredEverywhere = entries.length === variants.length && entries.every((entry) => entry.required);
|
|
447
|
+
if (requiredEverywhere) {
|
|
448
|
+
required.push(key);
|
|
449
|
+
} else {
|
|
450
|
+
const requiredLabels = entries.filter((entry) => entry.required).flatMap((entry) => entry.labels);
|
|
451
|
+
const presentLabels = entries.flatMap((entry) => entry.labels);
|
|
452
|
+
const labels = requiredLabels.length > 0 ? requiredLabels : presentLabels;
|
|
453
|
+
const verb = requiredLabels.length > 0 ? "Required" : "Available";
|
|
454
|
+
merged = appendHint(merged, `${verb} if ${discriminator.key} = ${[...new Set(labels)].join(" | ")}`);
|
|
455
|
+
}
|
|
456
|
+
properties[key] = merged;
|
|
457
|
+
}
|
|
458
|
+
const result = {};
|
|
459
|
+
for (const [key, value] of Object.entries(node)) {
|
|
460
|
+
if (key !== "oneOf" && key !== "anyOf" && key !== "properties" && key !== "required") {
|
|
461
|
+
result[key] = value;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
result.type = "object";
|
|
465
|
+
result.properties = properties;
|
|
466
|
+
result.required = required;
|
|
467
|
+
result.additionalProperties = variants.every((variant) => variant.additionalProperties === false) ? false : {};
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
470
|
+
function flattenToolJsonSchema(schema) {
|
|
471
|
+
const result = rebuild(schema);
|
|
472
|
+
return isRecord(result) ? result : {};
|
|
473
|
+
}
|
|
474
|
+
function freezeToolJsonSchema(schema) {
|
|
475
|
+
for (const value of Object.values(schema))
|
|
476
|
+
freezeJsonValue(value);
|
|
477
|
+
return Object.freeze(schema);
|
|
478
|
+
}
|
|
479
|
+
function freezeJsonValue(value) {
|
|
480
|
+
if (Array.isArray(value)) {
|
|
481
|
+
for (const item of value)
|
|
482
|
+
freezeJsonValue(item);
|
|
483
|
+
Object.freeze(value);
|
|
484
|
+
} else if (isRecord(value)) {
|
|
485
|
+
freezeToolJsonSchema(value);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/tools/untyped-properties.ts
|
|
490
|
+
var TYPE_KEYWORDS2 = ["type", "enum", "const", "anyOf", "oneOf", "allOf", "$ref", "not"];
|
|
491
|
+
function saysWhatItIs(schema) {
|
|
492
|
+
return TYPE_KEYWORDS2.some((keyword) => schema[keyword] !== undefined);
|
|
493
|
+
}
|
|
494
|
+
function findUntypedProperties(schema, prefix = "") {
|
|
495
|
+
if (!isRecord(schema))
|
|
496
|
+
return [];
|
|
497
|
+
const found = [];
|
|
498
|
+
const properties = schema.properties;
|
|
499
|
+
if (isRecord(properties)) {
|
|
500
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
501
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
502
|
+
if (isRecord(value)) {
|
|
503
|
+
if (!saysWhatItIs(value)) {
|
|
504
|
+
const description = value.description;
|
|
505
|
+
found.push({
|
|
506
|
+
path,
|
|
507
|
+
...typeof description === "string" && { description }
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
found.push(...findUntypedProperties(value, path));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for (const key of ["items", "additionalProperties"]) {
|
|
515
|
+
const child = schema[key];
|
|
516
|
+
if (isRecord(child))
|
|
517
|
+
found.push(...findUntypedProperties(child, prefix));
|
|
518
|
+
if (Array.isArray(child)) {
|
|
519
|
+
for (const entry of child) {
|
|
520
|
+
if (isRecord(entry))
|
|
521
|
+
found.push(...findUntypedProperties(entry, prefix));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
for (const key of ["prefixItems"]) {
|
|
526
|
+
const child = schema[key];
|
|
527
|
+
if (Array.isArray(child)) {
|
|
528
|
+
for (const entry of child) {
|
|
529
|
+
if (isRecord(entry))
|
|
530
|
+
found.push(...findUntypedProperties(entry, prefix));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const patterned = schema.patternProperties;
|
|
535
|
+
if (isRecord(patterned)) {
|
|
536
|
+
for (const value of Object.values(patterned)) {
|
|
537
|
+
if (isRecord(value))
|
|
538
|
+
found.push(...findUntypedProperties(value, prefix));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
for (const key of ["$defs", "definitions"]) {
|
|
542
|
+
const container = schema[key];
|
|
543
|
+
if (!isRecord(container))
|
|
544
|
+
continue;
|
|
545
|
+
for (const definition of Object.values(container)) {
|
|
546
|
+
if (isRecord(definition))
|
|
547
|
+
found.push(...findUntypedProperties(definition, prefix));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
for (const key of ["allOf", "anyOf", "oneOf"]) {
|
|
551
|
+
const branches = schema[key];
|
|
552
|
+
if (Array.isArray(branches)) {
|
|
553
|
+
for (const branch of branches)
|
|
554
|
+
found.push(...findUntypedProperties(branch, prefix));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return found;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/tools/schema.ts
|
|
561
|
+
import { z } from "zod";
|
|
562
|
+
function objectShapeKeys(schema) {
|
|
563
|
+
return schema instanceof z.ZodObject ? Object.keys(schema.shape) : [];
|
|
564
|
+
}
|
|
565
|
+
function keyPolicyOf(schema) {
|
|
566
|
+
return schema.def.catchall;
|
|
567
|
+
}
|
|
568
|
+
function rebuildObject(source, shape) {
|
|
569
|
+
return withKeyPolicy(z.object(shape), keyPolicyOf(source));
|
|
570
|
+
}
|
|
571
|
+
function withKeyPolicy(object, policy) {
|
|
572
|
+
if (policy === undefined)
|
|
573
|
+
return object;
|
|
574
|
+
return object.catchall(representable(policy));
|
|
575
|
+
}
|
|
576
|
+
function representable(policy) {
|
|
577
|
+
if (policy instanceof z.ZodNever || policy instanceof z.ZodUnknown)
|
|
578
|
+
return policy;
|
|
579
|
+
if (!(policy instanceof z.ZodType))
|
|
580
|
+
return z.unknown();
|
|
581
|
+
try {
|
|
582
|
+
toJsonSchema(policy, "input");
|
|
583
|
+
return policy;
|
|
584
|
+
} catch {
|
|
585
|
+
return z.unknown();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function mergeSchemas(paramsSchema, inputSchema) {
|
|
589
|
+
if (paramsSchema && !(paramsSchema instanceof z.ZodObject)) {
|
|
590
|
+
throw new Error("Tool params schema must be a z.object()");
|
|
591
|
+
}
|
|
592
|
+
const paramsObject = paramsSchema instanceof z.ZodObject ? paramsSchema : undefined;
|
|
593
|
+
if (!inputSchema) {
|
|
594
|
+
return paramsObject ?? z.object({});
|
|
595
|
+
}
|
|
596
|
+
if (inputSchema instanceof z.ZodObject) {
|
|
597
|
+
if (paramsObject) {
|
|
598
|
+
const conflicts = Object.keys(paramsObject.shape).filter((key) => (key in inputSchema.shape));
|
|
599
|
+
if (conflicts.length > 0) {
|
|
600
|
+
throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return rebuildObject(inputSchema, {
|
|
604
|
+
...paramsObject?.shape ?? {},
|
|
605
|
+
...inputSchema.shape
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
return paramsObject ? z.intersection(paramsObject, inputSchema) : inputSchema;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/tools/names.ts
|
|
612
|
+
var SINGULAR_EXCEPTIONS = new Set([
|
|
613
|
+
"analytics",
|
|
614
|
+
"status",
|
|
615
|
+
"stats",
|
|
616
|
+
"settings",
|
|
617
|
+
"media",
|
|
618
|
+
"progress",
|
|
619
|
+
"news"
|
|
620
|
+
]);
|
|
621
|
+
var TOOL_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
622
|
+
function normalizeService(value) {
|
|
623
|
+
return value.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
624
|
+
}
|
|
625
|
+
function normalizeMethod(value) {
|
|
626
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
627
|
+
}
|
|
628
|
+
function singularize(name) {
|
|
629
|
+
if (SINGULAR_EXCEPTIONS.has(name))
|
|
630
|
+
return name;
|
|
631
|
+
if (name.endsWith("ies"))
|
|
632
|
+
return `${name.slice(0, -3)}y`;
|
|
633
|
+
if (name.endsWith("s") && !name.endsWith("ss"))
|
|
634
|
+
return name.slice(0, -1);
|
|
635
|
+
return name;
|
|
636
|
+
}
|
|
637
|
+
function singularizeTail(name) {
|
|
638
|
+
const cut = name.lastIndexOf("_");
|
|
639
|
+
if (cut === -1)
|
|
640
|
+
return singularize(name);
|
|
641
|
+
return `${name.slice(0, cut + 1)}${singularize(name.slice(cut + 1))}`;
|
|
642
|
+
}
|
|
643
|
+
function hasUsableChars(value) {
|
|
644
|
+
return /[a-zA-Z0-9]/.test(value);
|
|
645
|
+
}
|
|
646
|
+
function assertToolName(name, serviceName, key) {
|
|
647
|
+
const where = `service "${serviceName}", method "${key}"`;
|
|
648
|
+
if (!TOOL_NAME_RE.test(name)) {
|
|
649
|
+
const why = name.length > 64 ? `is ${name.length} characters (max 64) — set an explicit \`toolName\`` : "must match [a-zA-Z0-9_-]";
|
|
650
|
+
throw new Error(`Tool name "${name}" (${where}) ${why}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function toToolName(serviceName, methodName) {
|
|
654
|
+
const normalized = normalizeService(serviceName);
|
|
655
|
+
const singular = singularizeTail(normalized);
|
|
656
|
+
const method = normalizeMethod(methodName);
|
|
657
|
+
if (method === "list")
|
|
658
|
+
return `list_${normalized}`;
|
|
659
|
+
if (method === "get")
|
|
660
|
+
return `get_${singular}`;
|
|
661
|
+
if (method === "create")
|
|
662
|
+
return `create_${singular}`;
|
|
663
|
+
if (method === "update")
|
|
664
|
+
return `update_${singular}`;
|
|
665
|
+
if (method === "delete")
|
|
666
|
+
return `delete_${singular}`;
|
|
667
|
+
const snake = method.replace(/([A-Z])/g, "_$1").toLowerCase();
|
|
668
|
+
return `${snake}_${singular}`.replace(/^_/, "");
|
|
669
|
+
}
|
|
670
|
+
function assertUniqueToolName(name, taken, surface) {
|
|
671
|
+
if (taken) {
|
|
672
|
+
throw new Error(`Duplicate ${surface} "${name}" across mounted operations`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/tools/presentation.ts
|
|
677
|
+
import { z as z2 } from "zod";
|
|
678
|
+
function withoutDialect(schema) {
|
|
679
|
+
const result = {};
|
|
680
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
681
|
+
if (key !== "$schema")
|
|
682
|
+
result[key] = value;
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
}
|
|
686
|
+
function hasLocalReference(value) {
|
|
687
|
+
if (Array.isArray(value))
|
|
688
|
+
return value.some(hasLocalReference);
|
|
689
|
+
if (!isRecord(value))
|
|
690
|
+
return false;
|
|
691
|
+
if (typeof value.$ref === "string" && value.$ref.startsWith("#"))
|
|
692
|
+
return true;
|
|
693
|
+
return Object.values(value).some(hasLocalReference);
|
|
694
|
+
}
|
|
695
|
+
function rewriteLocalReferences(value, namespace) {
|
|
696
|
+
if (Array.isArray(value)) {
|
|
697
|
+
return value.map((child) => rewriteLocalReferences(child, namespace));
|
|
698
|
+
}
|
|
699
|
+
if (!isRecord(value))
|
|
700
|
+
return value;
|
|
701
|
+
const rewritten = {};
|
|
702
|
+
for (const [key, child] of Object.entries(value)) {
|
|
703
|
+
rewritten[key] = key === "$ref" && typeof child === "string" && child.startsWith("#") ? `#/definitions/${namespace}${child.slice(1)}` : rewriteLocalReferences(child, namespace);
|
|
704
|
+
}
|
|
705
|
+
return rewritten;
|
|
706
|
+
}
|
|
707
|
+
function namespaceLocalReferences(schema, namespace) {
|
|
708
|
+
if (!schema || !hasLocalReference(schema))
|
|
709
|
+
return schema;
|
|
710
|
+
const rewritten = rewriteLocalReferences(schema, namespace);
|
|
711
|
+
if (!isRecord(rewritten))
|
|
712
|
+
return schema;
|
|
713
|
+
return { ...rewritten, definitions: { [namespace]: rewritten } };
|
|
714
|
+
}
|
|
715
|
+
function objectProperties(schema) {
|
|
716
|
+
return schema.type === "object" && isRecord(schema.properties) ? schema.properties : null;
|
|
717
|
+
}
|
|
718
|
+
function requiredKeys2(schema) {
|
|
719
|
+
return Array.isArray(schema.required) ? schema.required.filter((key) => typeof key === "string") : [];
|
|
720
|
+
}
|
|
721
|
+
function mergeObjectSchemas(left, right, rightOwnsUnknownKeys) {
|
|
722
|
+
const leftProperties = objectProperties(left);
|
|
723
|
+
const rightProperties = objectProperties(right);
|
|
724
|
+
if (!leftProperties || !rightProperties)
|
|
725
|
+
return { allOf: [left, right] };
|
|
726
|
+
const conflicts = Object.keys(leftProperties).filter((key) => (key in rightProperties));
|
|
727
|
+
if (conflicts.length > 0) {
|
|
728
|
+
throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
|
|
729
|
+
}
|
|
730
|
+
const merged = {
|
|
731
|
+
type: "object",
|
|
732
|
+
properties: { ...leftProperties, ...rightProperties },
|
|
733
|
+
required: [...new Set([...requiredKeys2(left), ...requiredKeys2(right)])]
|
|
734
|
+
};
|
|
735
|
+
const leftDefinitions = isRecord(left.definitions) ? left.definitions : {};
|
|
736
|
+
const rightDefinitions = isRecord(right.definitions) ? right.definitions : {};
|
|
737
|
+
const definitions = { ...leftDefinitions, ...rightDefinitions };
|
|
738
|
+
if (Object.keys(definitions).length > 0)
|
|
739
|
+
merged.definitions = definitions;
|
|
740
|
+
const unknownKeys = rightOwnsUnknownKeys ? right.additionalProperties : left.additionalProperties;
|
|
741
|
+
if (unknownKeys !== undefined)
|
|
742
|
+
merged.additionalProperties = unknownKeys;
|
|
743
|
+
return merged;
|
|
744
|
+
}
|
|
745
|
+
function schemaFromZod(schema, unrepresentable, flatten) {
|
|
746
|
+
if (!schema)
|
|
747
|
+
return;
|
|
748
|
+
const json = toJsonSchema(schema, "input", unrepresentable, "draft-07");
|
|
749
|
+
return flatten ? flattenToolJsonSchema(json) : json;
|
|
750
|
+
}
|
|
751
|
+
function buildToolPresentationSchema(config) {
|
|
752
|
+
const unrepresentable = config.unrepresentable ?? "any";
|
|
753
|
+
const flatten = config.flattenUnionInput ?? false;
|
|
754
|
+
const params = namespaceLocalReferences(schemaFromZod(config.paramsSchema, unrepresentable, flatten), "params");
|
|
755
|
+
const input = namespaceLocalReferences(schemaFromZod(config.inputSchema, unrepresentable, flatten), "input");
|
|
756
|
+
let presentation;
|
|
757
|
+
if (params && input) {
|
|
758
|
+
presentation = mergeObjectSchemas(withoutDialect(params), withoutDialect(input), true);
|
|
759
|
+
} else if (input) {
|
|
760
|
+
presentation = input;
|
|
761
|
+
} else if (params) {
|
|
762
|
+
presentation = params;
|
|
763
|
+
} else {
|
|
764
|
+
presentation = {
|
|
765
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
766
|
+
type: "object",
|
|
767
|
+
properties: {}
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
if (config.extendSchema) {
|
|
771
|
+
const extra = namespaceLocalReferences(schemaFromZod(z2.object(config.extendSchema), unrepresentable, flatten), "extend");
|
|
772
|
+
if (extra) {
|
|
773
|
+
presentation = mergeObjectSchemas(withoutDialect(extra), withoutDialect(presentation), true);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (presentation.$schema === undefined) {
|
|
777
|
+
presentation = {
|
|
778
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
779
|
+
...presentation
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
return freezeToolJsonSchema(presentation);
|
|
783
|
+
}
|
|
784
|
+
function isObjectPresentationSchema(schema) {
|
|
785
|
+
return schema.type === "object" && isRecord(schema.properties);
|
|
786
|
+
}
|
|
787
|
+
function presentationMetadata(schema) {
|
|
788
|
+
return withoutDialect(schema);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/tools/internal/surface-projector.ts
|
|
792
|
+
import { z as z3 } from "zod";
|
|
793
|
+
|
|
794
|
+
// src/tools/mcp-round-policy.ts
|
|
795
|
+
function validateMcpRoundPolicy(tool, policy, runtime) {
|
|
796
|
+
if (!runtime?.stateConfigured) {
|
|
797
|
+
throw new Error(`[stitchkit] MCP tool "${tool.name}" declares inputRequired but no multiRound.state key is configured`);
|
|
798
|
+
}
|
|
799
|
+
const { maxRounds } = runtime;
|
|
800
|
+
if (!Number.isInteger(maxRounds) || maxRounds < 1) {
|
|
801
|
+
throw new Error("[stitchkit] multiRound.serving.maxRounds must be a positive integer");
|
|
802
|
+
}
|
|
803
|
+
if (policy.inputRequired.length === 0) {
|
|
804
|
+
throw new Error(`[stitchkit] MCP tool "${tool.name}" must declare at least one input round`);
|
|
805
|
+
}
|
|
806
|
+
if (policy.inputRequired.length > maxRounds) {
|
|
807
|
+
throw new Error(`[stitchkit] MCP tool "${tool.name}" declares ${policy.inputRequired.length} input rounds, exceeding maxRounds ${maxRounds}`);
|
|
808
|
+
}
|
|
809
|
+
const keys = new Set;
|
|
810
|
+
for (const request of policy.inputRequired) {
|
|
811
|
+
if (keys.has(request.key)) {
|
|
812
|
+
throw new Error(`[stitchkit] MCP tool "${tool.name}" declares duplicate input key "${request.key}"`);
|
|
813
|
+
}
|
|
814
|
+
keys.add(request.key);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/tools/internal/surface-projector.ts
|
|
819
|
+
function assertToolExtensionCompatible(base, extra) {
|
|
820
|
+
if (!(base instanceof z3.ZodObject))
|
|
821
|
+
return;
|
|
822
|
+
const conflicts = Object.keys(extra).filter((key) => (key in base.shape));
|
|
823
|
+
if (conflicts.length > 0) {
|
|
824
|
+
throw new Error(`Tool extend conflict: ${conflicts.join(", ")} already declared by the contract`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function projectRuntimeTool(definition, assertName = true) {
|
|
828
|
+
if (assertName) {
|
|
829
|
+
assertProjectedName(definition.name, definition.identity.serviceName, definition.identity.action, "MCP", false);
|
|
830
|
+
}
|
|
831
|
+
return {
|
|
832
|
+
kind: "runtime",
|
|
833
|
+
source: definition,
|
|
834
|
+
serviceName: definition.identity.serviceName,
|
|
835
|
+
action: definition.identity.action,
|
|
836
|
+
name: definition.name,
|
|
837
|
+
presentationSchema: buildToolPresentationSchema({
|
|
838
|
+
inputSchema: definition.input,
|
|
839
|
+
unrepresentable: "any"
|
|
840
|
+
}),
|
|
841
|
+
shouldExtend: false,
|
|
842
|
+
...definition.mcp !== undefined && { mcp: definition.mcp }
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function projectedRuntimeToolSupports(definition, transport) {
|
|
846
|
+
if (definition.transports?.length === 0) {
|
|
847
|
+
throw new Error(`Runtime tool "${definition.name}" must expose at least one transport`);
|
|
848
|
+
}
|
|
849
|
+
return definition.transports ? definition.transports.includes(transport) : transport !== "CLI";
|
|
850
|
+
}
|
|
851
|
+
function duplicateLabel(transport) {
|
|
852
|
+
if (transport === "MCP")
|
|
853
|
+
return "MCP tool name";
|
|
854
|
+
if (transport === "AGENT")
|
|
855
|
+
return "agent tool name";
|
|
856
|
+
return "CLI command";
|
|
857
|
+
}
|
|
858
|
+
function assertProjectedName(name, serviceName, action, transport, derived) {
|
|
859
|
+
if (transport === "CLI")
|
|
860
|
+
return;
|
|
861
|
+
if (derived && !hasUsableChars(serviceName)) {
|
|
862
|
+
throw new Error(`Service prefix "${serviceName}" (method "${action}") has no characters usable in a tool name — set an explicit \`toolName\` or rename the prefix`);
|
|
863
|
+
}
|
|
864
|
+
assertToolName(name, serviceName, action);
|
|
865
|
+
}
|
|
866
|
+
function projectToolSurface(surface, transport, config = {}) {
|
|
867
|
+
const projected = [];
|
|
868
|
+
const names = new Set;
|
|
869
|
+
const append = (tool) => {
|
|
870
|
+
if (config.assertUniqueNames ?? true) {
|
|
871
|
+
assertUniqueToolName(tool.name, names.has(tool.name), duplicateLabel(transport));
|
|
872
|
+
}
|
|
873
|
+
names.add(tool.name);
|
|
874
|
+
projected.push(tool);
|
|
875
|
+
};
|
|
876
|
+
for (const service of surface.services ?? []) {
|
|
877
|
+
for (const [methodName, method] of Object.entries(service.methods)) {
|
|
878
|
+
if (transport === "CLI") {
|
|
879
|
+
if (!method.expose?.includes("CLI"))
|
|
880
|
+
continue;
|
|
881
|
+
} else if (method.expose && !method.expose.includes(transport)) {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (method.multipart || method.rawBody || method.responseMeta || method.rawResponse) {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const name = method.toolName ?? toToolName(service.name, methodName);
|
|
888
|
+
if (config.assertNames ?? true) {
|
|
889
|
+
assertProjectedName(name, service.name, methodName, transport, !method.toolName);
|
|
890
|
+
}
|
|
891
|
+
const shouldExtend = transport !== "CLI" && config.extend !== undefined && (!config.extend.filter || config.extend.filter(service, method));
|
|
892
|
+
if (shouldExtend && config.extend) {
|
|
893
|
+
assertToolExtensionCompatible(mergeSchemas(method.paramsSchema, method.inputSchema), config.extend.schema);
|
|
894
|
+
}
|
|
895
|
+
append({
|
|
896
|
+
kind: "contract",
|
|
897
|
+
source: method,
|
|
898
|
+
serviceName: method.serviceName,
|
|
899
|
+
action: method.key,
|
|
900
|
+
name,
|
|
901
|
+
presentationSchema: buildToolPresentationSchema({
|
|
902
|
+
paramsSchema: method.paramsSchema,
|
|
903
|
+
inputSchema: method.inputSchema,
|
|
904
|
+
extendSchema: shouldExtend ? config.extend?.schema : undefined,
|
|
905
|
+
flattenUnionInput: config.flattenUnionInput,
|
|
906
|
+
unrepresentable: "any"
|
|
907
|
+
}),
|
|
908
|
+
shouldExtend,
|
|
909
|
+
...method.mcp !== undefined && { mcp: method.mcp }
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
for (const definition of surface.runtimeTools ?? []) {
|
|
914
|
+
if (!projectedRuntimeToolSupports(definition, transport))
|
|
915
|
+
continue;
|
|
916
|
+
if (config.assertNames ?? true) {
|
|
917
|
+
assertProjectedName(definition.name, definition.identity.serviceName, definition.identity.action, transport, false);
|
|
918
|
+
}
|
|
919
|
+
append(projectRuntimeTool(definition, false));
|
|
920
|
+
}
|
|
921
|
+
return projected;
|
|
922
|
+
}
|
|
923
|
+
function mcpProjectionCandidate(tool) {
|
|
924
|
+
if (tool.kind === "contract") {
|
|
925
|
+
return {
|
|
926
|
+
tool,
|
|
927
|
+
name: tool.name,
|
|
928
|
+
...tool.source.paramsSchema !== undefined && {
|
|
929
|
+
paramsSchema: tool.source.paramsSchema
|
|
930
|
+
},
|
|
931
|
+
...tool.source.inputSchema !== undefined && {
|
|
932
|
+
inputSchema: tool.source.inputSchema
|
|
933
|
+
},
|
|
934
|
+
...tool.source.outputSchema !== undefined && {
|
|
935
|
+
outputSchema: tool.source.outputSchema
|
|
936
|
+
},
|
|
937
|
+
shouldExtend: tool.shouldExtend,
|
|
938
|
+
...tool.mcp !== undefined && { mcp: tool.mcp }
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
tool,
|
|
943
|
+
name: tool.name,
|
|
944
|
+
inputSchema: tool.source.input,
|
|
945
|
+
...tool.source.output !== undefined && { outputSchema: tool.source.output },
|
|
946
|
+
shouldExtend: false,
|
|
947
|
+
...tool.mcp !== undefined && { mcp: tool.mcp }
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function reportIncompatible(message, policy, logger, failures) {
|
|
951
|
+
if (policy === "throw")
|
|
952
|
+
failures.push(message);
|
|
953
|
+
else if (policy === "warn") {
|
|
954
|
+
if (logger)
|
|
955
|
+
logger.warn(`[stitchkit] ${message}`);
|
|
956
|
+
else
|
|
957
|
+
console.warn(`[stitchkit] ${message}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function prepareProjectedMcpTools(tools, config = {}) {
|
|
961
|
+
const policy = config.schemaValidation?.policy ?? "throw";
|
|
962
|
+
const failures = [];
|
|
963
|
+
const prepared = [];
|
|
964
|
+
const names = new Set;
|
|
965
|
+
for (const tool of tools) {
|
|
966
|
+
assertUniqueToolName(tool.name, names.has(tool.name), "MCP tool name");
|
|
967
|
+
names.add(tool.name);
|
|
968
|
+
if (tool.mcp)
|
|
969
|
+
validateMcpRoundPolicy(tool, tool.mcp, config.multiRound);
|
|
970
|
+
let inputSchema;
|
|
971
|
+
try {
|
|
972
|
+
inputSchema = buildToolPresentationSchema({
|
|
973
|
+
paramsSchema: tool.paramsSchema,
|
|
974
|
+
inputSchema: tool.inputSchema,
|
|
975
|
+
extendSchema: tool.shouldExtend && config.extend ? config.extend.schema : undefined,
|
|
976
|
+
flattenUnionInput: config.flattenUnionInput,
|
|
977
|
+
unrepresentable: "throw"
|
|
978
|
+
});
|
|
979
|
+
} catch (error) {
|
|
980
|
+
reportIncompatible(`MCP tool "${tool.name}" — input schema is not JSON Schema-compatible: ${error instanceof Error ? error.message : String(error)}`, policy, config.logger, failures);
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
if (!isObjectPresentationSchema(inputSchema)) {
|
|
984
|
+
reportIncompatible(`MCP tool "${tool.name}" — input must be an object schema; a union, discriminated union or scalar cannot be an MCP tool input (flatten it in the contract, or drop MCP from \`expose\`)`, policy, config.logger, failures);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (config.schemaValidation?.requireTypedProperties) {
|
|
988
|
+
const allowed = new Set(config.schemaValidation.allowUntyped ?? []);
|
|
989
|
+
for (const untyped of findUntypedProperties(inputSchema)) {
|
|
990
|
+
const path = `${tool.name}.${untyped.path}`;
|
|
991
|
+
if (allowed.has(path))
|
|
992
|
+
continue;
|
|
993
|
+
const clue = untyped.description ? ` (only a description: "${untyped.description}")` : "";
|
|
994
|
+
reportIncompatible(`MCP tool "${tool.name}" — input property "${untyped.path}" carries no type, enum or $ref${clue}. A model is given no way to know what to send. Use \`z.json()\` for an arbitrary JSON value; use \`allowUntyped\` only when the presentation value is genuinely not representable as JSON Schema.`, policy === "skip" ? "warn" : policy, config.logger, failures);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (config.schemaValidation?.requirePortableFormats) {
|
|
998
|
+
for (const finding of findNonPortableFormats(inputSchema, config.schemaValidation.allowFormats)) {
|
|
999
|
+
reportIncompatible(`MCP tool "${tool.name}" — input property "${finding.path}" uses non-portable JSON Schema format "${finding.format}". Use a portable pattern/schema, or list the format in \`allowFormats\` only when every MCP client supports it.`, policy === "skip" ? "warn" : policy, config.logger, failures);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const outputSchema = tool.outputSchema;
|
|
1003
|
+
let compatibleOutput;
|
|
1004
|
+
if (outputSchema) {
|
|
1005
|
+
try {
|
|
1006
|
+
const output = toJsonSchema(outputSchema, "output");
|
|
1007
|
+
compatibleOutput = outputSchema;
|
|
1008
|
+
if (config.schemaValidation?.requirePortableFormats) {
|
|
1009
|
+
for (const finding of findNonPortableFormats(output, config.schemaValidation.allowFormats)) {
|
|
1010
|
+
reportIncompatible(`MCP tool "${tool.name}" — output property "${finding.path}" uses non-portable JSON Schema format "${finding.format}". Use a portable pattern/schema, or list the format in \`allowFormats\` only when every MCP client supports it.`, policy === "skip" ? "warn" : policy, config.logger, failures);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
reportIncompatible(`MCP tool "${tool.name}" — output schema is not JSON Schema-compatible: ${error instanceof Error ? error.message : String(error)}`, policy, config.logger, failures);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
prepared.push({
|
|
1018
|
+
tool: tool.tool,
|
|
1019
|
+
inputSchema,
|
|
1020
|
+
...compatibleOutput !== undefined && { outputSchema: compatibleOutput }
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
if (failures.length > 0) {
|
|
1024
|
+
throw new Error(`[stitchkit] ${failures.length} problem(s) with MCP tool schemas:
|
|
1025
|
+
- ${failures.join(`
|
|
1026
|
+
- `)}`);
|
|
1027
|
+
}
|
|
1028
|
+
return prepared;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
export { objectShapeKeys, rebuildObject, mergeSchemas, validateMcpRoundPolicy, assertToolName, assertUniqueToolName, PORTABLE_JSON_SCHEMA_FORMATS, findNonPortableFormats, flattenToolJsonSchema, buildToolPresentationSchema, presentationMetadata, findUntypedProperties, assertToolExtensionCompatible, projectRuntimeTool, projectToolSurface, mcpProjectionCandidate, prepareProjectedMcpTools };
|