stitchkit 0.1.0 → 0.2.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 (44) hide show
  1. package/README.md +10 -7
  2. package/dist/contract/define.d.ts.map +1 -1
  3. package/dist/contract/errors.d.ts +4 -3
  4. package/dist/contract/errors.d.ts.map +1 -1
  5. package/dist/contract/index.js +1 -1
  6. package/dist/{index-gfzn1n4n.js → index-a35v22fh.js} +1 -1
  7. package/dist/{index-7wfkbvss.js → index-kckky6zw.js} +6 -2
  8. package/dist/index-v2z2v3mq.js +587 -0
  9. package/dist/index.js +2 -2
  10. package/dist/node.d.ts +4 -0
  11. package/dist/node.d.ts.map +1 -0
  12. package/dist/node.js +26 -0
  13. package/dist/server/create.d.ts +3 -3
  14. package/dist/server/create.d.ts.map +1 -1
  15. package/dist/server/index.d.ts +1 -1
  16. package/dist/server/index.d.ts.map +1 -1
  17. package/dist/server/index.js +10 -576
  18. package/dist/server/node.d.ts +12 -0
  19. package/dist/server/node.d.ts.map +1 -0
  20. package/dist/server/types.d.ts +24 -21
  21. package/dist/server/types.d.ts.map +1 -1
  22. package/dist/tools/agent.d.ts +15 -2
  23. package/dist/tools/agent.d.ts.map +1 -1
  24. package/dist/tools/coerce.d.ts +8 -0
  25. package/dist/tools/coerce.d.ts.map +1 -0
  26. package/dist/tools/execute.d.ts +26 -2
  27. package/dist/tools/execute.d.ts.map +1 -1
  28. package/dist/tools/flatten.d.ts +15 -0
  29. package/dist/tools/flatten.d.ts.map +1 -0
  30. package/dist/tools/json-schema.d.ts +18 -0
  31. package/dist/tools/json-schema.d.ts.map +1 -0
  32. package/dist/tools/manifest.d.ts +13 -0
  33. package/dist/tools/manifest.d.ts.map +1 -0
  34. package/dist/tools/mcp-handler.d.ts.map +1 -1
  35. package/dist/tools/mcp.d.ts +53 -3
  36. package/dist/tools/mcp.d.ts.map +1 -1
  37. package/dist/tools/mount.d.ts +18 -6
  38. package/dist/tools/mount.d.ts.map +1 -1
  39. package/dist/tools/schema.d.ts +19 -2
  40. package/dist/tools/schema.d.ts.map +1 -1
  41. package/dist/tools.d.ts +6 -2
  42. package/dist/tools.d.ts.map +1 -1
  43. package/dist/tools.js +337 -154
  44. package/package.json +15 -4
package/dist/tools.js CHANGED
@@ -1,23 +1,54 @@
1
1
  import {
2
2
  createClient
3
3
  } from "./index-5sxnvwb1.js";
4
- import {
5
- formatZodError,
6
- normalizeError
7
- } from "./index-gfzn1n4n.js";
8
- import"./index-7wfkbvss.js";
9
4
  import {
10
5
  isRecord
11
6
  } from "./index-809wc1tt.js";
7
+ import {
8
+ formatZodError,
9
+ normalizeError
10
+ } from "./index-a35v22fh.js";
11
+ import"./index-kckky6zw.js";
12
12
 
13
13
  // src/tools/agent.ts
14
14
  import { tool, zodSchema } from "ai";
15
15
 
16
- // src/tools/mount.ts
17
- import { z as z2 } from "zod";
16
+ // src/tools/schema.ts
17
+ import { z } from "zod";
18
+ function objectShapeKeys(schema) {
19
+ return schema instanceof z.ZodObject ? Object.keys(schema.shape) : [];
20
+ }
21
+ function mergeSchemas(paramsSchema, inputSchema) {
22
+ if (paramsSchema && !(paramsSchema instanceof z.ZodObject)) {
23
+ throw new Error("Tool params schema must be a z.object()");
24
+ }
25
+ const paramsObject = paramsSchema instanceof z.ZodObject ? paramsSchema : undefined;
26
+ if (!inputSchema) {
27
+ return paramsObject ?? z.object({});
28
+ }
29
+ if (inputSchema instanceof z.ZodObject) {
30
+ if (paramsObject) {
31
+ const conflicts = Object.keys(paramsObject.shape).filter((key) => (key in inputSchema.shape));
32
+ if (conflicts.length > 0) {
33
+ throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
34
+ }
35
+ }
36
+ return z.object({ ...paramsObject?.shape ?? {}, ...inputSchema.shape });
37
+ }
38
+ return paramsObject ? z.intersection(paramsObject, inputSchema) : inputSchema;
39
+ }
18
40
 
