stitchkit 0.55.0 → 0.56.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/browser/socket-io.d.ts +14 -6
  2. package/dist/browser/socket-io.d.ts.map +1 -1
  3. package/dist/cli.js +4 -4
  4. package/dist/contract/errors.d.ts +7 -0
  5. package/dist/contract/errors.d.ts.map +1 -1
  6. package/dist/contract/index.js +1 -1
  7. package/dist/files/boundary.d.ts +8 -5
  8. package/dist/files/boundary.d.ts.map +1 -1
  9. package/dist/files.js +1 -1
  10. package/dist/{index-rgd18hx3.js → index-28cqssm3.js} +7 -0
  11. package/dist/index-557by2db.js +34 -0
  12. package/dist/index-5r13htq1.js +283 -0
  13. package/dist/index-6y759j86.js +1031 -0
  14. package/dist/{index-npye5kt7.js → index-75njxz7p.js} +69 -7
  15. package/dist/{index-1k16zv57.js → index-bfcpjw20.js} +125 -31
  16. package/dist/{index-0gv7k0ra.js → index-escqg10p.js} +73 -666
  17. package/dist/{index-v4bbq3p4.js → index-j3dem06f.js} +1 -1
  18. package/dist/{index-qx84bqzk.js → index-jcc611vh.js} +1 -1
  19. package/dist/{index-pzc32v75.js → index-sm2tjx06.js} +41 -4
  20. package/dist/{index-js7yexng.js → index-xy8fmh6w.js} +83 -4
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +27 -6
  24. package/dist/node.js +5 -4
  25. package/dist/observability/audit.d.ts +4 -0
  26. package/dist/observability/audit.d.ts.map +1 -1
  27. package/dist/observability/event.d.ts +5 -0
  28. package/dist/observability/event.d.ts.map +1 -1
  29. package/dist/observability/index.js +17 -11
  30. package/dist/remote.js +1 -1
  31. package/dist/server/context-contribution.d.ts +8 -0
  32. package/dist/server/context-contribution.d.ts.map +1 -1
  33. package/dist/server/create.d.ts.map +1 -1
  34. package/dist/server/error-hook.d.ts +1 -1
  35. package/dist/server/error-hook.d.ts.map +1 -1
  36. package/dist/server/index.d.ts +1 -1
  37. package/dist/server/index.d.ts.map +1 -1
  38. package/dist/server/index.js +10 -6
  39. package/dist/server/logger.d.ts.map +1 -1
  40. package/dist/server/middleware/auth.d.ts +29 -2
  41. package/dist/server/middleware/auth.d.ts.map +1 -1
  42. package/dist/server/request-body.d.ts.map +1 -1
  43. package/dist/testing/surface-conformance.d.ts +143 -10
  44. package/dist/testing/surface-conformance.d.ts.map +1 -1
  45. package/dist/testing/surface-manifest.d.ts +132 -33
  46. package/dist/testing/surface-manifest.d.ts.map +1 -1
  47. package/dist/testing.d.ts +2 -2
  48. package/dist/testing.d.ts.map +1 -1
  49. package/dist/testing.js +470 -122
  50. package/dist/tools/async-operation.d.ts +210 -18
  51. package/dist/tools/async-operation.d.ts.map +1 -1
  52. package/dist/tools/define-download-tool.d.ts.map +1 -1
  53. package/dist/tools/define-upload-tool.d.ts.map +1 -1
  54. package/dist/tools/internal/surface-projector.d.ts +101 -0
  55. package/dist/tools/internal/surface-projector.d.ts.map +1 -0
  56. package/dist/tools/managed-file-error.d.ts +6 -0
  57. package/dist/tools/managed-file-error.d.ts.map +1 -0
  58. package/dist/tools/mcp-prepare.d.ts +3 -21
  59. package/dist/tools/mcp-prepare.d.ts.map +1 -1
  60. package/dist/tools/mcp-round-policy.d.ts +9 -0
  61. package/dist/tools/mcp-round-policy.d.ts.map +1 -0
  62. package/dist/tools/mcp-round.d.ts +0 -4
  63. package/dist/tools/mcp-round.d.ts.map +1 -1
  64. package/dist/tools/mount-download.d.ts.map +1 -1
  65. package/dist/tools/mount-upload.d.ts.map +1 -1
  66. package/dist/tools/mount.d.ts +5 -9
  67. package/dist/tools/mount.d.ts.map +1 -1
  68. package/dist/tools/runtime-tool.d.ts.map +1 -1
  69. package/dist/tools/surface.d.ts +7 -0
  70. package/dist/tools/surface.d.ts.map +1 -1
  71. package/dist/tools/view-file.d.ts.map +1 -1
  72. package/dist/tools.d.ts +1 -1
  73. package/dist/tools.d.ts.map +1 -1
  74. package/dist/tools.js +348 -383
  75. package/llms-full.txt +425 -66
  76. package/package.json +1 -1
  77. package/dist/index-h60df7rj.js +0 -110
  78. package/dist/index-k4dftwf7.js +0 -66
package/dist/tools.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  signJwt,
3
3
  verifyPkce
