stitchkit 0.53.2 → 0.55.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 (125) hide show
  1. package/README.md +4 -1
  2. package/dist/browser/socket-io.d.ts +11 -1
  3. package/dist/browser/socket-io.d.ts.map +1 -1
  4. package/dist/cli.d.ts +2 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +15 -8
  7. package/dist/contract/define.d.ts +27 -0
  8. package/dist/contract/define.d.ts.map +1 -1
  9. package/dist/contract/file-ref.d.ts +13 -0
  10. package/dist/contract/file-ref.d.ts.map +1 -0
  11. package/dist/contract/index.d.ts +2 -1
  12. package/dist/contract/index.d.ts.map +1 -1
  13. package/dist/contract/index.js +21 -17
  14. package/dist/files/boundary.d.ts +52 -0
  15. package/dist/files/boundary.d.ts.map +1 -0
  16. package/dist/files.d.ts +3 -0
  17. package/dist/files.d.ts.map +1 -0
  18. package/dist/files.js +16 -0
  19. package/dist/{index-c78w2y09.js → index-0gv7k0ra.js} +515 -156
  20. package/dist/index-1bx83sw4.js +4 -0
  21. package/dist/index-1k16zv57.js +269 -0
  22. package/dist/index-6k1937bx.js +20 -0
  23. package/dist/{index-bmnhqtya.js → index-7eq9va6e.js} +1 -4
  24. package/dist/{index-yxpe3phd.js → index-cby4ar3v.js} +1 -18
  25. package/dist/index-h60df7rj.js +110 -0
  26. package/dist/{index-yez419px.js → index-js7yexng.js} +19 -6
  27. package/dist/index-k4dftwf7.js +66 -0
  28. package/dist/{index-pdv1mjjr.js → index-npye5kt7.js} +30 -59
  29. package/dist/{index-yydqk4fh.js → index-pzc32v75.js} +90 -3
  30. package/dist/{index-e1es808r.js → index-qh4xevf5.js} +1 -1
  31. package/dist/{index-gvha32jh.js → index-qx84bqzk.js} +2 -2
  32. package/dist/{index-pfqjb5xy.js → index-rgd18hx3.js} +23 -5
  33. package/dist/index-sbdmyz75.js +18 -0
  34. package/dist/{index-173tvqcp.js → index-v4bbq3p4.js} +9 -114
  35. package/dist/index.js +162 -35
  36. package/dist/node.js +30 -27
  37. package/dist/observability/event.d.ts +2 -16
  38. package/dist/observability/event.d.ts.map +1 -1
  39. package/dist/observability/index.js +28 -25
  40. package/dist/react.js +2 -2
  41. package/dist/realtime/contract.d.ts +9 -0
  42. package/dist/realtime/contract.d.ts.map +1 -1
  43. package/dist/realtime/index.d.ts +2 -1
  44. package/dist/realtime/index.d.ts.map +1 -1
  45. package/dist/realtime/request.d.ts +21 -0
  46. package/dist/realtime/request.d.ts.map +1 -0
  47. package/dist/realtime/socket.d.ts +3 -0
  48. package/dist/realtime/socket.d.ts.map +1 -1
  49. package/dist/remote.d.ts +9 -0
  50. package/dist/remote.d.ts.map +1 -0
  51. package/dist/remote.js +79 -0
  52. package/dist/server/context-contribution.d.ts +10 -0
  53. package/dist/server/context-contribution.d.ts.map +1 -0
  54. package/dist/server/context.d.ts.map +1 -1
  55. package/dist/server/index.d.ts +2 -1
  56. package/dist/server/index.d.ts.map +1 -1
  57. package/dist/server/index.js +133 -88
  58. package/dist/server/lifecycle.d.ts +4 -0
  59. package/dist/server/lifecycle.d.ts.map +1 -0
  60. package/dist/server/middleware/auth.d.ts +35 -7
  61. package/dist/server/middleware/auth.d.ts.map +1 -1
  62. package/dist/server/process-signal-common.d.ts +6 -0
  63. package/dist/server/process-signal-common.d.ts.map +1 -0
  64. package/dist/server/process-signals.d.ts.map +1 -1
  65. package/dist/server/types.d.ts +1 -1
  66. package/dist/server/types.d.ts.map +1 -1
  67. package/dist/testing/surface-conformance.d.ts +47 -0
  68. package/dist/testing/surface-conformance.d.ts.map +1 -0
  69. package/dist/testing/surface-manifest.d.ts +156 -0
  70. package/dist/testing/surface-manifest.d.ts.map +1 -0
  71. package/dist/testing.d.ts +2 -0
  72. package/dist/testing.d.ts.map +1 -1
  73. package/dist/testing.js +366 -3
  74. package/dist/tools/agent.d.ts.map +1 -1
  75. package/dist/tools/async-operation.d.ts +168 -0
  76. package/dist/tools/async-operation.d.ts.map +1 -0
  77. package/dist/tools/cli-args.d.ts +1 -1
  78. package/dist/tools/cli-args.d.ts.map +1 -1
  79. package/dist/tools/cli-command.d.ts +29 -0
  80. package/dist/tools/cli-command.d.ts.map +1 -0
  81. package/dist/tools/cli-format.d.ts +2 -2
  82. package/dist/tools/cli-format.d.ts.map +1 -1
  83. package/dist/tools/cli-wait.d.ts +11 -2
  84. package/dist/tools/cli-wait.d.ts.map +1 -1
  85. package/dist/tools/cli.d.ts +13 -6
  86. package/dist/tools/cli.d.ts.map +1 -1
  87. package/dist/tools/define-download-tool.d.ts +18 -0
  88. package/dist/tools/define-download-tool.d.ts.map +1 -0
  89. package/dist/tools/define-upload-tool.d.ts +16 -0
  90. package/dist/tools/define-upload-tool.d.ts.map +1 -0
  91. package/dist/tools/define-view-file-tool.d.ts +10 -0
  92. package/dist/tools/define-view-file-tool.d.ts.map +1 -0
  93. package/dist/tools/define-wait-tool.d.ts +22 -0
  94. package/dist/tools/define-wait-tool.d.ts.map +1 -0
  95. package/dist/tools/download-core.d.ts +20 -0
  96. package/dist/tools/download-core.d.ts.map +1 -0
  97. package/dist/tools/execute.d.ts +3 -1
  98. package/dist/tools/execute.d.ts.map +1 -1
  99. package/dist/tools/lifecycle.d.ts +4 -0
  100. package/dist/tools/lifecycle.d.ts.map +1 -0
  101. package/dist/tools/mcp-round.d.ts +0 -1
  102. package/dist/tools/mcp-round.d.ts.map +1 -1
  103. package/dist/tools/mcp-stdio-signals.d.ts +33 -0
  104. package/dist/tools/mcp-stdio-signals.d.ts.map +1 -0
  105. package/dist/tools/mount-download.d.ts +10 -4
  106. package/dist/tools/mount-download.d.ts.map +1 -1
  107. package/dist/tools/mount-upload.d.ts +4 -2
  108. package/dist/tools/mount-upload.d.ts.map +1 -1
  109. package/dist/tools/mount-wait.d.ts.map +1 -1
  110. package/dist/tools/native-definition.d.ts +15 -0
  111. package/dist/tools/native-definition.d.ts.map +1 -0
  112. package/dist/tools/runtime-tool.d.ts +5 -4
  113. package/dist/tools/runtime-tool.d.ts.map +1 -1
  114. package/dist/tools/surface.d.ts.map +1 -1
  115. package/dist/tools/upload-core.d.ts +4 -0
  116. package/dist/tools/upload-core.d.ts.map +1 -0
  117. package/dist/tools/view-file.d.ts +84 -23
  118. package/dist/tools/view-file.d.ts.map +1 -1
  119. package/dist/tools/wait-core.d.ts +16 -2
  120. package/dist/tools/wait-core.d.ts.map +1 -1
  121. package/dist/tools.d.ts +12 -3
  122. package/dist/tools.d.ts.map +1 -1
  123. package/dist/tools.js +827 -495
  124. package/llms-full.txt +720 -53
  125. package/package.json +10 -2