19
41
  // src/tools/execute.ts
20
- async function executeToolMethod(method, toolName, rawArgs, context, hooks) {
42
+ function toolResultFromError(err) {
43
+ const appErr = normalizeError(err);
44
+ return {
45
+ ok: false,
46
+ code: appErr.code,
47
+ details: appErr.details ?? { message: appErr.message },
48
+ ...appErr.hint && { hint: appErr.hint }
49
+ };
50
+ }
51
+ async function executeToolMethod(method, toolName, rawArgs, context, hooks, lifecycle) {
21
52
  const startedAt = Date.now();
22
53
  const finish = async (result) => {
23
54
  await hooks?.afterToolCall?.(toolName, rawArgs, result, Date.now() - startedAt, context);
@@ -26,9 +57,18 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks) {
26
57
  if (hooks?.beforeToolCall) {
27
58
  await hooks.beforeToolCall(toolName, rawArgs, context);
28
59
  }
60
+ const paramKeys = new Set(objectShapeKeys(method.paramsSchema));
61
+ const paramArgs = {};
62
+ const inputArgs = {};
63
+ for (const [key, value] of Object.entries(rawArgs)) {
64
+ if (paramKeys.has(key))
65
+ paramArgs[key] = value;
66
+ else
67
+ inputArgs[key] = value;
68
+ }
29
69
  let params;
30
70
  if (method.paramsSchema) {
31
- const result = method.paramsSchema.safeParse(rawArgs);
71
+ const result = method.paramsSchema.safeParse(paramArgs);
32
72
  if (!result.success) {
33
73
  return finish({
34
74
  ok: false,
@@ -40,7 +80,7 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks) {
40
80
  }
41
81
  let input;
42
82
  if (method.inputSchema) {
43
- const result = method.inputSchema.safeParse(rawArgs);
83
+ const result = method.inputSchema.safeParse(inputArgs);
44
84
  if (!result.success) {
45
85
  return finish({
46
86
  ok: false,
@@ -51,120 +91,101 @@ async function executeToolMethod(method, toolName, rawArgs, context, hooks) {
51
91
  input = result.data;
52
92
  }
53
93
  try {
54
- const ctx = { params, input, ...context };
55
- const data = await method.handler(ctx);
94
+ const ctx = { ...context, params, input, source: context.source };
95
+ if (lifecycle?.beforeHandle) {
96
+ await lifecycle.beforeHandle(ctx, method);
97
+ }
98
+ let data = await method.handler(ctx);
99
+ if (lifecycle?.afterHandle) {
100
+ const transformed = await lifecycle.afterHandle(ctx, data, method);
101
+ if (transformed !== undefined)
102
+ data = transformed;
103
+ }
104
+ if (method.outputSchema) {
105
+ const parsed = method.outputSchema.safeParse(data);
106
+ if (!parsed.success) {
107
+ return finish({
108
+ ok: false,
109
+ code: "INTERNAL_SERVER_ERROR",
110
+ details: {
111
+ message: `Handler output does not match the contract: ${formatZodError(parsed.error)}`
112
+ }
113
+ });
114
+ }
115
+ data = parsed.data;
116
+ }
56
117
  const output = data === undefined || data === null ? { status: "ok" } : data;
57
118
  return finish({ ok: true, data: output });
58
119
  } catch (err) {
59
- const appErr = normalizeError(err);
60
- return finish({
61
- ok: false,
62
- code: appErr.code,
63
- details: appErr.details ?? { message: appErr.message },
64
- ...appErr.hint && { hint: appErr.hint }
65
- });
120
+ return finish(toolResultFromError(err));
66
121
  }
67
122
  }
68
123
 
69
- // src/tools/names.ts
70
- var SINGULAR_EXCEPTIONS = new Set([
71
- "analytics",
72
- "status",
73
- "stats",
74
- "settings",
75
- "media",
76
- "progress",
77
- "news"
78
- ]);
79
- function singularize(name) {
80
- if (SINGULAR_EXCEPTIONS.has(name))
81
- return name;
82
- if (name.endsWith("ies"))
83
- return `${name.slice(0, -3)}y`;
84
- if (name.endsWith("s") && !name.endsWith("ss"))
85
- return name.slice(0, -1);
86
- return name;
87
- }
88
- function toToolName(serviceName, methodName) {
89
- const normalized = serviceName.replace(/-/g, "_");
90
- const singular = singularize(normalized);
91
- if (methodName === "list")
92
- return `list_${normalized}`;
93
- if (methodName === "get")
94
- return `get_${singular}`;
95
- if (methodName === "create")
96
- return `create_${singular}`;
97
- if (methodName === "update")
98
- return `update_${singular}`;
99
- if (methodName === "delete")
100
- return `delete_${singular}`;
101
- const snake = methodName.replace(/([A-Z])/g, "_$1").toLowerCase();
102
- return `${snake}_${singular}`.replace(/^_/, "");
103
- }
124
+ // src/tools/mount.ts
125
+ import { z as z4 } from "zod";
104
126
 
105
- // src/tools/schema.ts
106
- import { z } from "zod";
127
+ // src/tools/coerce.ts
128
+ import { z as z2 } from "zod";
107
129
  function needsJsonCoercion(field) {
108
- if (field instanceof z.ZodArray || field instanceof z.ZodObject)
130
+ if (field instanceof z2.ZodArray || field instanceof z2.ZodObject)
109
131
  return true;
110
- if (field instanceof z.ZodOptional || field instanceof z.ZodNullable || field instanceof z.ZodDefault) {
132
+ if (field instanceof z2.ZodOptional || field instanceof z2.ZodNullable || field instanceof z2.ZodDefault) {
111
133
  return needsJsonCoercion(field.unwrap());
112
134
  }
113
135
  return false;
114
136
  }
115
137
  var jsonCoerce = (val) => {
116
- if (typeof val === "string") {
117
- try {
118
- return JSON.parse(val);
119
- } catch {
120
- return val;
121
- }
138
+ if (typeof val !== "string")
139
+ return val;
140
+ try {
141
+ return JSON.parse(val);
142
+ } catch {
143
+ return val;
122
144
  }
123
- return val;
124
145
  };
125
146
  function withJsonCoercion(schema) {
126
147
  const shape = schema.shape;
127
148
  const coerced = {};
128
- for (const [key, field] of Object.entries(shape)) {
129
- if (!needsJsonCoercion(field)) {
130
- coerced[key] = field;
149
+ for (const key of Object.keys(shape)) {
150
+ const field = shape[key];
151
+ if (!field || !needsJsonCoercion(field)) {
152
+ if (field)
153
+ coerced[key] = field;
131
154
  continue;
132
155
  }
133
156
  let inner = field;
134
157
  const wrappers = [];
135
- while (inner instanceof z.ZodOptional || inner instanceof z.ZodNullable) {
136
- if (inner instanceof z.ZodOptional) {
137
- wrappers.push("optional");
138
- inner = inner.unwrap();
139
- } else {
140
- wrappers.push("nullable");
141
- inner = inner.unwrap();
142
- }
158
+ while (inner instanceof z2.ZodOptional || inner instanceof z2.ZodNullable) {
159
+ wrappers.push(inner instanceof z2.ZodOptional ? "optional" : "nullable");
160
+ inner = inner.unwrap();
143
161
  }
144
- let result = z.preprocess(jsonCoerce, inner);
162
+ let result = z2.preprocess(jsonCoerce, inner);
145
163
  for (const wrapper of wrappers.reverse()) {
146
- result = wrapper === "optional" ? z.optional(result) : z.nullable(result);
164
+ result = wrapper === "optional" ? z2.optional(result) : z2.nullable(result);
147
165
  }
148
166
  coerced[key] = result;
149
167
  }
150
- return z.object(coerced);
168
+ return z2.object(coerced);
151
169
  }
170
+
171
+ // src/tools/flatten.ts
172
+ import { z as z3 } from "zod";
152
173
  function flattenDiscriminatedUnion(union) {
153
174
  const discriminator = union.def.discriminator;
154
175
  const literalValues = [];
155
176
  const fieldToVariants = new Map;
156
177
  const fieldSchemas = {};
157
178
  for (const opt of union.def.options) {
158
- if (!(opt instanceof z.ZodObject)) {
159
- throw new Error(`flattenDiscriminatedUnion: option for discriminator '${discriminator}' is not a ZodObject`);
179
+ if (!(opt instanceof z3.ZodObject)) {
180
+ throw new Error(`flattenDiscriminatedUnion: variant for discriminator '${discriminator}' is not a ZodObject`);
160
181
  }
161
182
  const discField = opt.shape[discriminator];
162
- if (!(discField instanceof z.ZodLiteral)) {
183
+ if (!(discField instanceof z3.ZodLiteral)) {
163
184
  throw new Error(`flattenDiscriminatedUnion: discriminator field '${discriminator}' must be z.literal()`);
164
185
  }
165
186
  const [literalValue] = discField.def.values;
166
187
  if (typeof literalValue !== "string") {
167
- throw new Error(`flattenDiscriminatedUnion: discriminator literal must be a string (got ${typeof literalValue})`);
188
+ throw new Error("flattenDiscriminatedUnion: discriminator literal must be a string");
168
189
  }
169
190
  literalValues.push(literalValue);
170
191
  for (const [key, field] of Object.entries(opt.shape)) {
@@ -174,7 +195,7 @@ function flattenDiscriminatedUnion(union) {
174
195
  variants.push(literalValue);
175
196
  fieldToVariants.set(key, variants);
176
197
  if (!(key in fieldSchemas)) {
177
- fieldSchemas[key] = field instanceof z.ZodOptional ? field : z.optional(field);
198
+ fieldSchemas[key] = field instanceof z3.ZodOptional ? field : z3.optional(field);
178
199
  }
179
200
  }
180
201
  }
@@ -183,32 +204,65 @@ function flattenDiscriminatedUnion(union) {
183
204
  throw new Error("flattenDiscriminatedUnion: union has no options");
184
205
  }
185
206
  const shape = {
186
- [discriminator]: z.enum([firstLiteral, ...restLiterals])
207
+ [discriminator]: z3.enum([firstLiteral, ...restLiterals])
187
208
  };
188
209
  for (const [key, field] of Object.entries(fieldSchemas)) {
189
210
  const variants = fieldToVariants.get(key) ?? [];
190
211
  const hint = `Required if ${discriminator} = ${variants.join(" | ")}`;
191
- shape[key] = field instanceof z.ZodType ? field.describe(hint) : field;
212
+ shape[key] = field instanceof z3.ZodType ? field.describe(hint) : field;
192
213
  }
193
- return z.object(shape);
214
+ return z3.object(shape);
194
215
  }
195
- function mergeSchemas(paramsSchema, inputSchema) {
196
- const paramsShape = paramsSchema instanceof z.ZodObject ? paramsSchema.shape : {};
197
- const inputShape = inputSchema instanceof z.ZodObject ? inputSchema.shape : inputSchema instanceof z.ZodDiscriminatedUnion ? flattenDiscriminatedUnion(inputSchema).shape : {};
198
- const paramsKeys = Object.keys(paramsShape);
199
- const inputKeys = new Set(Object.keys(inputShape));
200
- const conflicts = paramsKeys.filter((k) => inputKeys.has(k));
201
- if (conflicts.length > 0) {
202
- throw new Error(`Schema merge conflict: ${conflicts.join(", ")} appear in both params and input`);
203
- }
204
- const merged = { ...paramsShape, ...inputShape };
205
- if (Object.keys(merged).length === 0)
206
- return z.object({});
207
- return withJsonCoercion(z.object(merged));
216
+
217
+ // src/tools/names.ts
218
+ var SINGULAR_EXCEPTIONS = new Set([
219
+ "analytics",
220
+ "status",
221
+ "stats",
222
+ "settings",
223
+ "media",
224
+ "progress",
225
+ "news"
226
+ ]);
227
+ function singularize(name) {
228
+ if (SINGULAR_EXCEPTIONS.has(name))
229
+ return name;
230
+ if (name.endsWith("ies"))
231
+ return `${name.slice(0, -3)}y`;
232
+ if (name.endsWith("s") && !name.endsWith("ss"))
233
+ return name.slice(0, -1);
234
+ return name;
235
+ }
236
+ function toToolName(serviceName, methodName) {
237
+ const normalized = serviceName.replace(/-/g, "_");
238
+ const singular = singularize(normalized);
239
+ if (methodName === "list")
240
+ return `list_${normalized}`;
241
+ if (methodName === "get")
242
+ return `get_${singular}`;
243
+ if (methodName === "create")
244
+ return `create_${singular}`;
245
+ if (methodName === "update")
246
+ return `update_${singular}`;
247
+ if (methodName === "delete")
248
+ return `delete_${singular}`;
249
+ const snake = methodName.replace(/([A-Z])/g, "_$1").toLowerCase();
250
+ return `${snake}_${singular}`.replace(/^_/, "");
208
251
  }
209
252
 
210
253
  // src/tools/mount.ts
211
- function collectTools(service, transport, extend) {
254
+ function applyExtend(base, extra) {
255
+ if (base instanceof z4.ZodObject) {
256
+ const conflicts = Object.keys(extra).filter((key) => (key in base.shape));
257
+ if (conflicts.length > 0) {
258
+ throw new Error(`Tool extend conflict: ${conflicts.join(", ")} already declared by the contract`);
259
+ }
260
+ return z4.object({ ...extra, ...base.shape });
261
+ }
262
+ return z4.intersection(z4.object(extra), base);
263
+ }
264
+ function collectTools(service, transport, config = {}) {
265
+ const { extend, coerceJsonArgs = true, flattenUnionInput = false } = config;
212
266
  const tools = [];
213
267
  for (const [methodName, method] of Object.entries(service.methods)) {
214
268
  if (method.expose && !method.expose.includes(transport))
@@ -216,9 +270,15 @@ function collectTools(service, transport, extend) {
216
270
  if (method.multipart)
217
271
  continue;
218
272
  const name = method.toolName ?? toToolName(service.name, methodName);
219
- const baseSchema = mergeSchemas(method.paramsSchema, method.inputSchema);
273
+ let baseSchema = mergeSchemas(method.paramsSchema, method.inputSchema);
274
+ if (flattenUnionInput && baseSchema instanceof z4.ZodDiscriminatedUnion) {
275
+ baseSchema = flattenDiscriminatedUnion(baseSchema);
276
+ }
277
+ if (coerceJsonArgs && baseSchema instanceof z4.ZodObject) {
278
+ baseSchema = withJsonCoercion(baseSchema);
279
+ }
220
280
  const shouldExtend = !!extend && (!extend.filter || extend.filter(service, method));
221
- const schema = shouldExtend ? z2.object({ ...extend?.schema, ...baseSchema.shape }) : baseSchema;
281
+ const schema = shouldExtend && extend ? applyExtend(baseSchema, extend.schema) : baseSchema;
222
282
  tools.push({ method, name, schema, shouldExtend });
223
283
  }
224
284
  return tools;
@@ -231,111 +291,226 @@ function createToolRunner(config) {
231
291
  extraContext = await config.extend.resolve(rawArgs);
232
292
  }
233
293
  const cleanArgs = extendKeys ? Object.fromEntries(Object.entries(rawArgs).filter(([key]) => !extendKeys.has(key))) : rawArgs;
234
- return executeToolMethod(tool.method, tool.name, cleanArgs, { source: config.source, ...config.context, ...extraContext }, config.hooks);
294
+ return executeToolMethod(tool.method, tool.name, cleanArgs, { ...config.context, ...extraContext, source: config.source }, config.hooks, config.lifecycle);
235
295
  };
236
296
  }
237
- function formatToolError(result) {
297
+ function formatToolError(result, toolName, errorHint) {
238
298
  const err = { error: result.code };
239
299
  if (result.details)
240
300
  err.details = result.details;
301
+ const hints = [];
241
302
  if (result.hint)
242
- err._hint = result.hint;
303
+ hints.push(result.hint);
304
+ if (errorHint && toolName) {
305
+ const global = errorHint(toolName, result.code);
306
+ if (global)
307
+ hints.push(global);
308
+ }
309
+ if (hints.length > 0)
310
+ err._hint = hints.join(" ");
243
311
  return err;
244
312
  }
245
313
 
246
314
  // src/tools/agent.ts
247
- function formatAgentResult(result) {
248
- return result.ok ? result.data : formatToolError(result);
249
- }
250
- function mountAgent(service, config = {}) {
315
+ function mountAgent(services, config = {}) {
316
+ const serviceList = Array.isArray(services) ? services : [services];
251
317
  const tools = {};
252
318
  const runTool = createToolRunner({
253
319
  source: "agent",
254
320
  extend: config.extend,
255
321
  context: config.context,
256
- hooks: config.hooks
322
+ hooks: config.hooks,
323
+ lifecycle: config.lifecycle,
324
+ errorHint: config.errorHint
257
325
  });
258
- for (const mountable of collectTools(service, "AGENT", config.extend)) {
259
- tools[mountable.name] = tool({
260
- description: mountable.method.desc,
261
- inputSchema: zodSchema(mountable.schema),
262
- execute: async (rawArgs) => {
263
- try {
264
- const result = await runTool(mountable, rawArgs);
265
- return formatAgentResult(result);
266
- } catch (err) {
267
- const appErr = normalizeError(err);
268
- return { error: appErr.code, details: appErr.details };
269
- }
326
+ for (const service of serviceList) {
327
+ for (const mountable of collectTools(service, "AGENT", {
328
+ extend: config.extend,
329
+ coerceJsonArgs: config.coerceJsonArgs,
330
+ flattenUnionInput: config.flattenUnionInput
331
+ })) {
332
+ if (mountable.name in tools) {
333
+ throw new Error(`Duplicate agent tool name "${mountable.name}" across mounted services`);
270
334
  }
271
- });
335
+ tools[mountable.name] = tool({
336
+ description: mountable.method.desc,
337
+ inputSchema: zodSchema(mountable.schema),
338
+ execute: async (rawArgs) => {
339
+ const args = isRecord(rawArgs) ? rawArgs : {};
340
+ try {
341
+ const result = await runTool(mountable, args);
342
+ if (result.ok)
343
+ return result.data;
344
+ return formatToolError(result, mountable.name, config.errorHint);
345
+ } catch (err) {
346
+ return formatToolError(toolResultFromError(err), mountable.name, config.errorHint);
347
+ }
348
+ }
349
+ });
350
+ }
272
351
  }
273
352
  return tools;
274
353
  }
354
+ // src/tools/json-schema.ts
355
+ import { z as z5 } from "zod";
356
+ function toJsonSchema(schema, io) {
357
+ return z5.toJSONSchema(schema, {
358
+ io,
359
+ target: "draft-2020-12",
360
+ unrepresentable: "throw",
361
+ cycles: "ref"
362
+ });
363
+ }
364
+
365
+ // src/tools/manifest.ts
366
+ function buildToolManifest(tools) {
367
+ return tools.map((t) => ({
368
+ name: t.name,
369
+ description: t.method.desc,
370
+ inputSchema: toJsonSchema(t.schema, "input")
371
+ }));
372
+ }
275
373
  // src/tools/mcp.ts
276
374
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
277
- import { z as z3 } from "zod";
375
+ import { z as z6 } from "zod";
278
376
  function textBlock(text) {
279
377
  return [{ type: "text", text }];
280
378
  }
281
- function formatMcpResult(result, hasOutputSchema) {
379
+ function formatMcpResult(result, mode, toolName, errorHint) {
282
380
  if (result.ok) {
283
381
  const content = textBlock(JSON.stringify(result.data, null, 2));
284
- if (hasOutputSchema && isRecord(result.data)) {
382
+ if (mode === "wrapped") {
383
+ return { content, structuredContent: { result: result.data } };
384
+ }
385
+ if (mode === "direct" && isRecord(result.data)) {
285
386
  return { content, structuredContent: result.data };
286
387
  }
287
388
  return { content };
288
389
  }
289
390
  return {
290
- content: textBlock(JSON.stringify(formatToolError(result), null, 2)),
391
+ content: textBlock(JSON.stringify(formatToolError(result, toolName, errorHint), null, 2)),
291
392
  isError: true
292
393
  };
293
394
  }
395
+ function probeSchema(schema, io) {
396
+ try {
397
+ toJsonSchema(schema, io);
398
+ return null;
399
+ } catch (err) {
400
+ return err instanceof Error ? err.message : String(err);
401
+ }
402
+ }
403
+ function resolveOutputSchema(outputSchema) {
404
+ if (!outputSchema)
405
+ return null;
406
+ if (outputSchema instanceof z6.ZodObject) {
407
+ return { schema: outputSchema, mode: "direct" };
408
+ }
409
+ return { schema: z6.object({ result: outputSchema }), mode: "wrapped" };
410
+ }
411
+ function reportIncompatible(message, policy, logger, failures) {
412
+ if (policy === "throw") {
413
+ failures.push(message);
414
+ } else if (policy === "warn") {
415
+ if (logger)
416
+ logger.warn(`[stitchkit] ${message}`);
417
+ else
418
+ console.warn(`[stitchkit] ${message}`);
419
+ }
420
+ }
421
+ function throwIfFailures(failures) {
422
+ if (failures.length > 0) {
423
+ throw new Error(`[stitchkit] ${failures.length} MCP tool(s) have an incompatible schema:
424
+ - ${failures.join(`
425
+ - `)}`);
426
+ }
427
+ }
428
+ function prepareMcpTool(mountable, policy, logger, failures, seen) {
429
+ if (seen.has(mountable.name)) {
430
+ throw new Error(`Duplicate MCP tool name "${mountable.name}" across mounted services`);
431
+ }
432
+ seen.add(mountable.name);
433
+ if (!(mountable.schema instanceof z6.ZodObject)) {
434
+ reportIncompatible(`MCP tool "${mountable.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, logger, failures);
435
+ return null;
436
+ }
437
+ const inputError = probeSchema(mountable.schema, "input");
438
+ if (inputError) {
439
+ reportIncompatible(`MCP tool "${mountable.name}" — input schema is not JSON Schema-compatible: ${inputError}`, policy, logger, failures);
440
+ return null;
441
+ }
442
+ const resolved = resolveOutputSchema(mountable.method.outputSchema);
443
+ if (!resolved)
444
+ return { outputMode: "none" };
445
+ const outputError = probeSchema(resolved.schema, "output");
446
+ if (outputError) {
447
+ reportIncompatible(`MCP tool "${mountable.name}" — output schema is not JSON Schema-compatible: ${outputError}`, policy, logger, failures);
448
+ return { outputMode: "none" };
449
+ }
450
+ return { outputSchema: resolved.schema, outputMode: resolved.mode };
451
+ }
452
+ function validateMcpSchemas(services, onIncompatibleSchema = "throw", logger) {
453
+ const seen = new Set;
454
+ const failures = [];
455
+ for (const service of services) {
456
+ for (const mountable of collectTools(service, "MCP", undefined)) {
457
+ prepareMcpTool(mountable, onIncompatibleSchema, logger, failures, seen);
458
+ }
459
+ }
460
+ throwIfFailures(failures);
461
+ }
294
462
  function mountMcp(mcpServer, services, config = {}) {
295
463
  const serviceList = Array.isArray(services) ? services : [services];
464
+ const policy = config.onIncompatibleSchema ?? "throw";
296
465
  const runTool = createToolRunner({
297
466
  source: "mcp",
298
467
  extend: config.extend,
299
468
  context: config.context,
300
- hooks: config.hooks
469
+ hooks: config.hooks,
470
+ lifecycle: config.lifecycle,
471
+ errorHint: config.errorHint
301
472
  });
473
+ const seen = new Set;
474
+ const failures = [];
302
475
  for (const service of serviceList) {
303
- for (const mountable of collectTools(service, "MCP", config.extend)) {
304
- try {
305
- z3.toJSONSchema(mountable.schema);
306
- } catch (err) {
307
- console.error(`[stitchkit] SKIPPING MCP tool "${mountable.name}" — schema not JSON Schema-compatible: ${err instanceof Error ? err.message : err}`);
476
+ for (const mountable of collectTools(service, "MCP", {
477
+ extend: config.extend,
478
+ coerceJsonArgs: config.coerceJsonArgs,
479
+ flattenUnionInput: config.flattenUnionInput
480
+ })) {
481
+ const prepared = prepareMcpTool(mountable, policy, config.logger, failures, seen);
482
+ if (!prepared)
308
483
  continue;
309
- }
310
- let outputSchema;
311
- if (mountable.method.outputSchema instanceof z3.ZodObject) {
312
- try {
313
- z3.toJSONSchema(mountable.method.outputSchema);
314
- outputSchema = mountable.method.outputSchema;
315
- } catch {
316
- outputSchema = undefined;
317
- }
318
- }
319
- const toolConfig = { description: mountable.method.desc, inputSchema: mountable.schema.shape };
320
- if (outputSchema)
321
- toolConfig.outputSchema = outputSchema.shape;
484
+ const toolConfig = { description: mountable.method.desc, inputSchema: mountable.schema };
485
+ if (prepared.outputSchema)
486
+ toolConfig.outputSchema = prepared.outputSchema;
322
487
  mcpServer.registerTool(mountable.name, toolConfig, async (rawArgs) => {
488
+ const args = isRecord(rawArgs) ? rawArgs : {};
323
489
  try {
324
- const result = await runTool(mountable, rawArgs);
325
- return formatMcpResult(result, outputSchema !== undefined);
490
+ const result = await runTool(mountable, args);
491
+ return formatMcpResult(result, prepared.outputMode, mountable.name, config.errorHint);
326
492
  } catch (err) {
327
- const appErr = normalizeError(err);
328
- return formatMcpResult({ ok: false, code: appErr.code, details: appErr.details }, false);
493
+ return formatMcpResult(toolResultFromError(err), "none", mountable.name, config.errorHint);
329
494
  }
330
495
  });
331
496
  }
332
497
  }
498
+ throwIfFailures(failures);
333
499
  }
334
500
  function buildMcpServer(config, auth) {
335
501
  const server = new McpServer(config.serverInfo, config.instructions ? { instructions: config.instructions } : undefined);
336
502
  const services = typeof config.services === "function" ? config.services(auth) : config.services;
337
503
  const context = config.context?.(auth);
338
- mountMcp(server, services, { context, hooks: config.hooks });
504
+ mountMcp(server, services, {
505
+ context,
506
+ hooks: config.hooks,
507
+ lifecycle: config.lifecycle,
508
+ onIncompatibleSchema: config.onIncompatibleSchema,
509
+ logger: config.logger,
510
+ coerceJsonArgs: config.coerceJsonArgs,
511
+ flattenUnionInput: config.flattenUnionInput,
512
+ errorHint: config.errorHint
513
+ });
339
514
  config.nativeTools?.(server);
340
515
  return server;
341
516
  }
@@ -382,6 +557,9 @@ function jsonRpcError(code, message, status) {
382
557
  return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status });
383
558
  }
384
559
  function createMcpHandler(config) {
560
+ if (Array.isArray(config.services)) {
561
+ validateMcpSchemas(config.services, config.onIncompatibleSchema, config.logger);
562
+ }
385
563
  const eventStore = new InMemoryEventStore;
386
564
  const sessions = new Map;
387
565
  setInterval(() => {
@@ -481,7 +659,7 @@ import { lookup } from "node:dns/promises";
481
659
  import { readFile, stat } from "node:fs/promises";
482
660
  import { isIP } from "node:net";
483
661
  import { extname, resolve, sep } from "node:path";
484
- import { z as z4 } from "zod";
662
+ import { z as z7 } from "zod";
485
663
  var MAX_INLINE_BYTES = 20 * 1024 * 1024;
486
664
  var EXT_MIME = {
487
665
  ".png": "image/png",
@@ -624,7 +802,7 @@ function mountViewFile(server, options = {}) {
624
802
  server.registerTool("view_file", {
625
803
  description: "View media (image, audio, video) by URL or local path — returns it as content you can SEE / HEAR. Pass several paths to view multiple files at once. Use it on a generation `output` url to inspect the result.",
626
804
  inputSchema: {
627
- paths: z4.union([z4.string(), z4.array(z4.string())]).describe("Media URL(s) or file path(s) to view")
805
+ paths: z7.union([z7.string(), z7.array(z7.string())]).describe("Media URL(s) or file path(s) to view")
628
806
  }
629
807
  }, async (args) => {
630
808
  const list = Array.isArray(args.paths) ? args.paths : [args.paths];
@@ -643,12 +821,17 @@ function mountViewFile(server, options = {}) {
643
821
  });
644
822
  }
645
823
  export {
824
+ withJsonCoercion,
825
+ validateMcpSchemas,
646
826
  resolveMedia,
647
827
  mountViewFile,
648
828
  mountMcp,
649
829
  mountAgent,
650
830
  implementRemote,
831
+ flattenDiscriminatedUnion,
651
832
  createStdioMcpServer,
652
833
  createMcpHandler,
834
+ collectTools,
835
+ buildToolManifest,
653
836
  buildMcpServer
654
837
  };