4
- } from "./index-js7yexng.js";
4
+ } from "./index-xy8fmh6w.js";
5
5
  import {
6
6
  DEFAULT_CORS_ALLOW_HEADERS,
7
7
  DEFAULT_PROCESS_SIGNALS,
@@ -9,10 +9,9 @@ import {
9
9
  defaultSignalSource,
10
10
  guardSignalCallback,
11
11
  reportSignalError
12
- } from "./index-pzc32v75.js";
12
+ } from "./index-sm2tjx06.js";
13
13
  import {
14
14
  WaitTimeoutError,
15
- buildToolPresentationSchema,
16
15
  coerceJsonArgs,
17
16
  collectToolSurface,
18
17
  collectTools,
@@ -24,37 +23,42 @@ import {
24
23
  executeToolMethod,
25
24
  fetchGuarded,
26
25
  fetchPinnedDocument,
27
- flattenToolJsonSchema,
28
26
  formatToolError,
29
- isObjectPresentationSchema,
30
- presentationMetadata,
31
27
  readCapped,
32
28
  runWaitOperation,
33
29
  toolErrorFromResult,
34
30
  toolResultFromError
35
- } from "./index-0gv7k0ra.js";
31
+ } from "./index-escqg10p.js";
36
32
  import {
37
33
  ManagedFileError
38
- } from "./index-1k16zv57.js";
34
+ } from "./index-bfcpjw20.js";
39
35
  import"./index-sbdmyz75.js";
40
36
  import {
37
+ PORTABLE_JSON_SCHEMA_FORMATS,
41
38
  assertToolName,
42
- assertUniqueToolName
43
- } from "./index-k4dftwf7.js";
44
- import {
45
- toJsonSchema
46
- } from "./index-cby4ar3v.js";
39
+ assertUniqueToolName,
40
+ findNonPortableFormats,
41
+ findUntypedProperties,
42
+ flattenToolJsonSchema,
43
+ prepareProjectedMcpTools,
44
+ presentationMetadata,
45
+ validateMcpRoundPolicy
46
+ } from "./index-6y759j86.js";
47
+ import"./index-cby4ar3v.js";
47
48
  import {
48
49
  redact
49
- } from "./index-qx84bqzk.js";
50
+ } from "./index-jcc611vh.js";
50
51
  import {
51
52
  getRequestContext,
52
53
  getTraceId,
54
+ normalizeError,
53
55
  runWithRequestContext
54
- } from "./index-v4bbq3p4.js";
56
+ } from "./index-j3dem06f.js";
55
57
  import {
56
- AppError
57
- } from "./index-h60df7rj.js";
58
+ AppError,
59
+ STITCH_ERROR_STATUS,
60
+ defineContract
61
+ } from "./index-5r13htq1.js";
58
62
  import {
59
63
  ManagedFilePathSchema,
60
64
  ManagedFileRefSchema
@@ -328,6 +332,120 @@ function defineAsyncOperation(config) {
328
332
  }
329
333
  };
330
334
  }