package/dist/tools.js CHANGED
@@ -1,201 +1,72 @@
1
1
  import {
2
2
  signJwt,
3
3
  verifyPkce
4
- } from "./index-yez419px.js";
4
+ } from "./index-js7yexng.js";
5
5
  import {
6
6
  DEFAULT_CORS_ALLOW_HEADERS,
7
- contractOnlyService
8
- } from "./index-yydqk4fh.js";
7
+ DEFAULT_PROCESS_SIGNALS,
8
+ contractOnlyService,
9
+ defaultSignalSource,
10
+ guardSignalCallback,
11
+ reportSignalError
12
+ } from "./index-pzc32v75.js";
9
13
  import {
10
- assertToolName,
11
- assertUniqueToolName,
14
+ WaitTimeoutError,
12
15
  buildToolPresentationSchema,
13
16
  coerceJsonArgs,
17
+ collectToolSurface,
14
18
  collectTools,
15
19
  createCli,
20
+ createRuntimeToolFactory,
16
21
  createToolRunner,
22
+ defineCliCommand,
23
+ defineRuntimeTool,
17
24
  executeToolMethod,
18
25
  fetchGuarded,
19
26
  fetchPinnedDocument,
20
27
  flattenToolJsonSchema,
21
28
  formatToolError,
22
29
  isObjectPresentationSchema,
23
- pollUntil,
24
30
  presentationMetadata,
25
31
  readCapped,
32
+ runWaitOperation,
26
33
  toolErrorFromResult,
27
- toolResultFromError,
28
- writeDownload
29
- } from "./index-c78w2y09.js";
34
+ toolResultFromError
35
+ } from "./index-0gv7k0ra.js";
30
36
  import {
31
- isWithinDir,
32
- realPathWithinDir,
33
- toJsonSchema
34
- } from "./index-yxpe3phd.js";
37
+ ManagedFileError
38
+ } from "./index-1k16zv57.js";
39
+ import"./index-sbdmyz75.js";
35
40
  import {
36
- ApiError,
37
- createClient
38
- } from "./index-e1es808r.js";
39
- import"./index-48ffdxgk.js";
41
+ assertToolName,
42
+ assertUniqueToolName
43
+ } from "./index-k4dftwf7.js";
44
+ import {
45
+ toJsonSchema
46
+ } from "./index-cby4ar3v.js";
40
47
  import {
41
48
  redact
42
- } from "./index-gvha32jh.js";
49
+ } from "./index-qx84bqzk.js";
43
50
  import {
44
- AppError,
45
51
  getRequestContext,
46
52
  getTraceId,
47
- mergeMeta,
48
53
  runWithRequestContext
49
- } from "./index-173tvqcp.js";
54
+ } from "./index-v4bbq3p4.js";
55
+ import {
56
+ AppError
57
+ } from "./index-h60df7rj.js";
58
+ import {
59
+ ManagedFilePathSchema,
60
+ ManagedFileRefSchema
61
+ } from "./index-6k1937bx.js";
50
62
  import {
51
63
  isRecord,
52
64
  resolvePropagationContext
53
- } from "./index-bmnhqtya.js";
65
+ } from "./index-7eq9va6e.js";
66
+ import"./index-1bx83sw4.js";
54
67
 
55
68
  // src/tools/agent.ts
56
69
  import { jsonSchema, tool } from "ai";
57
-
58
- // src/tools/runtime-tool.ts
59
- function defineRuntimeTool(definition) {
60
- if (definition.transports?.length === 0) {
61
- throw new Error(`Runtime tool "${definition.name}" must expose at least one transport`);
62
- }
63
- return definition;
64
- }
65
- function createRuntimeToolFactory(config) {
66
- function parseContext(context) {
67
- const parsed = config.context.parse(context);
68
- return {
69
- ...context,
70
- ...parsed,
71
- params: undefined,
72
- input: context.input
73
- };
74
- }
75
- function define(definition) {
76
- if (definition.output !== undefined) {
77
- const { action: action2, method: method2, meta: meta2, handler: handler2, ...tool2 } = definition;
78
- const identity2 = {
79
- serviceName: config.serviceName,
80
- action: action2,
81
- method: method2,
82
- ...config.scope !== undefined && { scope: config.scope },
83
- ...(meta2 ?? config.meta) !== undefined && { meta: meta2 ?? config.meta }
84
- };
85
- return defineRuntimeTool({
86
- ...tool2,
87
- identity: identity2,
88
- handler: (context) => handler2(parseContext(context))
89
- });
90
- }
91
- const { action, method, meta, handler, ...tool } = definition;
92
- const identity = {
93
- serviceName: config.serviceName,
94
- action,
95
- method,
96
- ...config.scope !== undefined && { scope: config.scope },
97
- ...(meta ?? config.meta) !== undefined && { meta: meta ?? config.meta }
98
- };
99
- return defineRuntimeTool({
100
- ...tool,
101
- identity,
102
- handler: (context) => handler(parseContext(context))
103
- });
104
- }
105
- return { define };
106
- }
107
- function runtimeToolSupports(definition, transport) {
108
- if (definition.transports?.length === 0) {
109
- throw new Error(`Runtime tool "${definition.name}" must expose at least one transport`);
110
- }
111
- return !definition.transports || definition.transports.includes(transport);
112
- }
113
- function runtimeToolIdentity(definition) {
114
- return {
115
- method: definition.identity.method,
116
- desc: definition.description,
117
- serviceName: definition.identity.serviceName,
118
- key: definition.identity.action,
119
- toolName: definition.name,
120
- scope: definition.identity.scope,
121
- meta: definition.identity.meta,
122
- annotations: definition.annotations,
123
- ui: definition.ui,
124
- mcp: definition.mcp
125
- };
126
- }
127
- function runtimeToolMountable(definition, assertName = true) {
128
- if (assertName) {
129
- assertToolName(definition.name, definition.identity.serviceName, definition.identity.action);
130
- }
131
- const method = {
132
- ...runtimeToolIdentity(definition),
133
- inputSchema: definition.input,
134
- outputSchema: definition.output,
135
- handler: definition.handler
136
- };
137
- return {
138
- method,
139
- name: definition.name,
140
- argumentSchema: definition.input,
141
- presentationSchema: buildToolPresentationSchema({
142
- inputSchema: definition.input,
143
- unrepresentable: "any"
144
- }),
145
- shouldExtend: false
146
- };
147
- }
148
-
149
- // src/tools/surface.ts
150
- function duplicateLabel(transport) {
151
- if (transport === "MCP")
152
- return "MCP tool name";
153
- if (transport === "AGENT")
154
- return "agent tool name";
155
- return "CLI command";
156
- }
157
- function collectToolSurface({
158
- surface,
159
- transport,
160
- assertUniqueNames = true,
161
- ...collectConfig
162
- }) {
163
- const entries = [];
164
- const names = new Set;
165
- const append = (entry) => {
166
- if (assertUniqueNames) {
167
- assertUniqueToolName(entry.mountable.name, names.has(entry.mountable.name), duplicateLabel(transport));
168
- }
169
- names.add(entry.mountable.name);
170
- entries.push(entry);
171
- };
172
- for (const service of surface.services ?? []) {
173
- for (const mountable of collectTools(service, transport, collectConfig)) {
174
- append({
175
- kind: "contract",
176
- service: service.name,
177
- action: mountable.method.key,
178
- mountable
179
- });
180
- }
181
- }
182
- if (transport === "CLI")
183
- return entries;
184
- for (const definition of surface.runtimeTools ?? []) {
185
- if (!runtimeToolSupports(definition, transport))
186
- continue;
187
- append({
188
- kind: "runtime",
189
- service: definition.identity.serviceName,
190
- action: definition.identity.action,
191
- mountable: runtimeToolMountable(definition, collectConfig.assertNames),
192
- definition
193
- });
194
- }
195
- return entries;
196
- }
197
-
198
- // src/tools/agent.ts
199
70
  function mountAgent(services, config = {}) {
200
71
  const serviceList = Array.isArray(services) ? services : [services];
201
72
  const tools = {};
@@ -219,10 +90,10 @@ function mountAgent(services, config = {}) {
219
90
  const inputSchema = jsonSchema(mountable.presentationSchema, {
220
91
  validate: async (value) => isRecord(value) ? { success: true, value } : { success: false, error: new Error("Tool arguments must be an object") }
221
92
  });
222
- const execute = async (rawArgs) => {
93
+ const execute = async (rawArgs, options) => {
223
94
  const args = isRecord(rawArgs) ? rawArgs : {};
224
95
  try {
225
- const result = await runTool(mountable, args);
96
+ const result = await runTool(mountable, args, { signal: options.abortSignal });
226
97
  if (result.ok)
227
98
  return result.data;
228
99
  return formatToolError(result, mountable.name, config.errorHint);
@@ -262,6 +133,600 @@ function mountAgent(services, config = {}) {
262
133
  }
263
134
  return tools;
264
135
  }
136
+ // src/tools/async-operation.ts
137
+ import { z } from "zod";
138
+
139
+ // src/tools/native-definition.ts
140
+ function managedNativeIdentity(identity, method) {
141
+ return { ...identity, method };
142
+ }
143
+
144
+ // src/tools/define-wait-tool.ts
145
+ function defineWaitTool(config) {
146
+ const renderedMcp = config.render ? {
147
+ mcp: (state) => {
148
+ const rendered = config.render?.(state, false);
149
+ return {
150
+ content: [
151
+ {
152
+ type: "text",
153
+ text: rendered?.text ?? JSON.stringify(state, null, 2)
154
+ }
155
+ ]
156
+ };
157
+ }
158
+ } : undefined;
159
+ const present = renderedMcp || config.present ? { ...renderedMcp, ...config.present } : undefined;
160
+ return defineRuntimeTool({
161
+ name: config.name ?? "wait",
162
+ description: config.description,
163
+ identity: managedNativeIdentity(config.identity, "GET"),
164
+ input: config.input,
165
+ output: config.state,
166
+ transports: config.transports,
167
+ annotations: config.annotations,
168
+ present,
169
+ handler: async (context) => {
170
+ let result;
171
+ try {
172
+ result = await runWaitOperation({
173
+ input: context.input,
174
+ poll: async (input, signal) => config.state.parse(await config.poll(input, { ...context, signal })),
175
+ done: config.done,
176
+ backoff: config.backoff,
177
+ timeoutSec: config.timeoutFromInput?.(context.input) ?? config.defaultTimeout,
178
+ signal: context.signal
179
+ });
180
+ } catch (error) {
181
+ if (error instanceof WaitTimeoutError) {
182
+ throw new AppError("WAIT_TIMEOUT", "Wait timed out before the first snapshot", 408);
183
+ }
184
+ throw error;
185
+ }
186
+ const rendered = config.render?.(result.state, result.timedOut);
187
+ if (result.timedOut) {
188
+ throw new AppError("WAIT_TIMEOUT", rendered?.text ?? "Wait timed out before the operation completed", 408);
189
+ }
190
+ if (rendered?.isError) {
191
+ throw new AppError("WAIT_FAILED", rendered.text, 409);
192
+ }
193
+ return result.state;
194
+ }
195
+ });
196
+ }
197
+
198
+ // src/tools/async-operation.ts
199
+ var AsyncOperationCancelResultSchema = z.discriminatedUnion("outcome", [
200
+ z.object({ outcome: z.literal("accepted") }),
201
+ z.object({ outcome: z.literal("already_terminal") }),
202
+ z.object({ outcome: z.literal("rejected"), reason: z.string().min(1) })
203
+ ]);
204
+ function createAsyncOperationSnapshotSchema(config) {
205
+ const progress = config.progress ? { progress: config.progress.optional() } : {};
206
+ return z.discriminatedUnion("phase", [
207
+ z.object({ phase: z.literal("pending"), ...progress }),
208
+ z.object({ phase: z.literal("running"), ...progress }),
209
+ z.object({ phase: z.literal("succeeded"), ...progress }),
210
+ z.object({ phase: z.literal("failed"), failure: config.failure, ...progress }),
211
+ z.object({ phase: z.literal("cancelled"), ...progress })
212
+ ]);
213
+ }
214
+ function terminal(snapshot) {
215
+ if (typeof snapshot !== "object" || snapshot === null || !("phase" in snapshot)) {
216
+ return false;
217
+ }
218
+ return snapshot.phase === "succeeded" || snapshot.phase === "failed" || snapshot.phase === "cancelled";
219
+ }
220
+ function succeeded(snapshot) {
221
+ return typeof snapshot === "object" && snapshot !== null && "phase" in snapshot && snapshot.phase === "succeeded";
222
+ }
223
+ function defineAsyncOperation(config) {
224
+ const capabilityName = (capability) => config.names?.[capability] ?? `${config.name}_${capability}`;
225
+ const description = (capability) => config.descriptions?.[capability] ?? `${config.description}: ${capability}`;
226
+ const identity = (capability, method) => ({
227
+ serviceName: config.identity.serviceName,
228
+ action: `${config.identity.action}.${capability}`,
229
+ scope: config.scopes?.[capability] ?? config.identity.scope,
230
+ meta: config.identity.meta,
231
+ method
232
+ });
233
+ const common = (capability) => ({
234
+ name: capabilityName(capability),
235
+ description: description(capability),
236
+ transports: config.transports?.[capability],
237
+ annotations: config.annotations?.[capability] ?? (capability === "start" || capability === "cancel" ? { destructiveHint: true } : undefined)
238
+ });
239
+ const inspect = async (capability, context) => {
240
+ await config.authorize(context.input, capability, context);
241
+ const state = config.state.parse(await config.inspect(context.input, context));
242
+ const snapshot = config.snapshot.parse(await config.classify(state, context));
243
+ return { state, snapshot };
244
+ };
245
+ const start = defineRuntimeTool({
246
+ ...common("start"),
247
+ identity: identity("start", "POST"),
248
+ input: config.startInput,
249
+ output: config.id,
250
+ handler: (context) => config.start(context.input, context)
251
+ });
252
+ const status = defineRuntimeTool({
253
+ ...common("status"),
254
+ identity: identity("status", "GET"),
255
+ input: config.id,
256
+ output: config.snapshot,
257
+ handler: async (context) => (await inspect("status", context)).snapshot
258
+ });
259
+ const wait = defineWaitTool({
260
+ ...common("wait"),
261
+ identity: identity("wait", "GET"),
262
+ input: config.id,
263
+ state: config.snapshot,
264
+ poll: async (_id, context) => (await inspect("wait", context)).snapshot,
265
+ done: terminal,
266
+ backoff: config.backoff,
267
+ defaultTimeout: config.defaultTimeout,
268
+ timeoutFromInput: config.timeoutFromId
269
+ });
270
+ const definitions = { start, status, wait };
271
+ const names = new Set;
272
+ if (config.cancel) {
273
+ const capability = config.cancel;
274
+ definitions.cancel = defineRuntimeTool({
275
+ ...common("cancel"),
276
+ identity: identity("cancel", "DELETE"),
277
+ input: config.id,
278
+ output: AsyncOperationCancelResultSchema,
279
+ handler: async (context) => {
280
+ const inspected = await inspect("cancel", context);
281
+ return capability.handler(inspected.state, context);
282
+ }
283
+ });
284
+ }
285
+ if (config.result) {
286
+ const capability = config.result;
287
+ definitions.result = defineRuntimeTool({
288
+ ...common("result"),
289
+ identity: identity("result", "GET"),
290
+ input: config.id,
291
+ output: capability.output,
292
+ handler: async (context) => {
293
+ const inspected = await inspect("result", context);
294
+ if (!succeeded(inspected.snapshot)) {
295
+ throw new AppError("OPERATION_NOT_SUCCEEDED", "Operation result is not available", 409);
296
+ }
297
+ return capability.handler(inspected.state, context);
298
+ }
299
+ });
300
+ }
301
+ if (config.artifacts) {
302
+ const capability = config.artifacts;
303
+ definitions.artifacts = defineRuntimeTool({
304
+ ...common("artifacts"),
305
+ identity: identity("artifacts", "GET"),
306
+ input: config.id,
307
+ output: capability.output,
308
+ handler: async (context) => {
309
+ const inspected = await inspect("artifacts", context);
310
+ if (!succeeded(inspected.snapshot)) {
311
+ throw new AppError("OPERATION_NOT_SUCCEEDED", "Operation artifacts are not available", 409);
312
+ }
313
+ return capability.handler(inspected.state, context);
314
+ }
315
+ });
316
+ }
317
+ for (const definition of Object.values(definitions)) {
318
+ assertUniqueToolName(definition.name, names.has(definition.name), "in-process tool name");
319
+ names.add(definition.name);
320
+ }
321
+ return {
322
+ ...definitions,
323
+ runtimeTools: Object.values(definitions),
324
+ schemas: {
325
+ id: config.id,
326
+ snapshot: config.snapshot,
327
+ cancelResult: AsyncOperationCancelResultSchema
328
+ }
329
+ };
330
+ }
331
+ function bindContractAsyncOperation(config) {
332
+ const endpoint = (key) => {
333
+ const found = config.contract.endpoints[key];
334
+ if (!found)
335
+ throw new Error(`Contract async operation method "${key}" not found`);
336
+ return found;
337
+ };
338
+ const start = endpoint(config.capabilities.start);
339
+ const status = endpoint(config.capabilities.status);
340
+ const wait = endpoint(config.capabilities.wait);
341
+ if (!start.output || !status.output || !wait.output) {
342
+ throw new Error("Contract async operation requires outputs for start, status and wait");
343
+ }
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) {
349
+ throw new Error('Contract async operation capability "wait" output must reuse the same schema instance as the status output');
350
+ }
351
+ const validateOptional = (capability) => {
352
+ const key = config.capabilities[capability];
353
+ if (key && endpoint(key).input !== start.output) {
354
+ throw new Error(`Contract async operation capability "${capability}" input must reuse the same schema instance as the start output`);
355
+ }
356
+ };
357
+ validateOptional("cancel");
358
+ validateOptional("result");
359
+ validateOptional("artifacts");
360
+ return {
361
+ contract: config.contract,
362
+ handlers: config.handlers,
363
+ capabilities: config.capabilities,
364
+ schemas: { id: start.output, snapshot: status.output }
365
+ };
366
+ }
367
+ // src/tools/download-core.ts
368
+ import { basename, extname } from "node:path";
369
+ var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
370
+ var MIME_EXT = {
371
+ "image/png": ".png",
372
+ "image/jpeg": ".jpg",
373
+ "image/webp": ".webp",
374
+ "image/gif": ".gif",
375
+ "video/mp4": ".mp4",
376
+ "video/webm": ".webm",
377
+ "audio/mpeg": ".mp3",
378
+ "audio/wav": ".wav",
379
+ "audio/ogg": ".ogg"
380
+ };
381
+
382
+ class DownloadOperationError extends Error {
383
+ code;
384
+ status;
385
+ constructor(code, message, status) {
386
+ super(message);
387
+ this.code = code;
388
+ this.status = status;
389
+ this.name = "DownloadOperationError";
390
+ }
391
+ }
392
+ function extensionFor(url, mime) {
393
+ if (MIME_EXT[mime])
394
+ return MIME_EXT[mime];
395
+ try {
396
+ const ext = extname(new URL(url).pathname).toLowerCase();
397
+ if (ext)
398
+ return ext;
399
+ } catch {}
400
+ return ".bin";
401
+ }
402
+ function baseNameFor(url) {
403
+ try {
404
+ const path = new URL(url).pathname;
405
+ return (basename(path, extname(path)) || "download").slice(0, 60);
406
+ } catch {
407
+ return "download";
408
+ }
409
+ }
410
+ async function runDownloadOperation(config) {
411
+ const res = await fetchGuarded(new URL(config.url), config.allowPrivateHosts ?? false, {
412
+ timeoutMs: config.timeoutMs,
413
+ signal: config.signal
414
+ });
415
+ if (!res.ok) {
416
+ throw new DownloadOperationError("DOWNLOAD_HTTP_ERROR", `HTTP ${res.status}`, 502);
417
+ }
418
+ const mimeType = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() || "application/octet-stream";
419
+ const max = config.maxBytes ?? DEFAULT_MAX_BYTES;
420
+ const declared = Number(res.headers.get("content-length") ?? 0);
421
+ if (declared > max) {
422
+ await res.body?.cancel();
423
+ throw new DownloadOperationError("DOWNLOAD_TOO_LARGE", `file exceeds the ${max}-byte cap`, 413);
424
+ }
425
+ const path = config.path ?? `${baseNameFor(config.url)}${extensionFor(config.url, mimeType)}`;
426
+ return config.files.write(path, res.body ?? new Uint8Array, {
427
+ maxBytes: max,
428
+ mediaType: mimeType,
429
+ signal: config.signal
430
+ });
431
+ }
432
+
433
+ // src/tools/define-download-tool.ts
434
+ function defineDownloadTool(config) {
435
+ return defineRuntimeTool({
436
+ name: config.name ?? "download",
437
+ description: config.description,
438
+ identity: managedNativeIdentity(config.identity, "POST"),
439
+ input: config.input,
440
+ output: ManagedFileRefSchema,
441
+ transports: config.transports,
442
+ annotations: config.annotations,
443
+ present: config.present,
444
+ handler: async (context) => {
445
+ const url = await config.resolveUrl(context.input, context);
446
+ if (!url) {
447
+ throw new AppError("DOWNLOAD_NOT_FOUND", "Nothing to download", 404);
448
+ }
449
+ try {
450
+ return await runDownloadOperation({
451
+ url,
452
+ files: config.files,
453
+ path: config.pathFromInput?.(context.input),
454
+ allowPrivateHosts: config.allowPrivateHosts,
455
+ maxBytes: config.maxBytes,
456
+ timeoutMs: config.timeoutMs,
457
+ signal: context.signal
458
+ });
459
+ } catch (error) {
460
+ if (error instanceof DownloadOperationError) {
461
+ throw new AppError(error.code, `Download failed: ${error.message}`, error.status);
462
+ }
463
+ throw error;
464
+ }
465
+ }
466
+ });
467
+ }
468
+ // src/tools/define-upload-tool.ts
469
+ import { z as z2 } from "zod";
470
+
471
+ // src/tools/upload-core.ts
472
+ function runUploadOperation(files, path, upload, signal) {
473
+ signal?.throwIfAborted();
474
+ return files.read(path, { signal }).then((source) => upload(source, signal));
475
+ }
476
+
477
+ // src/tools/define-upload-tool.ts
478
+ var UploadToolInputSchema = z2.object({
479
+ path: ManagedFilePathSchema.describe("Path relative to the configured file boundary")
480
+ });
481
+ function defineUploadTool(config) {
482
+ return defineRuntimeTool({
483
+ name: config.name ?? "upload",
484
+ description: config.description,
485
+ identity: managedNativeIdentity(config.identity, "POST"),
486
+ input: UploadToolInputSchema,
487
+ output: config.output,
488
+ transports: config.transports,
489
+ annotations: config.annotations,
490
+ present: config.present,
491
+ handler: async (context) => runUploadOperation(config.files, context.input.path, (source) => config.upload(source, context), context.signal)
492
+ });
493
+ }
494
+ // src/tools/view-file.ts
495
+ import { extname as extname2 } from "node:path";
496
+ import { z as z3 } from "zod";
497
+ var MAX_INLINE_BYTES = 20 * 1024 * 1024;
498
+ var MAX_VIEW_FILES = 20;
499
+ var EXT_MIME = {
500
+ ".png": "image/png",
501
+ ".jpg": "image/jpeg",
502
+ ".jpeg": "image/jpeg",
503
+ ".gif": "image/gif",
504
+ ".webp": "image/webp",
505
+ ".mp3": "audio/mpeg",
506
+ ".wav": "audio/wav",
507
+ ".ogg": "audio/ogg",
508
+ ".m4a": "audio/mp4",
509
+ ".mp4": "video/mp4",
510
+ ".webm": "video/webm",
511
+ ".mov": "video/quicktime"
512
+ };
513
+ var McpAnnotationsSchema = z3.object({
514
+ audience: z3.array(z3.enum(["user", "assistant"])).optional(),
515
+ priority: z3.number().optional()
516
+ });
517
+ var McpMediaContentSchema = z3.discriminatedUnion("type", [
518
+ z3.object({ type: z3.literal("text"), text: z3.string() }),
519
+ z3.object({
520
+ type: z3.literal("image"),
521
+ data: z3.string(),
522
+ mimeType: z3.string(),
523
+ annotations: McpAnnotationsSchema.optional()
524
+ }),
525
+ z3.object({
526
+ type: z3.literal("audio"),
527
+ data: z3.string(),
528
+ mimeType: z3.string(),
529
+ annotations: McpAnnotationsSchema.optional()
530
+ })
531
+ ]);
532
+ var ViewFileErrorSchema = z3.object({
533
+ path: z3.string(),
534
+ message: z3.string()
535
+ });
536
+ var ViewFileOutputSchema = z3.object({
537
+ content: z3.array(McpMediaContentSchema),
538
+ errors: z3.array(ViewFileErrorSchema)
539
+ });
540
+ async function fetchSource(pathOrUrl, options, maxBytes) {
541
+ const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
542
+ if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
543
+ const url = new URL(pathOrUrl);
544
+ const res = await fetchGuarded(url, options.allowPrivateHosts ?? false, {
545
+ timeoutMs: options.timeoutMs,
546
+ signal: options.signal
547
+ });
548
+ if (!res.ok)
549
+ throw new Error(`HTTP ${res.status}`);
550
+ const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
551
+ const mimeType = headerMime || extMime || "application/octet-stream";
552
+ if (mimeType.startsWith("video/")) {
553
+ await res.body?.cancel();
554
+ return { video: true, mimeType, bytesRead: 0 };
555
+ }
556
+ const declared = Number(res.headers.get("content-length") ?? 0);
557
+ if (declared > maxBytes) {
558
+ await res.body?.cancel();
559
+ return { tooLarge: true, mimeType, bytesRead: 0 };
560
+ }
561
+ const buffer = await readCapped(res, maxBytes);
562
+ return buffer ? { buffer, mimeType, bytesRead: buffer.length } : { tooLarge: true, mimeType, bytesRead: maxBytes };
563
+ }
564
+ if (!options.files) {
565
+ throw new Error("local file paths are disabled — set files to allow them");
566
+ }
567
+ options.signal?.throwIfAborted();
568
+ if (!extMime) {
569
+ throw new Error("refusing to read a non-media file");
570
+ }
571
+ try {
572
+ const source = await options.files.read(pathOrUrl, {
573
+ maxBytes,
574
+ signal: options.signal
575
+ });
576
+ const buffer = Buffer.from(source.bytes);
577
+ return {
578
+ buffer,
579
+ mimeType: source.ref.mediaType ?? extMime,
580
+ bytesRead: buffer.length
581
+ };
582
+ } catch (error) {
583
+ if (error instanceof ManagedFileError && error.code === "FILE_TOO_LARGE") {
584
+ return { tooLarge: true, mimeType: extMime, bytesRead: maxBytes };
585
+ }
586
+ throw error;
587
+ }
588
+ }
589
+ async function resolveMediaWithinBudget(pathOrUrl, options, maxBytes) {
590
+ if (maxBytes <= 0) {
591
+ return {
592
+ content: [
593
+ { type: "text", text: `[media] total inline budget exhausted — ${pathOrUrl}` }
594
+ ],
595
+ bytes: 0
596
+ };
597
+ }
598
+ const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
599
+ if (extMime?.startsWith("video/")) {
600
+ return {
601
+ content: [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }],
602
+ bytes: 0
603
+ };
604
+ }
605
+ const result = await fetchSource(pathOrUrl, options, maxBytes);
606
+ if ("video" in result) {
607
+ return {
608
+ content: [{ type: "text", text: `[video] ${result.mimeType} — ${pathOrUrl}` }],
609
+ bytes: result.bytesRead
610
+ };
611
+ }
612
+ if ("tooLarge" in result) {
613
+ return {
614
+ content: [
615
+ { type: "text", text: `[${result.mimeType}] too large to inline — ${pathOrUrl}` }
616
+ ],
617
+ bytes: result.bytesRead
618
+ };
619
+ }
620
+ const { buffer, mimeType } = result;
621
+ const sizeKb = (buffer.length / 1024).toFixed(0);
622
+ if (mimeType.startsWith("image/")) {
623
+ return {
624
+ content: [
625
+ {
626
+ type: "image",
627
+ data: buffer.toString("base64"),
628
+ mimeType,
629
+ annotations: { audience: ["user", "assistant"], priority: 0.9 }
630
+ },
631
+ { type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
632
+ ],
633
+ bytes: buffer.length
634
+ };
635
+ }
636
+ if (mimeType.startsWith("audio/")) {
637
+ return {
638
+ content: [
639
+ {
640
+ type: "audio",
641
+ data: buffer.toString("base64"),
642
+ mimeType,
643
+ annotations: { audience: ["user", "assistant"], priority: 0.9 }
644
+ },
645
+ { type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
646
+ ],
647
+ bytes: buffer.length
648
+ };
649
+ }
650
+ return {
651
+ content: [{ type: "text", text: `[${mimeType}] ${sizeKb}KB — ${pathOrUrl}` }],
652
+ bytes: buffer.length
653
+ };
654
+ }
655
+ async function resolveMedia(pathOrUrl, options = {}) {
656
+ return (await resolveMediaWithinBudget(pathOrUrl, options, MAX_INLINE_BYTES)).content;
657
+ }
658
+ var ViewFileInputSchema = z3.object({
659
+ paths: z3.union([z3.string(), z3.array(z3.string()).max(MAX_VIEW_FILES)]).describe("Media URL(s) or file path(s) to view")
660
+ });
661
+ async function runViewFileOperation(paths, options = {}, signal) {
662
+ const list = Array.isArray(paths) ? paths : [paths];
663
+ const content = [];
664
+ const errors = [];
665
+ let remainingBytes = MAX_INLINE_BYTES;
666
+ for (const pathOrUrl of list) {
667
+ signal?.throwIfAborted();
668
+ try {
669
+ const resolved = await resolveMediaWithinBudget(pathOrUrl, { ...options, signal }, remainingBytes);
670
+ remainingBytes -= resolved.bytes;
671
+ content.push(...resolved.content);
672
+ } catch (error) {
673
+ if (signal?.aborted)
674
+ throw signal.reason ?? error;
675
+ const message = error instanceof Error ? error.message : String(error);
676
+ errors.push({ path: pathOrUrl, message });
677
+ content.push({ type: "text", text: `[${pathOrUrl}] Error: ${message}` });
678
+ }
679
+ }
680
+ return ViewFileOutputSchema.parse({ content, errors });
681
+ }
682
+ function mountViewFile(server, options = {}) {
683
+ server.registerTool("view_file", {
684
+ 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.",
685
+ inputSchema: ViewFileInputSchema.shape,
686
+ annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
687
+ }, async (args) => {
688
+ const result = await runViewFileOperation(args.paths, options);
689
+ return { content: result.content };
690
+ });
691
+ }
692
+
693
+ // src/tools/define-view-file-tool.ts
694
+ function defineViewFileTool(config) {
695
+ const defaults = {
696
+ mcp: (output) => ({ content: output.content }),
697
+ agent: (output) => ({
698
+ type: "content",
699
+ value: output.content.map((part) => {
700
+ if (part.type === "text")
701
+ return part;
702
+ return {
703
+ type: "file",
704
+ data: { type: "data", data: part.data },
705
+ mediaType: part.mimeType
706
+ };
707
+ })
708
+ })
709
+ };
710
+ return defineRuntimeTool({
711
+ name: config.name ?? "view_file",
712
+ description: config.description,
713
+ identity: managedNativeIdentity(config.identity, "GET"),
714
+ input: ViewFileInputSchema,
715
+ output: ViewFileOutputSchema,
716
+ transports: config.transports,
717
+ annotations: config.annotations ?? {
718
+ title: "View Media",
719
+ readOnlyHint: true,
720
+ idempotentHint: true
721
+ },
722
+ present: { ...defaults, ...config.present },
723
+ handler: (context) => runViewFileOperation(context.input.paths, {
724
+ files: config.files,
725
+ allowPrivateHosts: config.allowPrivateHosts,
726
+ timeoutMs: config.timeoutMs
727
+ }, context.signal)
728
+ });
729
+ }
265
730
  // src/tools/invoker.ts
266
731
  function createToolInvoker(services, config) {
267
732
  const serviceList = Array.isArray(services) ? services : [services];
@@ -309,6 +774,24 @@ function createToolInvoker(services, config) {
309
774
  }
310
775
  });
311
776
  }
777
+ // src/tools/lifecycle.ts
778
+ function composeToolLifecycle(...hooks) {
779
+ return {
780
+ beforeHandle: async (ctx, endpoint) => {
781
+ for (const hook of hooks)
782
+ await hook?.beforeHandle?.(ctx, endpoint);
783
+ },
784
+ afterHandle: async (ctx, result, endpoint) => {
785
+ let current = result;
786
+ for (const hook of hooks) {
787
+ const transformed = await hook?.afterHandle?.(ctx, current, endpoint);
788
+ if (transformed !== undefined)
789
+ current = transformed;
790
+ }
791
+ return current;
792
+ }
793
+ };
794
+ }
312
795
  // src/tools/list-names.ts
313
796
  var TOOL_TRANSPORTS = ["MCP", "AGENT", "CLI"];
314
797
  function listToolNames(surface) {
@@ -358,7 +841,7 @@ import {
358
841
  createRequestStateCodec,
359
842
  McpServer
360
843
  } from "@modelcontextprotocol/server";
361
- import { z as z2 } from "zod";
844
+ import { z as z5 } from "zod";
362
845
 
363
846
  // src/tools/mcp-app.ts
364
847
  import { readFileSync } from "node:fs";
@@ -975,13 +1458,13 @@ function runInMcpRequestContext(context, toolName, body) {
975
1458
  import {
976
1459
  isCallToolResult
977
1460
  } from "@modelcontextprotocol/server";
978
- import { z } from "zod";
1461
+ import { z as z4 } from "zod";
979
1462
  function mountPreparedRuntimeMcp(server, tools, config) {
980
1463
  for (const { definition, descriptor } of tools) {
981
1464
  const { mountable } = descriptor;
982
1465
  const toolConfig = {
983
1466
  description: definition.description,
984
- inputSchema: z.looseObject({}).meta(presentationMetadata(descriptor.inputSchema))
1467
+ inputSchema: z4.looseObject({}).meta(presentationMetadata(descriptor.inputSchema))
985
1468
  };
986
1469
  if (descriptor.outputSchema)
987
1470
  toolConfig.outputSchema = descriptor.outputSchema;
@@ -1070,7 +1553,7 @@ function mountPreparedMcp(mcpServer, prepared, config = {}) {
1070
1553
  const { mountable } = descriptor;
1071
1554
  const toolConfig = {
1072
1555
  description: mountable.method.desc,
1073
- inputSchema: z2.looseObject({}).meta(presentationMetadata(descriptor.inputSchema))
1556
+ inputSchema: z5.looseObject({}).meta(presentationMetadata(descriptor.inputSchema))
1074
1557
  };
1075
1558
  if (descriptor.outputSchema)
1076
1559
  toolConfig.outputSchema = descriptor.outputSchema;
@@ -1409,9 +1892,103 @@ async function createStdioMcpServer(config) {
1409
1892
  maxSubscriptions: 0
1410
1893
  });
1411
1894
  }
1895
+ // src/tools/mcp-stdio-signals.ts
1896
+ var bound = new WeakSet;
1897
+ function bindStdioProcessSignals(handle, options = {}) {
1898
+ if (bound.has(handle)) {
1899
+ throw new Error("[stitchkit] bindStdioProcessSignals: this stdio handle is already bound; close the first binding before creating another");
1900
+ }
1901
+ bound.add(handle);
1902
+ const signals = [...new Set(options.signals ?? DEFAULT_PROCESS_SIGNALS)];
1903
+ const source = options.signalSource ?? defaultSignalSource;
1904
+ const handlers = [];
1905
+ let started = false;
1906
+ let sameTurnAsStart = false;
1907
+ let closed = false;
1908
+ let resolveChain = () => {
1909
+ return;
1910
+ };
1911
+ let rejectChain = () => {
1912
+ return;
1913
+ };
1914
+ const promise = new Promise((resolve, reject) => {
1915
+ resolveChain = resolve;
1916
+ rejectChain = reject;
1917
+ });
1918
+ promise.catch(() => {
1919
+ return;
1920
+ });
1921
+ const removeListeners = () => {
1922
+ if (closed)
1923
+ return;
1924
+ closed = true;
1925
+ for (const [signal, handler] of handlers)
1926
+ source.off(signal, handler);
1927
+ if (!started)
1928
+ bound.delete(handle);
1929
+ };
1930
+ const close = () => {
1931
+ const hadStarted = started;
1932
+ removeListeners();
1933
+ if (!hadStarted)
1934
+ resolveChain();
1935
+ };
1936
+ const run = async (signal) => {
1937
+ try {
1938
+ await options.onClose?.(signal);
1939
+ } catch (error) {
1940
+ reportSignalError("prepare", error, options.onError);
1941
+ }
1942
+ try {
1943
+ await handle.close();
1944
+ } catch (error) {
1945
+ rejectChain(error);
1946
+ reportSignalError("close", error, options.onError);
1947
+ removeListeners();
1948
+ return;
1949
+ }
1950
+ resolveChain();
1951
+ try {
1952
+ await options.onComplete?.();
1953
+ } catch (error) {
1954
+ reportSignalError("complete", error, options.onError);
1955
+ } finally {
1956
+ removeListeners();
1957
+ }
1958
+ };
1959
+ const onSignal = (signal) => {
1960
+ if (!started) {
1961
+ started = true;
1962
+ sameTurnAsStart = true;
1963
+ queueMicrotask(() => {
1964
+ sameTurnAsStart = false;
1965
+ });
1966
+ run(signal).catch((error) => reportSignalError("close", error, options.onError));
1967
+ return;
1968
+ }
1969
+ if (sameTurnAsStart)
1970
+ return;
1971
+ removeListeners();
1972
+ guardSignalCallback(() => options.onRepeatedSignal?.(signal), "close", options.onError);
1973
+ let restored = false;
1974
+ try {
1975
+ restored = source.raiseDefault(signal);
1976
+ } catch (error) {
1977
+ reportSignalError("close", error, options.onError);
1978
+ }
1979
+ if (!restored) {
1980
+ guardSignalCallback(() => options.onEscalationBlocked?.(signal), "close", options.onError);
1981
+ }
1982
+ };
1983
+ for (const signal of signals) {
1984
+ const handler = () => onSignal(signal);
1985
+ handlers.push([signal, handler]);
1986
+ source.on(signal, handler);
1987
+ }
1988
+ return { promise, close };
1989
+ }
1412
1990
  // src/tools/mount-download.ts
1413
- import { basename, extname, resolve } from "node:path";
1414
- import { z as z3 } from "zod";
1991
+ import { z as z6 } from "zod";
1415
1992
 
1416
1993
  // src/tools/native-result.ts
1417
1994
  function textResult(text, isError = false) {
@@ -1420,84 +1997,45 @@ function textResult(text, isError = false) {
1420
1997
  }
1421
1998
 
1422
1999
  // src/tools/mount-download.ts
1423
- var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
1424
- var MIME_EXT = {
1425
- "image/png": ".png",
1426
- "image/jpeg": ".jpg",
1427
- "image/webp": ".webp",
1428
- "image/gif": ".gif",
1429
- "video/mp4": ".mp4",
1430
- "video/webm": ".webm",
1431
- "audio/mpeg": ".mp3",
1432
- "audio/wav": ".wav",
1433
- "audio/ogg": ".ogg"
1434
- };
1435
- function extensionFor(url, mime) {
1436
- if (MIME_EXT[mime])
1437
- return MIME_EXT[mime];
1438
- try {
1439
- const ext = extname(new URL(url).pathname).toLowerCase();
1440
- if (ext)
1441
- return ext;
1442
- } catch {}
1443
- return ".bin";
1444
- }
1445
- function baseNameFor(url) {
1446
- try {
1447
- const path = new URL(url).pathname;
1448
- return (basename(path, extname(path)) || "download").slice(0, 60);
1449
- } catch {
1450
- return "download";
1451
- }
1452
- }
1453
2000
  function mountDownload(server, config) {
1454
2001
  const name = config.name ?? "download";
1455
2002
  assertToolName(name, "<native>", "download");
1456
- server.registerTool(name, { description: config.description, inputSchema: z3.object(config.inputSchema) }, async (rawArgs) => {
2003
+ server.registerTool(name, { description: config.description, inputSchema: z6.object(config.inputSchema) }, async (rawArgs) => {
1457
2004
  const args = isRecord(rawArgs) ? rawArgs : {};
1458
2005
  try {
1459
2006
  const url = await config.resolveUrl(args);
1460
2007
  if (!url)
1461
2008
  return textResult("Nothing to download.", true);
1462
- const res = await fetchGuarded(new URL(url), config.allowPrivateHosts ?? false, {
2009
+ const result = await runDownloadOperation({
2010
+ url,
2011
+ files: config.files,
2012
+ path: config.pathFromArgs?.(args),
2013
+ allowPrivateHosts: config.allowPrivateHosts,
2014
+ maxBytes: config.maxBytes,
1463
2015
  timeoutMs: config.timeoutMs
1464
2016
  });
1465
- if (!res.ok)
1466
- return textResult(`Download failed: HTTP ${res.status}`, true);
1467
- const mimeType = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() || "application/octet-stream";
1468
- const max = config.maxBytes ?? DEFAULT_MAX_BYTES;
1469
- const declared = Number(res.headers.get("content-length") ?? 0);
1470
- if (declared > max) {
1471
- await res.body?.cancel();
1472
- return textResult(`Download failed: file exceeds the ${max}-byte cap`, true);
1473
- }
1474
- const buffer = await readCapped(res, max);
1475
- if (!buffer)
1476
- return textResult(`Download failed: file exceeds the ${max}-byte cap`, true);
1477
- const dir = resolve(config.dirFromArgs?.(args) ?? config.defaultDir);
1478
- const filePath = resolve(dir, `${baseNameFor(url)}${extensionFor(url, mimeType)}`);
1479
- await writeDownload(dir, filePath, buffer);
1480
- return textResult(JSON.stringify({ path: filePath, size: buffer.length, mimeType }, null, 2));
2017
+ return textResult(JSON.stringify(result, null, 2));
1481
2018
  } catch (err) {
1482
2019
  return textResult(`Download failed: ${err instanceof Error ? err.message : String(err)}`, true);
1483
2020
  }
1484
2021
  });
1485
2022
  }
1486
2023
  // src/tools/mount-upload.ts
1487
- import { z as z4 } from "zod";
1488
2024
  function mountUpload(server, config) {
1489
2025
  const name = config.name ?? "upload";
1490
2026
  assertToolName(name, "<native>", "upload");
1491
2027
  server.registerTool(name, {
1492
2028
  description: config.description,
1493
- inputSchema: { path: z4.string().describe("Path to a local file on this machine") }
2029
+ inputSchema: {
2030
+ path: ManagedFilePathSchema.describe("Path relative to the configured file boundary")
2031
+ }
1494
2032
  }, async (rawArgs) => {
1495
2033
  const args = isRecord(rawArgs) ? rawArgs : {};
1496
2034
  const path = typeof args.path === "string" ? args.path : "";
1497
2035
  if (!path)
1498
2036
  return textResult("Provide `path`.", true);
1499
2037
  try {
1500
- const uploaded = await config.upload(path);
2038
+ const uploaded = await runUploadOperation(config.files, path, config.upload);
1501
2039
  return textResult(JSON.stringify(uploaded ?? null, null, 2));
1502
2040
  } catch (err) {
1503
2041
  return textResult(`Upload failed: ${err instanceof Error ? err.message : String(err)}`, true);
@@ -1505,15 +2043,16 @@ function mountUpload(server, config) {
1505
2043
  });
1506
2044
  }
1507
2045
  // src/tools/mount-wait.ts
1508
- import { z as z5 } from "zod";
2046
+ import { z as z7 } from "zod";
1509
2047
  function mountWait(server, config) {
1510
2048
  const name = config.name ?? "wait";
1511
2049
  assertToolName(name, "<native>", "wait");
1512
- server.registerTool(name, { description: config.description, inputSchema: z5.object(config.inputSchema) }, async (rawArgs) => {
2050
+ server.registerTool(name, { description: config.description, inputSchema: z7.object(config.inputSchema) }, async (rawArgs) => {
1513
2051
  const args = isRecord(rawArgs) ? rawArgs : {};
1514
2052
  try {
1515
- const { state, timedOut } = await pollUntil({
1516
- poll: () => config.poll(args),
2053
+ const { state, timedOut } = await runWaitOperation({
2054
+ input: args,
2055
+ poll: (input) => config.poll(input),
1517
2056
  done: config.done,
1518
2057
  backoff: config.backoff,
1519
2058
  timeoutSec: config.timeoutFromArgs?.(args) ?? config.defaultTimeout
@@ -1527,19 +2066,19 @@ function mountWait(server, config) {
1527
2066
  }
1528
2067
  // src/tools/oauth-provider.ts
1529
2068
  import { randomUUID } from "node:crypto";
1530
- import { z as z6 } from "zod";
2069
+ import { z as z8 } from "zod";
1531
2070
  function isPlainDisplayCharacter(character) {
1532
2071
  const codePoint = character.codePointAt(0);
1533
2072
  return codePoint !== undefined && codePoint >= 32 && codePoint !== 127 && character !== "<" && character !== ">";
1534
2073
  }
1535
- var CimdClientMetadataSchema = z6.object({
1536
- client_id: z6.url(),
1537
- redirect_uris: z6.array(z6.url()).min(1),
1538
- client_name: z6.string().trim().min(1).max(200).refine((value) => Array.from(value).every(isPlainDisplayCharacter), {
2074
+ var CimdClientMetadataSchema = z8.object({
2075
+ client_id: z8.url(),
2076
+ redirect_uris: z8.array(z8.url()).min(1),
2077
+ client_name: z8.string().trim().min(1).max(200).refine((value) => Array.from(value).every(isPlainDisplayCharacter), {
1539
2078
  message: "client_name must be plain display text"
1540
2079
  }),
1541
- token_endpoint_auth_method: z6.literal("none"),
1542
- application_type: z6.enum(["native", "web"]).optional()
2080
+ token_endpoint_auth_method: z8.literal("none"),
2081
+ application_type: z8.enum(["native", "web"]).optional()
1543
2082
  });
1544
2083
  function createSecureClientMetadataFetcher(policy = {}) {
1545
2084
  assertPositiveInteger("cimd.maxBytes", policy.maxBytes ?? DEFAULT_CIMD_MAX_BYTES);
@@ -2172,67 +2711,6 @@ function mountOAuthProvider(config) {
2172
2711
  };
2173
2712
  return dcrEnabled ? [metadataRoute, registerRoute, authorizeRoute, tokenRoute] : [metadataRoute, authorizeRoute, tokenRoute];
2174
2713
  }
2175
- // src/tools/remote.ts
2176
- var HTTP_ONLY = Object.freeze(["HTTP"]);
2177
- function toArgs(ctx) {
2178
- const { params, input } = ctx;
2179
- return {
2180
- ...isRecord(params) ? params : {},
2181
- ...isRecord(input) ? input : {}
2182
- };
2183
- }
2184
- function implementRemote(contract, http, options) {
2185
- const client = createClient(contract, http);
2186
- const groupScope = contract.meta.scope ?? "public";
2187
- const methods = {};
2188
- for (const [key, endpoint] of Object.entries(contract.endpoints)) {
2189
- methods[key] = {
2190
- method: endpoint.method,
2191
- path: endpoint.path,
2192
- desc: endpoint.desc,
2193
- serviceName: contract.meta.prefix,
2194
- key,
2195
- toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
2196
- expose: endpoint.rawResponse || endpoint.rawBody || endpoint.responseMeta ? HTTP_ONLY : endpoint.expose,
2197
- ui: "ui" in endpoint ? endpoint.ui : undefined,
2198
- annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
2199
- meta: mergeMeta(contract.meta.meta, endpoint.meta),
2200
- scope: endpoint.scope ?? groupScope,
2201
- paramsSchema: endpoint.params,
2202
- inputSchema: endpoint.input,
2203
- outputSchema: endpoint.output,
2204
- multipart: endpoint.multipart,
2205
- maxJsonBodyBytes: endpoint.maxJsonBodyBytes,
2206
- idempotent: endpoint.idempotent,
2207
- rawResponse: endpoint.rawResponse,
2208
- rawBody: endpoint.rawBody,
2209
- responseMeta: endpoint.responseMeta,
2210
- contentType: "contentType" in endpoint ? endpoint.contentType : undefined,
2211
- handler: async (ctx) => {
2212
- const call = client[key];
2213
- if (!call) {
2214
- throw new Error(`implementRemote: endpoint "${key}" is not exposed over HTTP`);
2215
- }
2216
- const args = toArgs(ctx);
2217
- try {
2218
- const finalArgs = options?.transformArgs ? await options.transformArgs(key, args) : args;
2219
- return await call(finalArgs);
2220
- } catch (err) {
2221
- if (ApiError.is(err)) {
2222
- throw new AppError(err.code, err.message, err.status, isRecord(err.details) ? err.details : undefined, err.hint, err.traceId);
2223
- }
2224
- throw err;
2225
- }
2226
- }
2227
- };
2228
- }
2229
- return {
2230
- name: contract.meta.prefix,
2231
- prefix: contract.meta.prefix,
2232
- scope: groupScope,
2233
- methods
2234
- };
2235
- }
2236
2714
  // src/tools/tool-logger.ts
2237
2715
  function createToolLogger(config = {}) {
2238
2716
  const log = config.log ?? ((line) => console.error(line));
@@ -2332,206 +2810,60 @@ function summarizeTransports(surface) {
2332
2810
  sources
2333
2811
  };
2334
2812
  }
2335
- // src/tools/view-file.ts
2336
- import { readFile, stat } from "node:fs/promises";
2337
- import { extname as extname2, resolve as resolve2 } from "node:path";
2338
- import { z as z7 } from "zod";
2339
- var MAX_INLINE_BYTES = 20 * 1024 * 1024;
2340
- var MAX_VIEW_FILES = 20;
2341
- var EXT_MIME = {
2342
- ".png": "image/png",
2343
- ".jpg": "image/jpeg",
2344
- ".jpeg": "image/jpeg",
2345
- ".gif": "image/gif",
2346
- ".webp": "image/webp",
2347
- ".mp3": "audio/mpeg",
2348
- ".wav": "audio/wav",
2349
- ".ogg": "audio/ogg",
2350
- ".m4a": "audio/mp4",
2351
- ".mp4": "video/mp4",
2352
- ".webm": "video/webm",
2353
- ".mov": "video/quicktime"
2354
- };
2355
- async function fetchSource(pathOrUrl, options, maxBytes) {
2356
- const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
2357
- if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
2358
- const url = new URL(pathOrUrl);
2359
- const res = await fetchGuarded(url, options.allowPrivateHosts ?? false, {
2360
- timeoutMs: options.timeoutMs
2361
- });
2362
- if (!res.ok)
2363
- throw new Error(`HTTP ${res.status}`);
2364
- const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
2365
- const mimeType = headerMime || extMime || "application/octet-stream";
2366
- if (mimeType.startsWith("video/")) {
2367
- await res.body?.cancel();
2368
- return { video: true, mimeType, bytesRead: 0 };
2369
- }
2370
- const declared = Number(res.headers.get("content-length") ?? 0);
2371
- if (declared > maxBytes) {
2372
- await res.body?.cancel();
2373
- return { tooLarge: true, mimeType, bytesRead: 0 };
2374
- }
2375
- const buffer2 = await readCapped(res, maxBytes);
2376
- return buffer2 ? { buffer: buffer2, mimeType, bytesRead: buffer2.length } : { tooLarge: true, mimeType, bytesRead: maxBytes };
2377
- }
2378
- if (!options.baseDir) {
2379
- throw new Error("local file paths are disabled — set baseDir to allow them");
2380
- }
2381
- if (!extMime) {
2382
- throw new Error("refusing to read a non-media file");
2383
- }
2384
- const root = resolve2(options.baseDir);
2385
- const target = resolve2(root, pathOrUrl);
2386
- if (!isWithinDir(root, target)) {
2387
- throw new Error("path escapes the allowed directory");
2388
- }
2389
- const realTarget = await realPathWithinDir(root, target);
2390
- if (realTarget === null) {
2391
- throw new Error("path escapes the allowed directory");
2392
- }
2393
- const info = await stat(realTarget).catch(() => null);
2394
- if (!info?.isFile())
2395
- throw new Error("file not found");
2396
- if (info.size > maxBytes)
2397
- return { tooLarge: true, mimeType: extMime, bytesRead: 0 };
2398
- const buffer = await readFile(realTarget);
2399
- return { buffer, mimeType: extMime, bytesRead: buffer.length };
2400
- }
2401
- async function resolveMediaWithinBudget(pathOrUrl, options, maxBytes) {
2402
- if (maxBytes <= 0) {
2403
- return {
2404
- content: [
2405
- { type: "text", text: `[media] total inline budget exhausted — ${pathOrUrl}` }
2406
- ],
2407
- bytes: 0
2408
- };
2409
- }
2410
- const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
2411
- if (extMime?.startsWith("video/")) {
2412
- return {
2413
- content: [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }],
2414
- bytes: 0
2415
- };
2416
- }
2417
- const result = await fetchSource(pathOrUrl, options, maxBytes);
2418
- if ("video" in result) {
2419
- return {
2420
- content: [{ type: "text", text: `[video] ${result.mimeType} — ${pathOrUrl}` }],
2421
- bytes: result.bytesRead
2422
- };
2423
- }
2424
- if ("tooLarge" in result) {
2425
- return {
2426
- content: [
2427
- { type: "text", text: `[${result.mimeType}] too large to inline — ${pathOrUrl}` }
2428
- ],
2429
- bytes: result.bytesRead
2430
- };
2431
- }
2432
- const { buffer, mimeType } = result;
2433
- const sizeKb = (buffer.length / 1024).toFixed(0);
2434
- if (mimeType.startsWith("image/")) {
2435
- return {
2436
- content: [
2437
- {
2438
- type: "image",
2439
- data: buffer.toString("base64"),
2440
- mimeType,
2441
- annotations: { audience: ["user", "assistant"], priority: 0.9 }
2442
- },
2443
- { type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
2444
- ],
2445
- bytes: buffer.length
2446
- };
2447
- }
2448
- if (mimeType.startsWith("audio/")) {
2449
- return {
2450
- content: [
2451
- {
2452
- type: "audio",
2453
- data: buffer.toString("base64"),
2454
- mimeType,
2455
- annotations: { audience: ["user", "assistant"], priority: 0.9 }
2456
- },
2457
- { type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
2458
- ],
2459
- bytes: buffer.length
2460
- };
2461
- }
2462
- return {
2463
- content: [{ type: "text", text: `[${mimeType}] ${sizeKb}KB — ${pathOrUrl}` }],
2464
- bytes: buffer.length
2465
- };
2466
- }
2467
- async function resolveMedia(pathOrUrl, options = {}) {
2468
- return (await resolveMediaWithinBudget(pathOrUrl, options, MAX_INLINE_BYTES)).content;
2469
- }
2470
- var ViewFileInputSchema = z7.object({
2471
- paths: z7.union([z7.string(), z7.array(z7.string()).max(MAX_VIEW_FILES)]).describe("Media URL(s) or file path(s) to view")
2472
- });
2473
- function mountViewFile(server, options = {}) {
2474
- server.registerTool("view_file", {
2475
- 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.",
2476
- inputSchema: ViewFileInputSchema.shape,
2477
- annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
2478
- }, async (args) => {
2479
- const list = Array.isArray(args.paths) ? args.paths : [args.paths];
2480
- const content = [];
2481
- let remainingBytes = MAX_INLINE_BYTES;
2482
- for (const pathOrUrl of list) {
2483
- try {
2484
- const resolved = await resolveMediaWithinBudget(pathOrUrl, options, remainingBytes);
2485
- remainingBytes -= resolved.bytes;
2486
- content.push(...resolved.content);
2487
- } catch (err) {
2488
- content.push({
2489
- type: "text",
2490
- text: `[${pathOrUrl}] Error: ${err instanceof Error ? err.message : String(err)}`
2491
- });
2492
- }
2493
- }
2494
- return { content };
2495
- });
2496
- }
2497
2813
  export {
2498
- wwwAuthenticateHeader,
2499
- validateMcpSchemas,
2500
- summarizeTransports,
2501
- resolveMedia,
2502
- protectedResourceMetadataUrl,
2503
- oauthProtectedResourceRoute,
2504
- mountWait,
2505
- mountViewFile,
2506
- mountUpload,
2507
- mountOAuthProvider,
2508
- mountMcpResource,
2509
- mountMcp,
2510
- mountDownload,
2511
- mountAgent,
2512
- listToolNames,
2513
- listContractToolNames,
2514
- inlineMcpAppBundle,
2515
- implementRemote,
2516
- flattenToolJsonSchema,
2517
- findUntypedProperties,
2518
- findNonPortableFormats,
2519
- defineRuntimeTool,
2520
- createToolkit,
2521
- createToolLogger,
2522
- createToolInvoker,
2523
- createStdioMcpServer,
2524
- createSecureClientMetadataFetcher,
2525
- createRuntimeToolFactory,
2526
- createMcpHttpRoute,
2527
- createMcpHandler,
2528
- createCli,
2529
- collectTools,
2530
- coerceJsonArgs,
2531
- buildToolManifest,
2532
- buildMcpServer,
2533
- RESOURCE_MIME_TYPE,
2534
- PROTECTED_RESOURCE_PATH,
2814
+ AsyncOperationCancelResultSchema,
2815
+ EXT_APPS_BUNDLE_PLACEHOLDER,
2816
+ McpAnnotationsSchema,
2817
+ McpMediaContentSchema,
2535
2818
  PORTABLE_JSON_SCHEMA_FORMATS,
2536
- EXT_APPS_BUNDLE_PLACEHOLDER
2819
+ PROTECTED_RESOURCE_PATH,
2820
+ RESOURCE_MIME_TYPE,
2821
+ UploadToolInputSchema,
2822
+ ViewFileErrorSchema,
2823
+ ViewFileInputSchema,
2824
+ ViewFileOutputSchema,
2825
+ bindContractAsyncOperation,
2826
+ bindStdioProcessSignals,
2827
+ buildMcpServer,
2828
+ buildToolManifest,
2829
+ coerceJsonArgs,
2830
+ collectTools,
2831
+ composeToolLifecycle,
2832
+ createAsyncOperationSnapshotSchema,
2833
+ createCli,
2834
+ createMcpHandler,
2835
+ createMcpHttpRoute,
2836
+ createRuntimeToolFactory,
2837
+ createSecureClientMetadataFetcher,
2838
+ createStdioMcpServer,
2839
+ createToolInvoker,
2840
+ createToolLogger,
2841
+ createToolkit,
2842
+ defineAsyncOperation,
2843
+ defineCliCommand,
2844
+ defineDownloadTool,
2845
+ defineRuntimeTool,
2846
+ defineUploadTool,
2847
+ defineViewFileTool,
2848
+ defineWaitTool,
2849
+ findNonPortableFormats,
2850
+ findUntypedProperties,
2851
+ flattenToolJsonSchema,
2852
+ inlineMcpAppBundle,
2853
+ listContractToolNames,
2854
+ listToolNames,
2855
+ mountAgent,
2856
+ mountDownload,
2857
+ mountMcp,
2858
+ mountMcpResource,
2859
+ mountOAuthProvider,
2860
+ mountUpload,
2861
+ mountViewFile,
2862
+ mountWait,
2863
+ oauthProtectedResourceRoute,
2864
+ protectedResourceMetadataUrl,
2865
+ resolveMedia,
2866
+ summarizeTransports,
2867
+ validateMcpSchemas,
2868
+ wwwAuthenticateHeader
2537
2869
  };