335
+ function adapterResult(capability, target, schema, value) {
336
+ const parsed = schema.safeParse(value);
337
+ if (!parsed.success) {
338
+ throw new Error(`Async operation adapter for capability "${capability}" returned invalid ${target}`, { cause: parsed.error });
339
+ }
340
+ return parsed.data;
341
+ }
342
+ var NON_WIRE_STABLE_ZOD_TYPES = new Set([
343
+ "catch",
344
+ "default",
345
+ "lazy",
346
+ "pipe",
347
+ "prefault",
348
+ "readonly",
349
+ "success",
350
+ "transform"
351
+ ]);
352
+ function hasOverwriteCheck(value) {
353
+ return isRecord(value) && isRecord(value._zod) && isRecord(value._zod.def) && value._zod.def.check === "overwrite";
354
+ }
355
+ function assertWireStableIdSchema(schema, boundary) {
356
+ const visited = new Set;
357
+ const pending = [schema];
358
+ while (pending.length > 0) {
359
+ const value = pending.pop();
360
+ if (typeof value !== "object" || value === null || visited.has(value))
361
+ continue;
362
+ visited.add(value);
363
+ if (value instanceof z.ZodType) {
364
+ const definition = value._zod.def;
365
+ if (NON_WIRE_STABLE_ZOD_TYPES.has(definition.type) || "coerce" in definition && definition.coerce === true || "checks" in definition && Array.isArray(definition.checks) && definition.checks.some(hasOverwriteCheck)) {
366
+ throw new Error(`${boundary} must use a wire-stable ID schema without transforms, coercion, defaults or overwrites; use binding: "adapted" with explicit ID adapters`);
367
+ }
368
+ pending.push(...Object.values(definition));
369
+ continue;
370
+ }
371
+ if (Array.isArray(value)) {
372
+ pending.push(...value);
373
+ continue;
374
+ }
375
+ if (isRecord(value))
376
+ pending.push(...Object.values(value));
377
+ }
378
+ }
379
+ function defineAsyncOperationContract(config) {
380
+ assertWireStableIdSchema(config.id, "Async operation contract");
381
+ const hasStartOutput = config.startOutput !== undefined;
382
+ const hasIdFromStart = config.idFromStart !== undefined;
383
+ if (hasStartOutput !== hasIdFromStart) {
384
+ throw new Error("Async operation contract startOutput and idFromStart must be configured together");
385
+ }
386
+ const description = (capability) => config.descriptions?.[capability] ?? `${config.description}: ${capability}`;
387
+ const endpoint = (capability, method, path, input, output) => {
388
+ const scope = config.scopes?.[capability];
389
+ return {
390
+ method,
391
+ path,
392
+ desc: description(capability),
393
+ input,
394
+ output,
395
+ ...scope !== undefined && { scope }
396
+ };
397
+ };
398
+ const startOutput = config.startOutput ?? config.id;
399
+ const endpoints = {
400
+ start: endpoint("start", "POST", "/", config.startInput, startOutput),
401
+ status: endpoint("status", "POST", "/status", config.id, config.snapshot),
402
+ wait: endpoint("wait", "POST", "/wait", config.id, config.snapshot)
403
+ };
404
+ const capabilities = {
405
+ start: "start",
406
+ status: "status",
407
+ wait: "wait"
408
+ };
409
+ const schemas = {
410
+ startInput: config.startInput,
411
+ startOutput,
412
+ id: config.id,
413
+ snapshot: config.snapshot,
414
+ cancelResult: AsyncOperationCancelResultSchema
415
+ };
416
+ const inputFor = {
417
+ status: (id) => adapterResult("status", "input", config.id, id),
418
+ wait: (id) => adapterResult("wait", "input", config.id, id)
419
+ };
420
+ if (config.cancel) {
421
+ endpoints.cancel = endpoint("cancel", "POST", "/cancel", config.id, AsyncOperationCancelResultSchema);
422
+ capabilities.cancel = "cancel";
423
+ schemas.cancel = AsyncOperationCancelResultSchema;
424
+ inputFor.cancel = (id) => adapterResult("cancel", "input", config.id, id);
425
+ }
426
+ if (config.result) {
427
+ endpoints.result = endpoint("result", "POST", "/result", config.id, config.result);
428
+ capabilities.result = "result";
429
+ schemas.result = config.result;
430
+ inputFor.result = (id) => adapterResult("result", "input", config.id, id);
431
+ }
432
+ if (config.artifacts) {
433
+ endpoints.artifacts = endpoint("artifacts", "POST", "/artifacts", config.id, config.artifacts);
434
+ capabilities.artifacts = "artifacts";
435
+ schemas.artifacts = config.artifacts;
436
+ inputFor.artifacts = (id) => adapterResult("artifacts", "input", config.id, id);
437
+ }
438
+ const contract = config.scope ? defineContract({ prefix: config.prefix, scope: config.scope }, endpoints) : defineContract({ prefix: config.prefix }, endpoints);
439
+ const applicationStartOutput = config.startOutput;
440
+ const applicationIdFromStart = config.idFromStart;
441
+ const idFromStart = applicationStartOutput && applicationIdFromStart ? (output) => adapterResult("start", "id", config.id, applicationIdFromStart(output)) : (output) => adapterResult("start", "id", config.id, output);
442
+ return {
443
+ contract,
444
+ capabilities,
445
+ schemas,
446
+ adapters: { idFromStart, inputFor }
447
+ };
448
+ }
331
449
  function bindContractAsyncOperation(config) {
332
450
  const endpoint = (key) => {
333
451
  const found = config.contract.endpoints[key];
@@ -341,27 +459,82 @@ function bindContractAsyncOperation(config) {
341
459
  if (!start.output || !status.output || !wait.output) {
342
460
  throw new Error("Contract async operation requires outputs for start, status and wait");
343
461
  }
344
- if (status.input !== start.output || wait.input !== start.output) {
345
- const capability = status.input !== start.output ? "status" : "wait";
346
- throw new Error(`Contract async operation capability "${capability}" input must reuse the same schema instance as the start output`);
347
- }
348
- if (wait.output !== status.output) {
462
+ const startOutput = start.output;
463
+ const statusOutput = status.output;
464
+ if (wait.output !== statusOutput) {
349
465
  throw new Error('Contract async operation capability "wait" output must reuse the same schema instance as the status output');
350
466
  }
351
- const validateOptional = (capability) => {
352
- const key = config.capabilities[capability];
353
- if (key && endpoint(key).input !== start.output) {
467
+ const optionalCapabilities = [
468
+ "cancel",
469
+ "result",
470
+ "artifacts"
471
+ ];
472
+ if (config.binding !== "adapted") {
473
+ assertWireStableIdSchema(startOutput, "Direct contract async operation");
474
+ if (status.input !== startOutput || wait.input !== startOutput) {
475
+ const capability = status.input !== startOutput ? "status" : "wait";
354
476
  throw new Error(`Contract async operation capability "${capability}" input must reuse the same schema instance as the start output`);
355
477
  }
478
+ const inputFor2 = {
479
+ status: (id) => adapterResult("status", "input", startOutput, id),
480
+ wait: (id) => adapterResult("wait", "input", startOutput, id)
481
+ };
482
+ for (const capability of optionalCapabilities) {
483
+ const key = config.capabilities[capability];
484
+ if (!key)
485
+ continue;
486
+ if (endpoint(key).input !== startOutput) {
487
+ throw new Error(`Contract async operation capability "${capability}" input must reuse the same schema instance as the start output`);
488
+ }
489
+ inputFor2[capability] = (id) => adapterResult(capability, "input", startOutput, id);
490
+ }
491
+ return {
492
+ contract: config.contract,
493
+ handlers: config.handlers,
494
+ capabilities: config.capabilities,
495
+ schemas: { id: startOutput, snapshot: statusOutput },
496
+ adapters: {
497
+ idFromStart: (output) => adapterResult("start", "id", startOutput, output),
498
+ inputFor: inputFor2
499
+ }
500
+ };
501
+ }
502
+ const adapters = config.adapters;
503
+ const idSchema = config.id;
504
+ const adaptedInput = (capability, key, builder) => {
505
+ const input = endpoint(key).input;
506
+ if (!input) {
507
+ throw new Error(`Contract async operation capability "${capability}" requires an input schema`);
508
+ }
509
+ return (id) => adapterResult(capability, "input", input, builder(id));
510
+ };
511
+ const inputFor = {
512
+ status: adaptedInput("status", config.capabilities.status, adapters.inputFor.status),
513
+ wait: adaptedInput("wait", config.capabilities.wait, adapters.inputFor.wait)
514
+ };
515
+ const optionalInput = (capability, key, builder) => {
516
+ if (!key)
517
+ return;
518
+ if (!builder) {
519
+ throw new Error(`Contract async operation capability "${capability}" requires an input adapter`);
520
+ }
521
+ inputFor[capability] = adaptedInput(capability, key, builder);
356
522
  };
357
- validateOptional("cancel");
358
- validateOptional("result");
359
- validateOptional("artifacts");
523
+ optionalInput("cancel", config.capabilities.cancel, "cancel" in adapters.inputFor ? adapters.inputFor.cancel : undefined);
524
+ optionalInput("result", config.capabilities.result, "result" in adapters.inputFor ? adapters.inputFor.result : undefined);
525
+ optionalInput("artifacts", config.capabilities.artifacts, "artifacts" in adapters.inputFor ? adapters.inputFor.artifacts : undefined);
526
+ if (!idSchema) {
527
+ throw new Error("Contract adapted async operation requires an id schema");
528
+ }
360
529
  return {
361
530
  contract: config.contract,
362
531
  handlers: config.handlers,
363
532
  capabilities: config.capabilities,
364
- schemas: { id: start.output, snapshot: status.output }
533
+ schemas: { id: idSchema, snapshot: statusOutput },
534
+ adapters: {
535
+ idFromStart: (output) => adapterResult("start", "id", idSchema, adapters.idFromStart(output)),
536
+ inputFor
537
+ }
365
538
  };
366
539
  }
367
540
  // src/tools/download-core.ts
@@ -430,6 +603,36 @@ async function runDownloadOperation(config) {
430
603
  });
431
604
  }
432
605
 
606
+ // src/tools/managed-file-error.ts
607
+ var SAFE_MANAGED_FILE_MESSAGES = {
608
+ FILE_INVALID_PATH: "Invalid managed-file path",
609
+ FILE_OUTSIDE_ROOT: "Managed-file path escapes its boundary",
610
+ FILE_NOT_FOUND: "Managed file not found",
611
+ FILE_NOT_REGULAR: "Managed path is not a regular file",
612
+ FILE_INSPECTION_REJECTED: "Managed file rejected by inspection",
613
+ FILE_TOO_LARGE: "Managed file exceeds the configured size limit",
614
+ FILE_EXISTS: "Managed file already exists"
615
+ };
616
+ function managedFileAppError(error) {
617
+ if (!(error instanceof ManagedFileError))
618
+ return null;
619
+ switch (error.code) {
620
+ case "FILE_INVALID_PATH":
621
+ case "FILE_OUTSIDE_ROOT":
622
+ case "FILE_NOT_FOUND":
623
+ case "FILE_NOT_REGULAR":
624
+ case "FILE_INSPECTION_REJECTED":
625
+ case "FILE_TOO_LARGE":
626
+ case "FILE_EXISTS":
627
+ return new AppError(error.code, SAFE_MANAGED_FILE_MESSAGES[error.code], STITCH_ERROR_STATUS[error.code]);
628
+ case "FILE_IO_ERROR":
629
+ return null;
630
+ }
631
+ }
632
+ function normalizeFileToolError(error) {
633
+ return managedFileAppError(error) ?? normalizeError(error);
634
+ }
635
+
433
636
  // src/tools/define-download-tool.ts
434
637
  function defineDownloadTool(config) {
435
638
  return defineRuntimeTool({
@@ -460,6 +663,9 @@ function defineDownloadTool(config) {
460
663
  if (error instanceof DownloadOperationError) {
461
664
  throw new AppError(error.code, `Download failed: ${error.message}`, error.status);
462
665
  }
666
+ const managedError = managedFileAppError(error);
667
+ if (managedError)
668
+ throw managedError;
463
669
  throw error;
464
670
  }
465
671
  }
@@ -488,7 +694,16 @@ function defineUploadTool(config) {
488
694
  transports: config.transports,
489
695
  annotations: config.annotations,
490
696
  present: config.present,
491
- handler: async (context) => runUploadOperation(config.files, context.input.path, (source) => config.upload(source, context), context.signal)
697
+ handler: async (context) => {
698
+ try {
699
+ return await runUploadOperation(config.files, context.input.path, (source) => config.upload(source, context), context.signal);
700
+ } catch (error) {
701
+ const managedError = managedFileAppError(error);
702
+ if (managedError)
703
+ throw managedError;
704
+ throw error;
705
+ }
706
+ }
492
707
  });
493
708
  }
494
709
  // src/tools/view-file.ts
@@ -546,7 +761,7 @@ async function fetchSource(pathOrUrl, options, maxBytes) {
546
761
  signal: options.signal
547
762
  });
548
763
  if (!res.ok)
549
- throw new Error(`HTTP ${res.status}`);
764
+ throw new AppError("VIEW_HTTP_ERROR", `HTTP ${res.status}`, 502);
550
765
  const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
551
766
  const mimeType = headerMime || extMime || "application/octet-stream";
552
767
  if (mimeType.startsWith("video/")) {
@@ -562,11 +777,11 @@ async function fetchSource(pathOrUrl, options, maxBytes) {
562
777
  return buffer ? { buffer, mimeType, bytesRead: buffer.length } : { tooLarge: true, mimeType, bytesRead: maxBytes };
563
778
  }
564
779
  if (!options.files) {
565
- throw new Error("local file paths are disabled — set files to allow them");
780
+ throw new AppError("FILE_INVALID_PATH", "local file paths are disabled — set files to allow them", 400);
566
781
  }
567
782
  options.signal?.throwIfAborted();
568
783
  if (!extMime) {
569
- throw new Error("refusing to read a non-media file");
784
+ throw new AppError("FILE_INSPECTION_REJECTED", "refusing to read a non-media file", 422);
570
785
  }
571
786
  try {
572
787
  const source = await options.files.read(pathOrUrl, {
@@ -672,7 +887,7 @@ async function runViewFileOperation(paths, options = {}, signal) {
672
887
  } catch (error) {
673
888
  if (signal?.aborted)
674
889
  throw signal.reason ?? error;
675
- const message = error instanceof Error ? error.message : String(error);
890
+ const message = normalizeFileToolError(error).message;
676
891
  errors.push({ path: pathOrUrl, message });
677
892
  content.push({ type: "text", text: `[${pathOrUrl}] Error: ${message}` });
678
893
  }
@@ -864,6 +1079,95 @@ function inlineMcpAppBundle(html) {
864
1079
  return html.replace(EXT_APPS_BUNDLE_PLACEHOLDER, () => bundle);
865
1080
  }
866
1081
 
1082
+ // src/tools/mcp-prepare.ts
1083
+ function textBlock(text) {
1084
+ return [{ type: "text", text }];
1085
+ }
1086
+ function formatMcpResult(result, mode, toolName, errorHint) {
1087
+ if (result.ok) {
1088
+ if (mode === "none")
1089
+ return { content: [] };
1090
+ let serialized;
1091
+ try {
1092
+ serialized = JSON.stringify(result.data, null, 2);
1093
+ } catch {
1094
+ serialized = undefined;
1095
+ }
1096
+ if (serialized === undefined) {
1097
+ const safe = redact(result.data, { sensitiveKeys: /(?!)/ });
1098
+ return { content: textBlock(JSON.stringify(safe, null, 2)), structuredContent: safe };
1099
+ }
1100
+ return { content: textBlock(serialized), structuredContent: result.data };
1101
+ }
1102
+ return {
1103
+ content: textBlock(JSON.stringify(formatToolError(result, toolName, errorHint), null, 2)),
1104
+ isError: true
1105
+ };
1106
+ }
1107
+ function prepareMcpSurface(services, config = {}) {
1108
+ const serviceList = Array.isArray(services) ? services : [services];
1109
+ const tools = serviceList.flatMap((service) => collectTools(service, "MCP", config));
1110
+ return prepareMcpTools(tools, config);
1111
+ }
1112
+ function prepareMcpTools(tools, config = {}) {
1113
+ const prepared = prepareProjectedMcpTools(tools.map((mountable) => ({
1114
+ tool: mountable,
1115
+ name: mountable.name,
1116
+ ...mountable.method.paramsSchema !== undefined && {
1117
+ paramsSchema: mountable.method.paramsSchema
1118
+ },
1119
+ ...mountable.method.inputSchema !== undefined && {
1120
+ inputSchema: mountable.method.inputSchema
1121
+ },
1122
+ ...mountable.method.outputSchema !== undefined && {
1123
+ outputSchema: mountable.method.outputSchema
1124
+ },
1125
+ shouldExtend: mountable.shouldExtend,
1126
+ ...mountable.method.mcp !== undefined && { mcp: mountable.method.mcp }
1127
+ })), config).map((entry) => {
1128
+ Object.freeze(entry.tool);
1129
+ return Object.freeze({
1130
+ mountable: entry.tool,
1131
+ inputSchema: entry.inputSchema,
1132
+ ...entry.outputSchema !== undefined && { outputSchema: entry.outputSchema },
1133
+ outputMode: entry.outputSchema === undefined ? "none" : "direct"
1134
+ });
1135
+ });
1136
+ return Object.freeze(prepared);
1137
+ }
1138
+ function prepareMcpServerSurface(surface, config = {}) {
1139
+ const contractMountables = [];
1140
+ const definitions = [];
1141
+ const runtimeMountables = [];
1142
+ for (const entry of collectToolSurface({
1143
+ surface,
1144
+ transport: "MCP",
1145
+ extend: config.extend,
1146
+ flattenUnionInput: config.flattenUnionInput
1147
+ })) {
1148
+ if (entry.kind === "contract") {
1149
+ contractMountables.push(entry.mountable);
1150
+ } else {
1151
+ definitions.push(entry.definition);
1152
+ runtimeMountables.push(entry.mountable);
1153
+ }
1154
+ }
1155
+ const contractTools = prepareMcpTools(contractMountables, config);
1156
+ const runtimeDescriptors = prepareMcpTools(runtimeMountables, {
1157
+ schemaValidation: config.schemaValidation,
1158
+ logger: config.logger,
1159
+ multiRound: config.multiRound
1160
+ });
1161
+ const descriptorsByName = new Map(runtimeDescriptors.map((descriptor) => [descriptor.mountable.name, descriptor]));
1162
+ const runtimeTools = [];
1163
+ for (const definition of definitions) {
1164
+ const descriptor = descriptorsByName.get(definition.name);
1165
+ if (descriptor)
1166
+ runtimeTools.push(Object.freeze({ definition, descriptor }));
1167
+ }
1168
+ return Object.freeze({ contractTools, runtimeTools: Object.freeze(runtimeTools) });
1169
+ }
1170
+
867
1171
  // src/tools/mcp-round.ts
868
1172
  import {
869
1173
  acceptedContent,
@@ -945,28 +1249,6 @@ async function runRoundFailure(tool2, rawArgs, runTool, context, code, message)
945
1249
  }
946
1250
  }, rawArgs, context);
947
1251
  }
948
- function validateMcpRoundPolicy(tool2, policy, runtime) {
949
- if (!runtime?.stateConfigured) {
950
- throw new Error(`[stitchkit] MCP tool "${tool2.name}" declares inputRequired but no multiRound.state key is configured`);
951
- }
952
- const { maxRounds } = runtime;
953
- if (!Number.isInteger(maxRounds) || maxRounds < 1) {
954
- throw new Error("[stitchkit] multiRound.serving.maxRounds must be a positive integer");
955
- }
956
- if (policy.inputRequired.length === 0) {
957
- throw new Error(`[stitchkit] MCP tool "${tool2.name}" must declare at least one input round`);
958
- }
959
- if (policy.inputRequired.length > maxRounds) {
960
- throw new Error(`[stitchkit] MCP tool "${tool2.name}" declares ${policy.inputRequired.length} input rounds, exceeding maxRounds ${maxRounds}`);
961
- }
962
- const keys = new Set;
963
- for (const request of policy.inputRequired) {
964
- if (keys.has(request.key)) {
965
- throw new Error(`[stitchkit] MCP tool "${tool2.name}" declares duplicate input key "${request.key}"`);
966
- }
967
- keys.add(request.key);
968
- }
969
- }
970
1252
  async function failedResolution(options) {
971
1253
  const result = await runRoundFailure(options.tool, options.rawArgs, options.runTool, options.context, options.code, options.message);
972
1254
  return { kind: "response", response: options.formatFailure(result) };
@@ -1099,329 +1381,6 @@ async function resolveMcpRound(options) {
1099
1381
  };
1100
1382
  }
1101
1383
 
1102
- // src/tools/portable-formats.ts
1103
- var PORTABLE_JSON_SCHEMA_FORMATS = new Set([
1104
- "date",
1105
- "time",
1106
- "date-time",
1107
- "duration",
1108
- "uri",
1109
- "uri-reference",
1110
- "uri-template",
1111
- "email",
1112
- "hostname",
1113
- "ipv4",
1114
- "ipv6",
1115
- "regex",
1116
- "uuid",
1117
- "json-pointer",
1118
- "relative-json-pointer"
1119
- ]);
1120
- function propertyPath(prefix, property) {
1121
- return prefix ? `${prefix}.${property}` : property;
1122
- }
1123
- function keywordPath(prefix, keyword) {
1124
- if (prefix && keyword.startsWith("["))
1125
- return `${prefix}${keyword}`;
1126
- return prefix ? `${prefix}.${keyword}` : keyword;
1127
- }
1128
- function findNonPortableFormats(schema, allowFormats = []) {
1129
- const allowed = new Set([...PORTABLE_JSON_SCHEMA_FORMATS, ...allowFormats]);
1130
- const found = [];
1131
- const pending = [{ value: schema, path: "" }];
1132
- while (pending.length > 0) {
1133
- const current = pending.pop();
1134
- if (!current || !isRecord(current.value))
1135
- continue;
1136
- const format = current.value.format;
1137
- if (typeof format === "string" && !allowed.has(format)) {
1138
- found.push({ path: current.path || "(root)", format });
1139
- }
1140
- const properties = current.value.properties;
1141
- if (isRecord(properties)) {
1142
- for (const [key, value] of Object.entries(properties)) {
1143
- pending.push({ value, path: propertyPath(current.path, key) });
1144
- }
1145
- }
1146
- for (const mapKey of ["$defs", "definitions", "patternProperties", "dependentSchemas"]) {
1147
- const entries = current.value[mapKey];
1148
- if (!isRecord(entries))
1149
- continue;
1150
- for (const [key, value] of Object.entries(entries)) {
1151
- pending.push({ value, path: keywordPath(current.path, `${mapKey}.${key}`) });
1152
- }
1153
- }
1154
- const directChildren = [
1155
- ["items", "[]"],
1156
- ["contains", "contains"],
1157
- ["additionalProperties", "*"],
1158
- ["unevaluatedProperties", "*"],
1159
- ["propertyNames", "propertyNames"],
1160
- ["not", "not"],
1161
- ["if", "if"],
1162
- ["then", "then"],
1163
- ["else", "else"]
1164
- ];
1165
- for (const [keyword, segment] of directChildren) {
1166
- const child = current.value[keyword];
1167
- if (isRecord(child)) {
1168
- pending.push({ value: child, path: keywordPath(current.path, segment) });
1169
- }
1170
- if (Array.isArray(child)) {
1171
- child.forEach((value, index) => {
1172
- pending.push({ value, path: keywordPath(current.path, `${segment}[${index}]`) });
1173
- });
1174
- }
1175
- }
1176
- for (const keyword of ["prefixItems", "allOf", "anyOf", "oneOf"]) {
1177
- const branches = current.value[keyword];
1178
- if (!Array.isArray(branches))
1179
- continue;
1180
- branches.forEach((value, index) => {
1181
- pending.push({ value, path: keywordPath(current.path, `${keyword}[${index}]`) });
1182
- });
1183
- }
1184
- }
1185
- return found.sort((left, right) => left.path.localeCompare(right.path) || left.format.localeCompare(right.format));
1186
- }
1187
-
1188
- // src/tools/untyped-properties.ts
1189
- var TYPE_KEYWORDS = ["type", "enum", "const", "anyOf", "oneOf", "allOf", "$ref", "not"];
1190
- function saysWhatItIs(schema) {
1191
- return TYPE_KEYWORDS.some((keyword) => schema[keyword] !== undefined);
1192
- }
1193
- function findUntypedProperties(schema, prefix = "") {
1194
- if (!isRecord(schema))
1195
- return [];
1196
- const found = [];
1197
- const properties = schema.properties;
1198
- if (isRecord(properties)) {
1199
- for (const [key, value] of Object.entries(properties)) {
1200
- const path = prefix ? `${prefix}.${key}` : key;
1201
- if (isRecord(value)) {
1202
- if (!saysWhatItIs(value)) {
1203
- const description = value.description;
1204
- found.push({
1205
- path,
1206
- ...typeof description === "string" && { description }
1207
- });
1208
- }
1209
- found.push(...findUntypedProperties(value, path));
1210
- }
1211
- }
1212
- }
1213
- for (const key of ["items", "additionalProperties"]) {
1214
- const child = schema[key];
1215
- if (isRecord(child))
1216
- found.push(...findUntypedProperties(child, prefix));
1217
- if (Array.isArray(child)) {
1218
- for (const entry of child) {
1219
- if (isRecord(entry))
1220
- found.push(...findUntypedProperties(entry, prefix));
1221
- }
1222
- }
1223
- }
1224
- for (const key of ["prefixItems"]) {
1225
- const child = schema[key];
1226
- if (Array.isArray(child)) {
1227
- for (const entry of child) {
1228
- if (isRecord(entry))
1229
- found.push(...findUntypedProperties(entry, prefix));
1230
- }
1231
- }
1232
- }
1233
- const patterned = schema.patternProperties;
1234
- if (isRecord(patterned)) {
1235
- for (const value of Object.values(patterned)) {
1236
- if (isRecord(value))
1237
- found.push(...findUntypedProperties(value, prefix));
1238
- }
1239
- }
1240
- for (const key of ["$defs", "definitions"]) {
1241
- const container = schema[key];
1242
- if (!isRecord(container))
1243
- continue;
1244
- for (const definition of Object.values(container)) {
1245
- if (isRecord(definition))
1246
- found.push(...findUntypedProperties(definition, prefix));
1247
- }
1248
- }
1249
- for (const key of ["allOf", "anyOf", "oneOf"]) {
1250
- const branches = schema[key];
1251
- if (Array.isArray(branches)) {
1252
- for (const branch of branches)
1253
- found.push(...findUntypedProperties(branch, prefix));
1254
- }
1255
- }
1256
- return found;
1257
- }
1258
-
1259
- // src/tools/mcp-prepare.ts
1260
- function textBlock(text) {
1261
- return [{ type: "text", text }];
1262
- }
1263
- function formatMcpResult(result, mode, toolName, errorHint) {
1264
- if (result.ok) {
1265
- if (mode === "none")
1266
- return { content: [] };
1267
- let serialized;
1268
- try {
1269
- serialized = JSON.stringify(result.data, null, 2);
1270
- } catch {
1271
- serialized = undefined;
1272
- }
1273
- if (serialized === undefined) {
1274
- const safe = redact(result.data, { sensitiveKeys: /(?!)/ });
1275
- return { content: textBlock(JSON.stringify(safe, null, 2)), structuredContent: safe };
1276
- }
1277
- return { content: textBlock(serialized), structuredContent: result.data };
1278
- }
1279
- return {
1280
- content: textBlock(JSON.stringify(formatToolError(result, toolName, errorHint), null, 2)),
1281
- isError: true
1282
- };
1283
- }
1284
- function probeSchema(schema, io) {
1285
- try {
1286
- toJsonSchema(schema, io);
1287
- return null;
1288
- } catch (err) {
1289
- return err instanceof Error ? err.message : String(err);
1290
- }
1291
- }
1292
- function resolveOutputSchema(outputSchema) {
1293
- if (!outputSchema)
1294
- return null;
1295
- return { schema: outputSchema, mode: "direct" };
1296
- }
1297
- function reportIncompatible(message, policy, logger, failures) {
1298
- if (policy === "throw") {
1299
- failures.push(message);
1300
- } else if (policy === "warn") {
1301
- if (logger)
1302
- logger.warn(`[stitchkit] ${message}`);
1303
- else
1304
- console.warn(`[stitchkit] ${message}`);
1305
- }
1306
- }
1307
- function throwIfFailures(failures) {
1308
- if (failures.length > 0) {
1309
- throw new Error(`[stitchkit] ${failures.length} problem(s) with MCP tool schemas:
1310
- - ${failures.join(`
1311
- - `)}`);
1312
- }
1313
- }
1314
- function prepareMcpTool(mountable, config, validation, logger, failures, seen) {
1315
- const policy = validation.policy ?? "throw";
1316
- assertUniqueToolName(mountable.name, seen.has(mountable.name), "MCP tool name");
1317
- seen.add(mountable.name);
1318
- if (mountable.method.mcp) {
1319
- validateMcpRoundPolicy(mountable, mountable.method.mcp, config.multiRound);
1320
- }
1321
- let inputJsonSchema;
1322
- try {
1323
- inputJsonSchema = buildToolPresentationSchema({
1324
- paramsSchema: mountable.method.paramsSchema,
1325
- inputSchema: mountable.method.inputSchema,
1326
- extendSchema: mountable.shouldExtend && config.extend ? config.extend.schema : undefined,
1327
- flattenUnionInput: config.flattenUnionInput,
1328
- unrepresentable: "throw"
1329
- });
1330
- } catch (err) {
1331
- reportIncompatible(`MCP tool "${mountable.name}" — input schema is not JSON Schema-compatible: ${err instanceof Error ? err.message : String(err)}`, policy, logger, failures);
1332
- return null;
1333
- }
1334
- if (!isObjectPresentationSchema(inputJsonSchema)) {
1335
- 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);
1336
- return null;
1337
- }
1338
- if (validation.requireTypedProperties) {
1339
- const allowed = new Set(validation.allowUntyped ?? []);
1340
- for (const untyped of findUntypedProperties(inputJsonSchema)) {
1341
- const path = `${mountable.name}.${untyped.path}`;
1342
- if (allowed.has(path))
1343
- continue;
1344
- const clue = untyped.description ? ` (only a description: "${untyped.description}")` : "";
1345
- reportIncompatible(`MCP tool "${mountable.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, logger, failures);
1346
- }
1347
- }
1348
- if (validation.requirePortableFormats) {
1349
- for (const finding of findNonPortableFormats(inputJsonSchema, validation.allowFormats)) {
1350
- reportIncompatible(`MCP tool "${mountable.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, logger, failures);
1351
- }
1352
- }
1353
- const resolved = resolveOutputSchema(mountable.method.outputSchema);
1354
- if (!resolved)
1355
- return { mountable, inputSchema: inputJsonSchema, outputMode: "none" };
1356
- const outputError = probeSchema(resolved.schema, "output");
1357
- if (outputError) {
1358
- reportIncompatible(`MCP tool "${mountable.name}" — output schema is not JSON Schema-compatible: ${outputError}`, policy, logger, failures);
1359
- return { mountable, inputSchema: inputJsonSchema, outputMode: "none" };
1360
- }
1361
- if (validation.requirePortableFormats) {
1362
- for (const finding of findNonPortableFormats(toJsonSchema(resolved.schema, "output"), validation.allowFormats)) {
1363
- reportIncompatible(`MCP tool "${mountable.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, logger, failures);
1364
- }
1365
- }
1366
- return {
1367
- mountable,
1368
- inputSchema: inputJsonSchema,
1369
- outputSchema: resolved.schema,
1370
- outputMode: resolved.mode
1371
- };
1372
- }
1373
- function prepareMcpSurface(services, config = {}) {
1374
- const serviceList = Array.isArray(services) ? services : [services];
1375
- const tools = serviceList.flatMap((service) => collectTools(service, "MCP", config));
1376
- return prepareMcpTools(tools, config);
1377
- }
1378
- function prepareMcpTools(tools, config = {}) {
1379
- const seen = new Set;
1380
- const failures = [];
1381
- const prepared = [];
1382
- for (const mountable of tools) {
1383
- const tool2 = prepareMcpTool(mountable, config, config.schemaValidation ?? {}, config.logger, failures, seen);
1384
- if (tool2) {
1385
- Object.freeze(tool2.mountable);
1386
- prepared.push(Object.freeze(tool2));
1387
- }
1388
- }
1389
- throwIfFailures(failures);
1390
- return Object.freeze(prepared);
1391
- }
1392
- function prepareMcpServerSurface(surface, config = {}) {
1393
- const contractMountables = [];
1394
- const definitions = [];
1395
- const runtimeMountables = [];
1396
- for (const entry of collectToolSurface({
1397
- surface,
1398
- transport: "MCP",
1399
- extend: config.extend,
1400
- flattenUnionInput: config.flattenUnionInput
1401
- })) {
1402
- if (entry.kind === "contract") {
1403
- contractMountables.push(entry.mountable);
1404
- } else {
1405
- definitions.push(entry.definition);
1406
- runtimeMountables.push(entry.mountable);
1407
- }
1408
- }
1409
- const contractTools = prepareMcpTools(contractMountables, config);
1410
- const runtimeDescriptors = prepareMcpTools(runtimeMountables, {
1411
- schemaValidation: config.schemaValidation,
1412
- logger: config.logger,
1413
- multiRound: config.multiRound
1414
- });
1415
- const descriptorsByName = new Map(runtimeDescriptors.map((descriptor) => [descriptor.mountable.name, descriptor]));
1416
- const runtimeTools = [];
1417
- for (const definition of definitions) {
1418
- const descriptor = descriptorsByName.get(definition.name);
1419
- if (descriptor)
1420
- runtimeTools.push(Object.freeze({ definition, descriptor }));
1421
- }
1422
- return Object.freeze({ contractTools, runtimeTools: Object.freeze(runtimeTools) });
1423
- }
1424
-
1425
1384
  // src/tools/mcp-trace.ts
1426
1385
  import {
1427
1386
  BAGGAGE_META_KEY,
@@ -2015,8 +1974,12 @@ function mountDownload(server, config) {
2015
1974
  timeoutMs: config.timeoutMs
2016
1975
  });
2017
1976
  return textResult(JSON.stringify(result, null, 2));
2018
- } catch (err) {
2019
- return textResult(`Download failed: ${err instanceof Error ? err.message : String(err)}`, true);
1977
+ } catch (error) {
1978
+ if (error instanceof DownloadOperationError) {
1979
+ return textResult(`Download failed [${error.code}]: ${error.message}`, true);
1980
+ }
1981
+ const normalized = normalizeFileToolError(error);
1982
+ return textResult(`Download failed [${normalized.code}]: ${normalized.message}`, true);
2020
1983
  }
2021
1984
  });
2022
1985
  }
@@ -2037,8 +2000,9 @@ function mountUpload(server, config) {
2037
2000
  try {
2038
2001
  const uploaded = await runUploadOperation(config.files, path, config.upload);
2039
2002
  return textResult(JSON.stringify(uploaded ?? null, null, 2));
2040
- } catch (err) {
2041
- return textResult(`Upload failed: ${err instanceof Error ? err.message : String(err)}`, true);
2003
+ } catch (error) {
2004
+ const normalized = normalizeFileToolError(error);
2005
+ return textResult(`Upload failed [${normalized.code}]: ${normalized.message}`, true);
2042
2006
  }
2043
2007
  });
2044
2008
  }
@@ -2840,6 +2804,7 @@ export {
2840
2804
  createToolLogger,
2841
2805
  createToolkit,
2842
2806
  defineAsyncOperation,
2807
+ defineAsyncOperationContract,
2843
2808
  defineCliCommand,
2844
2809
  defineDownloadTool,
2845
2810
  defineRuntimeTool,