stitchkit 0.54.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/browser/socket-io.d.ts +25 -7
- package/dist/browser/socket-io.d.ts.map +1 -1
- package/dist/cli.js +14 -9
- package/dist/contract/errors.d.ts +7 -0
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/file-ref.d.ts +13 -0
- package/dist/contract/file-ref.d.ts.map +1 -0
- package/dist/contract/index.d.ts +1 -0
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +21 -17
- package/dist/files/boundary.d.ts +55 -0
- package/dist/files/boundary.d.ts.map +1 -0
- package/dist/files.d.ts +3 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +16 -0
- package/dist/index-1bx83sw4.js +4 -0
- package/dist/{index-pfqjb5xy.js → index-28cqssm3.js} +30 -5
- package/dist/index-557by2db.js +34 -0
- package/dist/index-5r13htq1.js +283 -0
- package/dist/index-6k1937bx.js +20 -0
- package/dist/index-6y759j86.js +1031 -0
- package/dist/{index-bmnhqtya.js → index-7eq9va6e.js} +1 -4
- package/dist/index-bfcpjw20.js +363 -0
- package/dist/{index-yxpe3phd.js → index-cby4ar3v.js} +1 -18
- package/dist/{index-7vb44hy8.js → index-escqg10p.js} +193 -751
- package/dist/{index-173tvqcp.js → index-j3dem06f.js} +9 -114
- package/dist/{index-gvha32jh.js → index-jcc611vh.js} +2 -2
- package/dist/{index-z7761jk2.js → index-n4nfa7gh.js} +16 -22
- package/dist/{index-e1es808r.js → index-qh4xevf5.js} +1 -1
- package/dist/index-sbdmyz75.js +18 -0
- package/dist/{index-b15rtha1.js → index-sm2tjx06.js} +98 -3
- package/dist/{index-yez419px.js → index-xy8fmh6w.js} +99 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +188 -40
- package/dist/node.js +31 -27
- package/dist/observability/index.js +28 -25
- package/dist/react.js +2 -2
- package/dist/realtime/contract.d.ts +9 -0
- package/dist/realtime/contract.d.ts.map +1 -1
- package/dist/realtime/index.d.ts +2 -1
- package/dist/realtime/index.d.ts.map +1 -1
- package/dist/realtime/request.d.ts +21 -0
- package/dist/realtime/request.d.ts.map +1 -0
- package/dist/realtime/socket.d.ts +3 -0
- package/dist/realtime/socket.d.ts.map +1 -1
- package/dist/remote.d.ts +9 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +79 -0
- package/dist/server/context-contribution.d.ts +18 -0
- package/dist/server/context-contribution.d.ts.map +1 -0
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +136 -88
- package/dist/server/lifecycle.d.ts +4 -0
- package/dist/server/lifecycle.d.ts.map +1 -0
- package/dist/server/middleware/auth.d.ts +64 -9
- package/dist/server/middleware/auth.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/testing/surface-conformance.d.ts +180 -0
- package/dist/testing/surface-conformance.d.ts.map +1 -0
- package/dist/testing/surface-manifest.d.ts +255 -0
- package/dist/testing/surface-manifest.d.ts.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +714 -3
- package/dist/tools/agent.d.ts.map +1 -1
- package/dist/tools/async-operation.d.ts +360 -0
- package/dist/tools/async-operation.d.ts.map +1 -0
- package/dist/tools/cli-args.d.ts +1 -1
- package/dist/tools/cli-args.d.ts.map +1 -1
- package/dist/tools/cli-format.d.ts +2 -2
- package/dist/tools/cli-format.d.ts.map +1 -1
- package/dist/tools/cli-wait.d.ts +11 -2
- package/dist/tools/cli-wait.d.ts.map +1 -1
- package/dist/tools/cli.d.ts +2 -0
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/define-download-tool.d.ts +6 -5
- package/dist/tools/define-download-tool.d.ts.map +1 -1
- package/dist/tools/define-upload-tool.d.ts +3 -1
- package/dist/tools/define-upload-tool.d.ts.map +1 -1
- package/dist/tools/define-wait-tool.d.ts +1 -1
- package/dist/tools/define-wait-tool.d.ts.map +1 -1
- package/dist/tools/download-core.d.ts +5 -10
- package/dist/tools/download-core.d.ts.map +1 -1
- package/dist/tools/internal/surface-projector.d.ts +101 -0
- package/dist/tools/internal/surface-projector.d.ts.map +1 -0
- package/dist/tools/lifecycle.d.ts +4 -0
- package/dist/tools/lifecycle.d.ts.map +1 -0
- package/dist/tools/managed-file-error.d.ts +6 -0
- package/dist/tools/managed-file-error.d.ts.map +1 -0
- package/dist/tools/mcp-prepare.d.ts +3 -21
- package/dist/tools/mcp-prepare.d.ts.map +1 -1
- package/dist/tools/mcp-round-policy.d.ts +9 -0
- package/dist/tools/mcp-round-policy.d.ts.map +1 -0
- package/dist/tools/mcp-round.d.ts +0 -4
- package/dist/tools/mcp-round.d.ts.map +1 -1
- package/dist/tools/mount-download.d.ts +5 -4
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/mount-upload.d.ts +4 -2
- package/dist/tools/mount-upload.d.ts.map +1 -1
- package/dist/tools/mount.d.ts +5 -9
- package/dist/tools/mount.d.ts.map +1 -1
- package/dist/tools/runtime-tool.d.ts.map +1 -1
- package/dist/tools/surface.d.ts +7 -0
- package/dist/tools/surface.d.ts.map +1 -1
- package/dist/tools/upload-core.d.ts +3 -2
- package/dist/tools/upload-core.d.ts.map +1 -1
- package/dist/tools/view-file.d.ts +4 -3
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools/wait-core.d.ts +6 -1
- package/dist/tools/wait-core.d.ts.map +1 -1
- package/dist/tools.d.ts +3 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +705 -599
- package/llms-full.txt +727 -34
- package/package.json +10 -2
package/dist/tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
signJwt,
|
|
3
3
|
verifyPkce
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-xy8fmh6w.js";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_CORS_ALLOW_HEADERS,
|
|
7
7
|
DEFAULT_PROCESS_SIGNALS,
|
|
@@ -9,11 +9,9 @@ import {
|
|
|
9
9
|
defaultSignalSource,
|
|
10
10
|
guardSignalCallback,
|
|
11
11
|
reportSignalError
|
|
12
|
-
} from "./index-
|
|
12
|
+
} from "./index-sm2tjx06.js";
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
assertUniqueToolName,
|
|
16
|
-
buildToolPresentationSchema,
|
|
14
|
+
WaitTimeoutError,
|
|
17
15
|
coerceJsonArgs,
|
|
18
16
|
collectToolSurface,
|
|
19
17
|
collectTools,
|
|
@@ -25,40 +23,51 @@ import {
|
|
|
25
23
|
executeToolMethod,
|
|
26
24
|
fetchGuarded,
|
|
27
25
|
fetchPinnedDocument,
|
|
28
|
-
flattenToolJsonSchema,
|
|
29
26
|
formatToolError,
|
|
30
|
-
isObjectPresentationSchema,
|
|
31
|
-
presentationMetadata,
|
|
32
27
|
readCapped,
|
|
33
28
|
runWaitOperation,
|
|
34
29
|
toolErrorFromResult,
|
|
35
|
-
toolResultFromError
|
|
36
|
-
|
|
37
|
-
} from "./index-7vb44hy8.js";
|
|
30
|
+
toolResultFromError
|
|
31
|
+
} from "./index-escqg10p.js";
|
|
38
32
|
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} from "./index-yxpe3phd.js";
|
|
33
|
+
ManagedFileError
|
|
34
|
+
} from "./index-bfcpjw20.js";
|
|
35
|
+
import"./index-sbdmyz75.js";
|
|
43
36
|
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
PORTABLE_JSON_SCHEMA_FORMATS,
|
|
38
|
+
assertToolName,
|
|
39
|
+
assertUniqueToolName,
|
|
40
|
+
findNonPortableFormats,
|
|
41
|
+
findUntypedProperties,
|
|
42
|
+
flattenToolJsonSchema,
|
|
43
|
+
prepareProjectedMcpTools,
|
|
44
|
+
presentationMetadata,
|
|
45
|
+
validateMcpRoundPolicy
|
|
46
|
+
} from "./index-6y759j86.js";
|
|
47
|
+
import"./index-cby4ar3v.js";
|
|
48
48
|
import {
|
|
49
49
|
redact
|
|
50
|
-
} from "./index-
|
|
50
|
+
} from "./index-jcc611vh.js";
|
|
51
51
|
import {
|
|
52
|
-
AppError,
|
|
53
52
|
getRequestContext,
|
|
54
53
|
getTraceId,
|
|
55
|
-
|
|
54
|
+
normalizeError,
|
|
56
55
|
runWithRequestContext
|
|
57
|
-
} from "./index-
|
|
56
|
+
} from "./index-j3dem06f.js";
|
|
57
|
+
import {
|
|
58
|
+
AppError,
|
|
59
|
+
STITCH_ERROR_STATUS,
|
|
60
|
+
defineContract
|
|
61
|
+
} from "./index-5r13htq1.js";
|
|
62
|
+
import {
|
|
63
|
+
ManagedFilePathSchema,
|
|
64
|
+
ManagedFileRefSchema
|
|
65
|
+
} from "./index-6k1937bx.js";
|
|
58
66
|
import {
|
|
59
67
|
isRecord,
|
|
60
68
|
resolvePropagationContext
|
|
61
|
-
} from "./index-
|
|
69
|
+
} from "./index-7eq9va6e.js";
|
|
70
|
+
import"./index-1bx83sw4.js";
|
|
62
71
|
|
|
63
72
|
// src/tools/agent.ts
|
|
64
73
|
import { jsonSchema, tool } from "ai";
|
|
@@ -85,10 +94,10 @@ function mountAgent(services, config = {}) {
|
|
|
85
94
|
const inputSchema = jsonSchema(mountable.presentationSchema, {
|
|
86
95
|
validate: async (value) => isRecord(value) ? { success: true, value } : { success: false, error: new Error("Tool arguments must be an object") }
|
|
87
96
|
});
|
|
88
|
-
const execute = async (rawArgs) => {
|
|
97
|
+
const execute = async (rawArgs, options) => {
|
|
89
98
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
90
99
|
try {
|
|
91
|
-
const result = await runTool(mountable, args);
|
|
100
|
+
const result = await runTool(mountable, args, { signal: options.abortSignal });
|
|
92
101
|
if (result.ok)
|
|
93
102
|
return result.data;
|
|
94
103
|
return formatToolError(result, mountable.name, config.errorHint);
|
|
@@ -128,9 +137,408 @@ function mountAgent(services, config = {}) {
|
|
|
128
137
|
}
|
|
129
138
|
return tools;
|
|
130
139
|
}
|
|
131
|
-
// src/tools/
|
|
132
|
-
import { basename, extname, resolve } from "node:path";
|
|
140
|
+
// src/tools/async-operation.ts
|
|
133
141
|
import { z } from "zod";
|
|
142
|
+
|
|
143
|
+
// src/tools/native-definition.ts
|
|
144
|
+
function managedNativeIdentity(identity, method) {
|
|
145
|
+
return { ...identity, method };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/tools/define-wait-tool.ts
|
|
149
|
+
function defineWaitTool(config) {
|
|
150
|
+
const renderedMcp = config.render ? {
|
|
151
|
+
mcp: (state) => {
|
|
152
|
+
const rendered = config.render?.(state, false);
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: rendered?.text ?? JSON.stringify(state, null, 2)
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
} : undefined;
|
|
163
|
+
const present = renderedMcp || config.present ? { ...renderedMcp, ...config.present } : undefined;
|
|
164
|
+
return defineRuntimeTool({
|
|
165
|
+
name: config.name ?? "wait",
|
|
166
|
+
description: config.description,
|
|
167
|
+
identity: managedNativeIdentity(config.identity, "GET"),
|
|
168
|
+
input: config.input,
|
|
169
|
+
output: config.state,
|
|
170
|
+
transports: config.transports,
|
|
171
|
+
annotations: config.annotations,
|
|
172
|
+
present,
|
|
173
|
+
handler: async (context) => {
|
|
174
|
+
let result;
|
|
175
|
+
try {
|
|
176
|
+
result = await runWaitOperation({
|
|
177
|
+
input: context.input,
|
|
178
|
+
poll: async (input, signal) => config.state.parse(await config.poll(input, { ...context, signal })),
|
|
179
|
+
done: config.done,
|
|
180
|
+
backoff: config.backoff,
|
|
181
|
+
timeoutSec: config.timeoutFromInput?.(context.input) ?? config.defaultTimeout,
|
|
182
|
+
signal: context.signal
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error instanceof WaitTimeoutError) {
|
|
186
|
+
throw new AppError("WAIT_TIMEOUT", "Wait timed out before the first snapshot", 408);
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
const rendered = config.render?.(result.state, result.timedOut);
|
|
191
|
+
if (result.timedOut) {
|
|
192
|
+
throw new AppError("WAIT_TIMEOUT", rendered?.text ?? "Wait timed out before the operation completed", 408);
|
|
193
|
+
}
|
|
194
|
+
if (rendered?.isError) {
|
|
195
|
+
throw new AppError("WAIT_FAILED", rendered.text, 409);
|
|
196
|
+
}
|
|
197
|
+
return result.state;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/tools/async-operation.ts
|
|
203
|
+
var AsyncOperationCancelResultSchema = z.discriminatedUnion("outcome", [
|
|
204
|
+
z.object({ outcome: z.literal("accepted") }),
|
|
205
|
+
z.object({ outcome: z.literal("already_terminal") }),
|
|
206
|
+
z.object({ outcome: z.literal("rejected"), reason: z.string().min(1) })
|
|
207
|
+
]);
|
|
208
|
+
function createAsyncOperationSnapshotSchema(config) {
|
|
209
|
+
const progress = config.progress ? { progress: config.progress.optional() } : {};
|
|
210
|
+
return z.discriminatedUnion("phase", [
|
|
211
|
+
z.object({ phase: z.literal("pending"), ...progress }),
|
|
212
|
+
z.object({ phase: z.literal("running"), ...progress }),
|
|
213
|
+
z.object({ phase: z.literal("succeeded"), ...progress }),
|
|
214
|
+
z.object({ phase: z.literal("failed"), failure: config.failure, ...progress }),
|
|
215
|
+
z.object({ phase: z.literal("cancelled"), ...progress })
|
|
216
|
+
]);
|
|
217
|
+
}
|
|
218
|
+
function terminal(snapshot) {
|
|
219
|
+
if (typeof snapshot !== "object" || snapshot === null || !("phase" in snapshot)) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return snapshot.phase === "succeeded" || snapshot.phase === "failed" || snapshot.phase === "cancelled";
|
|
223
|
+
}
|
|
224
|
+
function succeeded(snapshot) {
|
|
225
|
+
return typeof snapshot === "object" && snapshot !== null && "phase" in snapshot && snapshot.phase === "succeeded";
|
|
226
|
+
}
|
|
227
|
+
function defineAsyncOperation(config) {
|
|
228
|
+
const capabilityName = (capability) => config.names?.[capability] ?? `${config.name}_${capability}`;
|
|
229
|
+
const description = (capability) => config.descriptions?.[capability] ?? `${config.description}: ${capability}`;
|
|
230
|
+
const identity = (capability, method) => ({
|
|
231
|
+
serviceName: config.identity.serviceName,
|
|
232
|
+
action: `${config.identity.action}.${capability}`,
|
|
233
|
+
scope: config.scopes?.[capability] ?? config.identity.scope,
|
|
234
|
+
meta: config.identity.meta,
|
|
235
|
+
method
|
|
236
|
+
});
|
|
237
|
+
const common = (capability) => ({
|
|
238
|
+
name: capabilityName(capability),
|
|
239
|
+
description: description(capability),
|
|
240
|
+
transports: config.transports?.[capability],
|
|
241
|
+
annotations: config.annotations?.[capability] ?? (capability === "start" || capability === "cancel" ? { destructiveHint: true } : undefined)
|
|
242
|
+
});
|
|
243
|
+
const inspect = async (capability, context) => {
|
|
244
|
+
await config.authorize(context.input, capability, context);
|
|
245
|
+
const state = config.state.parse(await config.inspect(context.input, context));
|
|
246
|
+
const snapshot = config.snapshot.parse(await config.classify(state, context));
|
|
247
|
+
return { state, snapshot };
|
|
248
|
+
};
|
|
249
|
+
const start = defineRuntimeTool({
|
|
250
|
+
...common("start"),
|
|
251
|
+
identity: identity("start", "POST"),
|
|
252
|
+
input: config.startInput,
|
|
253
|
+
output: config.id,
|
|
254
|
+
handler: (context) => config.start(context.input, context)
|
|
255
|
+
});
|
|
256
|
+
const status = defineRuntimeTool({
|
|
257
|
+
...common("status"),
|
|
258
|
+
identity: identity("status", "GET"),
|
|
259
|
+
input: config.id,
|
|
260
|
+
output: config.snapshot,
|
|
261
|
+
handler: async (context) => (await inspect("status", context)).snapshot
|
|
262
|
+
});
|
|
263
|
+
const wait = defineWaitTool({
|
|
264
|
+
...common("wait"),
|
|
265
|
+
identity: identity("wait", "GET"),
|
|
266
|
+
input: config.id,
|
|
267
|
+
state: config.snapshot,
|
|
268
|
+
poll: async (_id, context) => (await inspect("wait", context)).snapshot,
|
|
269
|
+
done: terminal,
|
|
270
|
+
backoff: config.backoff,
|
|
271
|
+
defaultTimeout: config.defaultTimeout,
|
|
272
|
+
timeoutFromInput: config.timeoutFromId
|
|
273
|
+
});
|
|
274
|
+
const definitions = { start, status, wait };
|
|
275
|
+
const names = new Set;
|
|
276
|
+
if (config.cancel) {
|
|
277
|
+
const capability = config.cancel;
|
|
278
|
+
definitions.cancel = defineRuntimeTool({
|
|
279
|
+
...common("cancel"),
|
|
280
|
+
identity: identity("cancel", "DELETE"),
|
|
281
|
+
input: config.id,
|
|
282
|
+
output: AsyncOperationCancelResultSchema,
|
|
283
|
+
handler: async (context) => {
|
|
284
|
+
const inspected = await inspect("cancel", context);
|
|
285
|
+
return capability.handler(inspected.state, context);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (config.result) {
|
|
290
|
+
const capability = config.result;
|
|
291
|
+
definitions.result = defineRuntimeTool({
|
|
292
|
+
...common("result"),
|
|
293
|
+
identity: identity("result", "GET"),
|
|
294
|
+
input: config.id,
|
|
295
|
+
output: capability.output,
|
|
296
|
+
handler: async (context) => {
|
|
297
|
+
const inspected = await inspect("result", context);
|
|
298
|
+
if (!succeeded(inspected.snapshot)) {
|
|
299
|
+
throw new AppError("OPERATION_NOT_SUCCEEDED", "Operation result is not available", 409);
|
|
300
|
+
}
|
|
301
|
+
return capability.handler(inspected.state, context);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (config.artifacts) {
|
|
306
|
+
const capability = config.artifacts;
|
|
307
|
+
definitions.artifacts = defineRuntimeTool({
|
|
308
|
+
...common("artifacts"),
|
|
309
|
+
identity: identity("artifacts", "GET"),
|
|
310
|
+
input: config.id,
|
|
311
|
+
output: capability.output,
|
|
312
|
+
handler: async (context) => {
|
|
313
|
+
const inspected = await inspect("artifacts", context);
|
|
314
|
+
if (!succeeded(inspected.snapshot)) {
|
|
315
|
+
throw new AppError("OPERATION_NOT_SUCCEEDED", "Operation artifacts are not available", 409);
|
|
316
|
+
}
|
|
317
|
+
return capability.handler(inspected.state, context);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
for (const definition of Object.values(definitions)) {
|
|
322
|
+
assertUniqueToolName(definition.name, names.has(definition.name), "in-process tool name");
|
|
323
|
+
names.add(definition.name);
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
...definitions,
|
|
327
|
+
runtimeTools: Object.values(definitions),
|
|
328
|
+
schemas: {
|
|
329
|
+
id: config.id,
|
|
330
|
+
snapshot: config.snapshot,
|
|
331
|
+
cancelResult: AsyncOperationCancelResultSchema
|
|
332
|
+
}
|
|
333
|
+
};
|
|
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
|
+
}
|
|
449
|
+
function bindContractAsyncOperation(config) {
|
|
450
|
+
const endpoint = (key) => {
|
|
451
|
+
const found = config.contract.endpoints[key];
|
|
452
|
+
if (!found)
|
|
453
|
+
throw new Error(`Contract async operation method "${key}" not found`);
|
|
454
|
+
return found;
|
|
455
|
+
};
|
|
456
|
+
const start = endpoint(config.capabilities.start);
|
|
457
|
+
const status = endpoint(config.capabilities.status);
|
|
458
|
+
const wait = endpoint(config.capabilities.wait);
|
|
459
|
+
if (!start.output || !status.output || !wait.output) {
|
|
460
|
+
throw new Error("Contract async operation requires outputs for start, status and wait");
|
|
461
|
+
}
|
|
462
|
+
const startOutput = start.output;
|
|
463
|
+
const statusOutput = status.output;
|
|
464
|
+
if (wait.output !== statusOutput) {
|
|
465
|
+
throw new Error('Contract async operation capability "wait" output must reuse the same schema instance as the status output');
|
|
466
|
+
}
|
|
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";
|
|
476
|
+
throw new Error(`Contract async operation capability "${capability}" input must reuse the same schema instance as the start output`);
|
|
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);
|
|
522
|
+
};
|
|
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
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
contract: config.contract,
|
|
531
|
+
handlers: config.handlers,
|
|
532
|
+
capabilities: config.capabilities,
|
|
533
|
+
schemas: { id: idSchema, snapshot: statusOutput },
|
|
534
|
+
adapters: {
|
|
535
|
+
idFromStart: (output) => adapterResult("start", "id", idSchema, adapters.idFromStart(output)),
|
|
536
|
+
inputFor
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
// src/tools/download-core.ts
|
|
541
|
+
import { basename, extname } from "node:path";
|
|
134
542
|
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
135
543
|
var MIME_EXT = {
|
|
136
544
|
"image/png": ".png",
|
|
@@ -143,11 +551,6 @@ var MIME_EXT = {
|
|
|
143
551
|
"audio/wav": ".wav",
|
|
144
552
|
"audio/ogg": ".ogg"
|
|
145
553
|
};
|
|
146
|
-
var DownloadResultSchema = z.object({
|
|
147
|
-
path: z.string(),
|
|
148
|
-
size: z.number().int().nonnegative(),
|
|
149
|
-
mimeType: z.string()
|
|
150
|
-
});
|
|
151
554
|
|
|
152
555
|
class DownloadOperationError extends Error {
|
|
153
556
|
code;
|
|
@@ -192,19 +595,42 @@ async function runDownloadOperation(config) {
|
|
|
192
595
|
await res.body?.cancel();
|
|
193
596
|
throw new DownloadOperationError("DOWNLOAD_TOO_LARGE", `file exceeds the ${max}-byte cap`, 413);
|
|
194
597
|
}
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
await writeDownload(dir, filePath, buffer);
|
|
202
|
-
return { path: filePath, size: buffer.length, mimeType };
|
|
598
|
+
const path = config.path ?? `${baseNameFor(config.url)}${extensionFor(config.url, mimeType)}`;
|
|
599
|
+
return config.files.write(path, res.body ?? new Uint8Array, {
|
|
600
|
+
maxBytes: max,
|
|
601
|
+
mediaType: mimeType,
|
|
602
|
+
signal: config.signal
|
|
603
|
+
});
|
|
203
604
|
}
|
|
204
605
|
|
|
205
|
-
// src/tools/
|
|
206
|
-
|
|
207
|
-
|
|
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);
|
|
208
634
|
}
|
|
209
635
|
|
|
210
636
|
// src/tools/define-download-tool.ts
|
|
@@ -214,7 +640,7 @@ function defineDownloadTool(config) {
|
|
|
214
640
|
description: config.description,
|
|
215
641
|
identity: managedNativeIdentity(config.identity, "POST"),
|
|
216
642
|
input: config.input,
|
|
217
|
-
output:
|
|
643
|
+
output: ManagedFileRefSchema,
|
|
218
644
|
transports: config.transports,
|
|
219
645
|
annotations: config.annotations,
|
|
220
646
|
present: config.present,
|
|
@@ -226,7 +652,8 @@ function defineDownloadTool(config) {
|
|
|
226
652
|
try {
|
|
227
653
|
return await runDownloadOperation({
|
|
228
654
|
url,
|
|
229
|
-
|
|
655
|
+
files: config.files,
|
|
656
|
+
path: config.pathFromInput?.(context.input),
|
|
230
657
|
allowPrivateHosts: config.allowPrivateHosts,
|
|
231
658
|
maxBytes: config.maxBytes,
|
|
232
659
|
timeoutMs: config.timeoutMs,
|
|
@@ -236,6 +663,9 @@ function defineDownloadTool(config) {
|
|
|
236
663
|
if (error instanceof DownloadOperationError) {
|
|
237
664
|
throw new AppError(error.code, `Download failed: ${error.message}`, error.status);
|
|
238
665
|
}
|
|
666
|
+
const managedError = managedFileAppError(error);
|
|
667
|
+
if (managedError)
|
|
668
|
+
throw managedError;
|
|
239
669
|
throw error;
|
|
240
670
|
}
|
|
241
671
|
}
|
|
@@ -245,14 +675,14 @@ function defineDownloadTool(config) {
|
|
|
245
675
|
import { z as z2 } from "zod";
|
|
246
676
|
|
|
247
677
|
// src/tools/upload-core.ts
|
|
248
|
-
function runUploadOperation(path, upload, signal) {
|
|
678
|
+
function runUploadOperation(files, path, upload, signal) {
|
|
249
679
|
signal?.throwIfAborted();
|
|
250
|
-
return
|
|
680
|
+
return files.read(path, { signal }).then((source) => upload(source, signal));
|
|
251
681
|
}
|
|
252
682
|
|
|
253
683
|
// src/tools/define-upload-tool.ts
|
|
254
684
|
var UploadToolInputSchema = z2.object({
|
|
255
|
-
path:
|
|
685
|
+
path: ManagedFilePathSchema.describe("Path relative to the configured file boundary")
|
|
256
686
|
});
|
|
257
687
|
function defineUploadTool(config) {
|
|
258
688
|
return defineRuntimeTool({
|
|
@@ -264,12 +694,20 @@ function defineUploadTool(config) {
|
|
|
264
694
|
transports: config.transports,
|
|
265
695
|
annotations: config.annotations,
|
|
266
696
|
present: config.present,
|
|
267
|
-
handler: async (context) =>
|
|
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
|
+
}
|
|
268
707
|
});
|
|
269
708
|
}
|
|
270
709
|
// src/tools/view-file.ts
|
|
271
|
-
import {
|
|
272
|
-
import { extname as extname2, resolve as resolve2 } from "node:path";
|
|
710
|
+
import { extname as extname2 } from "node:path";
|
|
273
711
|
import { z as z3 } from "zod";
|
|
274
712
|
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
275
713
|
var MAX_VIEW_FILES = 20;
|
|
@@ -323,7 +761,7 @@ async function fetchSource(pathOrUrl, options, maxBytes) {
|
|
|
323
761
|
signal: options.signal
|
|
324
762
|
});
|
|
325
763
|
if (!res.ok)
|
|
326
|
-
throw new
|
|
764
|
+
throw new AppError("VIEW_HTTP_ERROR", `HTTP ${res.status}`, 502);
|
|
327
765
|
const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
328
766
|
const mimeType = headerMime || extMime || "application/octet-stream";
|
|
329
767
|
if (mimeType.startsWith("video/")) {
|
|
@@ -335,32 +773,33 @@ async function fetchSource(pathOrUrl, options, maxBytes) {
|
|
|
335
773
|
await res.body?.cancel();
|
|
336
774
|
return { tooLarge: true, mimeType, bytesRead: 0 };
|
|
337
775
|
}
|
|
338
|
-
const
|
|
339
|
-
return
|
|
776
|
+
const buffer = await readCapped(res, maxBytes);
|
|
777
|
+
return buffer ? { buffer, mimeType, bytesRead: buffer.length } : { tooLarge: true, mimeType, bytesRead: maxBytes };
|
|
340
778
|
}
|
|
341
|
-
if (!options.
|
|
342
|
-
throw new
|
|
779
|
+
if (!options.files) {
|
|
780
|
+
throw new AppError("FILE_INVALID_PATH", "local file paths are disabled — set files to allow them", 400);
|
|
343
781
|
}
|
|
344
782
|
options.signal?.throwIfAborted();
|
|
345
783
|
if (!extMime) {
|
|
346
|
-
throw new
|
|
347
|
-
}
|
|
348
|
-
const root = resolve2(options.baseDir);
|
|
349
|
-
const target = resolve2(root, pathOrUrl);
|
|
350
|
-
if (!isWithinDir(root, target)) {
|
|
351
|
-
throw new Error("path escapes the allowed directory");
|
|
784
|
+
throw new AppError("FILE_INSPECTION_REJECTED", "refusing to read a non-media file", 422);
|
|
352
785
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
786
|
+
try {
|
|
787
|
+
const source = await options.files.read(pathOrUrl, {
|
|
788
|
+
maxBytes,
|
|
789
|
+
signal: options.signal
|
|
790
|
+
});
|
|
791
|
+
const buffer = Buffer.from(source.bytes);
|
|
792
|
+
return {
|
|
793
|
+
buffer,
|
|
794
|
+
mimeType: source.ref.mediaType ?? extMime,
|
|
795
|
+
bytesRead: buffer.length
|
|
796
|
+
};
|
|
797
|
+
} catch (error) {
|
|
798
|
+
if (error instanceof ManagedFileError && error.code === "FILE_TOO_LARGE") {
|
|
799
|
+
return { tooLarge: true, mimeType: extMime, bytesRead: maxBytes };
|
|
800
|
+
}
|
|
801
|
+
throw error;
|
|
356
802
|
}
|
|
357
|
-
const info = await stat(realTarget).catch(() => null);
|
|
358
|
-
if (!info?.isFile())
|
|
359
|
-
throw new Error("file not found");
|
|
360
|
-
if (info.size > maxBytes)
|
|
361
|
-
return { tooLarge: true, mimeType: extMime, bytesRead: 0 };
|
|
362
|
-
const buffer = await readFile(realTarget, { signal: options.signal });
|
|
363
|
-
return { buffer, mimeType: extMime, bytesRead: buffer.length };
|
|
364
803
|
}
|
|
365
804
|
async function resolveMediaWithinBudget(pathOrUrl, options, maxBytes) {
|
|
366
805
|
if (maxBytes <= 0) {
|
|
@@ -448,7 +887,7 @@ async function runViewFileOperation(paths, options = {}, signal) {
|
|
|
448
887
|
} catch (error) {
|
|
449
888
|
if (signal?.aborted)
|
|
450
889
|
throw signal.reason ?? error;
|
|
451
|
-
const message = error
|
|
890
|
+
const message = normalizeFileToolError(error).message;
|
|
452
891
|
errors.push({ path: pathOrUrl, message });
|
|
453
892
|
content.push({ type: "text", text: `[${pathOrUrl}] Error: ${message}` });
|
|
454
893
|
}
|
|
@@ -497,57 +936,12 @@ function defineViewFileTool(config) {
|
|
|
497
936
|
},
|
|
498
937
|
present: { ...defaults, ...config.present },
|
|
499
938
|
handler: (context) => runViewFileOperation(context.input.paths, {
|
|
500
|
-
|
|
939
|
+
files: config.files,
|
|
501
940
|
allowPrivateHosts: config.allowPrivateHosts,
|
|
502
941
|
timeoutMs: config.timeoutMs
|
|
503
942
|
}, context.signal)
|
|
504
943
|
});
|
|
505
944
|
}
|
|
506
|
-
// src/tools/define-wait-tool.ts
|
|
507
|
-
function defineWaitTool(config) {
|
|
508
|
-
const renderedMcp = config.render ? {
|
|
509
|
-
mcp: (state) => {
|
|
510
|
-
const rendered = config.render?.(state, false);
|
|
511
|
-
return {
|
|
512
|
-
content: [
|
|
513
|
-
{
|
|
514
|
-
type: "text",
|
|
515
|
-
text: rendered?.text ?? JSON.stringify(state, null, 2)
|
|
516
|
-
}
|
|
517
|
-
]
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
} : undefined;
|
|
521
|
-
const present = renderedMcp || config.present ? { ...renderedMcp, ...config.present } : undefined;
|
|
522
|
-
return defineRuntimeTool({
|
|
523
|
-
name: config.name ?? "wait",
|
|
524
|
-
description: config.description,
|
|
525
|
-
identity: managedNativeIdentity(config.identity, "GET"),
|
|
526
|
-
input: config.input,
|
|
527
|
-
output: config.state,
|
|
528
|
-
transports: config.transports,
|
|
529
|
-
annotations: config.annotations,
|
|
530
|
-
present,
|
|
531
|
-
handler: async (context) => {
|
|
532
|
-
const result = await runWaitOperation({
|
|
533
|
-
input: context.input,
|
|
534
|
-
poll: async (input) => config.state.parse(await config.poll(input, context)),
|
|
535
|
-
done: config.done,
|
|
536
|
-
backoff: config.backoff,
|
|
537
|
-
timeoutSec: config.timeoutFromInput?.(context.input) ?? config.defaultTimeout,
|
|
538
|
-
signal: context.signal
|
|
539
|
-
});
|
|
540
|
-
const rendered = config.render?.(result.state, result.timedOut);
|
|
541
|
-
if (result.timedOut) {
|
|
542
|
-
throw new AppError("WAIT_TIMEOUT", rendered?.text ?? "Wait timed out before the operation completed", 408);
|
|
543
|
-
}
|
|
544
|
-
if (rendered?.isError) {
|
|
545
|
-
throw new AppError("WAIT_FAILED", rendered.text, 409);
|
|
546
|
-
}
|
|
547
|
-
return result.state;
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
945
|
// src/tools/invoker.ts
|
|
552
946
|
function createToolInvoker(services, config) {
|
|
553
947
|
const serviceList = Array.isArray(services) ? services : [services];
|
|
@@ -595,6 +989,24 @@ function createToolInvoker(services, config) {
|
|
|
595
989
|
}
|
|
596
990
|
});
|
|
597
991
|
}
|
|
992
|
+
// src/tools/lifecycle.ts
|
|
993
|
+
function composeToolLifecycle(...hooks) {
|
|
994
|
+
return {
|
|
995
|
+
beforeHandle: async (ctx, endpoint) => {
|
|
996
|
+
for (const hook of hooks)
|
|
997
|
+
await hook?.beforeHandle?.(ctx, endpoint);
|
|
998
|
+
},
|
|
999
|
+
afterHandle: async (ctx, result, endpoint) => {
|
|
1000
|
+
let current = result;
|
|
1001
|
+
for (const hook of hooks) {
|
|
1002
|
+
const transformed = await hook?.afterHandle?.(ctx, current, endpoint);
|
|
1003
|
+
if (transformed !== undefined)
|
|
1004
|
+
current = transformed;
|
|
1005
|
+
}
|
|
1006
|
+
return current;
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
598
1010
|
// src/tools/list-names.ts
|
|
599
1011
|
var TOOL_TRANSPORTS = ["MCP", "AGENT", "CLI"];
|
|
600
1012
|
function listToolNames(surface) {
|
|
@@ -667,6 +1079,95 @@ function inlineMcpAppBundle(html) {
|
|
|
667
1079
|
return html.replace(EXT_APPS_BUNDLE_PLACEHOLDER, () => bundle);
|
|
668
1080
|
}
|
|
669
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
|
+
|
|
670
1171
|
// src/tools/mcp-round.ts
|
|
671
1172
|
import {
|
|
672
1173
|
acceptedContent,
|
|
@@ -746,29 +1247,7 @@ async function runRoundFailure(tool2, rawArgs, runTool, context, code, message)
|
|
|
746
1247
|
throw new AppError(code, message, 400);
|
|
747
1248
|
}
|
|
748
1249
|
}
|
|
749
|
-
}, rawArgs, context);
|
|
750
|
-
}
|
|
751
|
-
function validateMcpRoundPolicy(tool2, policy, runtime) {
|
|
752
|
-
if (!runtime?.stateConfigured) {
|
|
753
|
-
throw new Error(`[stitchkit] MCP tool "${tool2.name}" declares inputRequired but no multiRound.state key is configured`);
|
|
754
|
-
}
|
|
755
|
-
const { maxRounds } = runtime;
|
|
756
|
-
if (!Number.isInteger(maxRounds) || maxRounds < 1) {
|
|
757
|
-
throw new Error("[stitchkit] multiRound.serving.maxRounds must be a positive integer");
|
|
758
|
-
}
|
|
759
|
-
if (policy.inputRequired.length === 0) {
|
|
760
|
-
throw new Error(`[stitchkit] MCP tool "${tool2.name}" must declare at least one input round`);
|
|
761
|
-
}
|
|
762
|
-
if (policy.inputRequired.length > maxRounds) {
|
|
763
|
-
throw new Error(`[stitchkit] MCP tool "${tool2.name}" declares ${policy.inputRequired.length} input rounds, exceeding maxRounds ${maxRounds}`);
|
|
764
|
-
}
|
|
765
|
-
const keys = new Set;
|
|
766
|
-
for (const request of policy.inputRequired) {
|
|
767
|
-
if (keys.has(request.key)) {
|
|
768
|
-
throw new Error(`[stitchkit] MCP tool "${tool2.name}" declares duplicate input key "${request.key}"`);
|
|
769
|
-
}
|
|
770
|
-
keys.add(request.key);
|
|
771
|
-
}
|
|
1250
|
+
}, rawArgs, context);
|
|
772
1251
|
}
|
|
773
1252
|
async function failedResolution(options) {
|
|
774
1253
|
const result = await runRoundFailure(options.tool, options.rawArgs, options.runTool, options.context, options.code, options.message);
|
|
@@ -902,329 +1381,6 @@ async function resolveMcpRound(options) {
|
|
|
902
1381
|
};
|
|
903
1382
|
}
|
|
904
1383
|
|
|
905
|
-
// src/tools/portable-formats.ts
|
|
906
|
-
var PORTABLE_JSON_SCHEMA_FORMATS = new Set([
|
|
907
|
-
"date",
|
|
908
|
-
"time",
|
|
909
|
-
"date-time",
|
|
910
|
-
"duration",
|
|
911
|
-
"uri",
|
|
912
|
-
"uri-reference",
|
|
913
|
-
"uri-template",
|
|
914
|
-
"email",
|
|
915
|
-
"hostname",
|
|
916
|
-
"ipv4",
|
|
917
|
-
"ipv6",
|
|
918
|
-
"regex",
|
|
919
|
-
"uuid",
|
|
920
|
-
"json-pointer",
|
|
921
|
-
"relative-json-pointer"
|
|
922
|
-
]);
|
|
923
|
-
function propertyPath(prefix, property) {
|
|
924
|
-
return prefix ? `${prefix}.${property}` : property;
|
|
925
|
-
}
|
|
926
|
-
function keywordPath(prefix, keyword) {
|
|
927
|
-
if (prefix && keyword.startsWith("["))
|
|
928
|
-
return `${prefix}${keyword}`;
|
|
929
|
-
return prefix ? `${prefix}.${keyword}` : keyword;
|
|
930
|
-
}
|
|
931
|
-
function findNonPortableFormats(schema, allowFormats = []) {
|
|
932
|
-
const allowed = new Set([...PORTABLE_JSON_SCHEMA_FORMATS, ...allowFormats]);
|
|
933
|
-
const found = [];
|
|
934
|
-
const pending = [{ value: schema, path: "" }];
|
|
935
|
-
while (pending.length > 0) {
|
|
936
|
-
const current = pending.pop();
|
|
937
|
-
if (!current || !isRecord(current.value))
|
|
938
|
-
continue;
|
|
939
|
-
const format = current.value.format;
|
|
940
|
-
if (typeof format === "string" && !allowed.has(format)) {
|
|
941
|
-
found.push({ path: current.path || "(root)", format });
|
|
942
|
-
}
|
|
943
|
-
const properties = current.value.properties;
|
|
944
|
-
if (isRecord(properties)) {
|
|
945
|
-
for (const [key, value] of Object.entries(properties)) {
|
|
946
|
-
pending.push({ value, path: propertyPath(current.path, key) });
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
for (const mapKey of ["$defs", "definitions", "patternProperties", "dependentSchemas"]) {
|
|
950
|
-
const entries = current.value[mapKey];
|
|
951
|
-
if (!isRecord(entries))
|
|
952
|
-
continue;
|
|
953
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
954
|
-
pending.push({ value, path: keywordPath(current.path, `${mapKey}.${key}`) });
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
const directChildren = [
|
|
958
|
-
["items", "[]"],
|
|
959
|
-
["contains", "contains"],
|
|
960
|
-
["additionalProperties", "*"],
|
|
961
|
-
["unevaluatedProperties", "*"],
|
|
962
|
-
["propertyNames", "propertyNames"],
|
|
963
|
-
["not", "not"],
|
|
964
|
-
["if", "if"],
|
|
965
|
-
["then", "then"],
|
|
966
|
-
["else", "else"]
|
|
967
|
-
];
|
|
968
|
-
for (const [keyword, segment] of directChildren) {
|
|
969
|
-
const child = current.value[keyword];
|
|
970
|
-
if (isRecord(child)) {
|
|
971
|
-
pending.push({ value: child, path: keywordPath(current.path, segment) });
|
|
972
|
-
}
|
|
973
|
-
if (Array.isArray(child)) {
|
|
974
|
-
child.forEach((value, index) => {
|
|
975
|
-
pending.push({ value, path: keywordPath(current.path, `${segment}[${index}]`) });
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
for (const keyword of ["prefixItems", "allOf", "anyOf", "oneOf"]) {
|
|
980
|
-
const branches = current.value[keyword];
|
|
981
|
-
if (!Array.isArray(branches))
|
|
982
|
-
continue;
|
|
983
|
-
branches.forEach((value, index) => {
|
|
984
|
-
pending.push({ value, path: keywordPath(current.path, `${keyword}[${index}]`) });
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
return found.sort((left, right) => left.path.localeCompare(right.path) || left.format.localeCompare(right.format));
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// src/tools/untyped-properties.ts
|
|
992
|
-
var TYPE_KEYWORDS = ["type", "enum", "const", "anyOf", "oneOf", "allOf", "$ref", "not"];
|
|
993
|
-
function saysWhatItIs(schema) {
|
|
994
|
-
return TYPE_KEYWORDS.some((keyword) => schema[keyword] !== undefined);
|
|
995
|
-
}
|
|
996
|
-
function findUntypedProperties(schema, prefix = "") {
|
|
997
|
-
if (!isRecord(schema))
|
|
998
|
-
return [];
|
|
999
|
-
const found = [];
|
|
1000
|
-
const properties = schema.properties;
|
|
1001
|
-
if (isRecord(properties)) {
|
|
1002
|
-
for (const [key, value] of Object.entries(properties)) {
|
|
1003
|
-
const path = prefix ? `${prefix}.${key}` : key;
|
|
1004
|
-
if (isRecord(value)) {
|
|
1005
|
-
if (!saysWhatItIs(value)) {
|
|
1006
|
-
const description = value.description;
|
|
1007
|
-
found.push({
|
|
1008
|
-
path,
|
|
1009
|
-
...typeof description === "string" && { description }
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
found.push(...findUntypedProperties(value, path));
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
for (const key of ["items", "additionalProperties"]) {
|
|
1017
|
-
const child = schema[key];
|
|
1018
|
-
if (isRecord(child))
|
|
1019
|
-
found.push(...findUntypedProperties(child, prefix));
|
|
1020
|
-
if (Array.isArray(child)) {
|
|
1021
|
-
for (const entry of child) {
|
|
1022
|
-
if (isRecord(entry))
|
|
1023
|
-
found.push(...findUntypedProperties(entry, prefix));
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
for (const key of ["prefixItems"]) {
|
|
1028
|
-
const child = schema[key];
|
|
1029
|
-
if (Array.isArray(child)) {
|
|
1030
|
-
for (const entry of child) {
|
|
1031
|
-
if (isRecord(entry))
|
|
1032
|
-
found.push(...findUntypedProperties(entry, prefix));
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
const patterned = schema.patternProperties;
|
|
1037
|
-
if (isRecord(patterned)) {
|
|
1038
|
-
for (const value of Object.values(patterned)) {
|
|
1039
|
-
if (isRecord(value))
|
|
1040
|
-
found.push(...findUntypedProperties(value, prefix));
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
for (const key of ["$defs", "definitions"]) {
|
|
1044
|
-
const container = schema[key];
|
|
1045
|
-
if (!isRecord(container))
|
|
1046
|
-
continue;
|
|
1047
|
-
for (const definition of Object.values(container)) {
|
|
1048
|
-
if (isRecord(definition))
|
|
1049
|
-
found.push(...findUntypedProperties(definition, prefix));
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
for (const key of ["allOf", "anyOf", "oneOf"]) {
|
|
1053
|
-
const branches = schema[key];
|
|
1054
|
-
if (Array.isArray(branches)) {
|
|
1055
|
-
for (const branch of branches)
|
|
1056
|
-
found.push(...findUntypedProperties(branch, prefix));
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
return found;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// src/tools/mcp-prepare.ts
|
|
1063
|
-
function textBlock(text) {
|
|
1064
|
-
return [{ type: "text", text }];
|
|
1065
|
-
}
|
|
1066
|
-
function formatMcpResult(result, mode, toolName, errorHint) {
|
|
1067
|
-
if (result.ok) {
|
|
1068
|
-
if (mode === "none")
|
|
1069
|
-
return { content: [] };
|
|
1070
|
-
let serialized;
|
|
1071
|
-
try {
|
|
1072
|
-
serialized = JSON.stringify(result.data, null, 2);
|
|
1073
|
-
} catch {
|
|
1074
|
-
serialized = undefined;
|
|
1075
|
-
}
|
|
1076
|
-
if (serialized === undefined) {
|
|
1077
|
-
const safe = redact(result.data, { sensitiveKeys: /(?!)/ });
|
|
1078
|
-
return { content: textBlock(JSON.stringify(safe, null, 2)), structuredContent: safe };
|
|
1079
|
-
}
|
|
1080
|
-
return { content: textBlock(serialized), structuredContent: result.data };
|
|
1081
|
-
}
|
|
1082
|
-
return {
|
|
1083
|
-
content: textBlock(JSON.stringify(formatToolError(result, toolName, errorHint), null, 2)),
|
|
1084
|
-
isError: true
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
function probeSchema(schema, io) {
|
|
1088
|
-
try {
|
|
1089
|
-
toJsonSchema(schema, io);
|
|
1090
|
-
return null;
|
|
1091
|
-
} catch (err) {
|
|
1092
|
-
return err instanceof Error ? err.message : String(err);
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
function resolveOutputSchema(outputSchema) {
|
|
1096
|
-
if (!outputSchema)
|
|
1097
|
-
return null;
|
|
1098
|
-
return { schema: outputSchema, mode: "direct" };
|
|
1099
|
-
}
|
|
1100
|
-
function reportIncompatible(message, policy, logger, failures) {
|
|
1101
|
-
if (policy === "throw") {
|
|
1102
|
-
failures.push(message);
|
|
1103
|
-
} else if (policy === "warn") {
|
|
1104
|
-
if (logger)
|
|
1105
|
-
logger.warn(`[stitchkit] ${message}`);
|
|
1106
|
-
else
|
|
1107
|
-
console.warn(`[stitchkit] ${message}`);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
function throwIfFailures(failures) {
|
|
1111
|
-
if (failures.length > 0) {
|
|
1112
|
-
throw new Error(`[stitchkit] ${failures.length} problem(s) with MCP tool schemas:
|
|
1113
|
-
- ${failures.join(`
|
|
1114
|
-
- `)}`);
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
function prepareMcpTool(mountable, config, validation, logger, failures, seen) {
|
|
1118
|
-
const policy = validation.policy ?? "throw";
|
|
1119
|
-
assertUniqueToolName(mountable.name, seen.has(mountable.name), "MCP tool name");
|
|
1120
|
-
seen.add(mountable.name);
|
|
1121
|
-
if (mountable.method.mcp) {
|
|
1122
|
-
validateMcpRoundPolicy(mountable, mountable.method.mcp, config.multiRound);
|
|
1123
|
-
}
|
|
1124
|
-
let inputJsonSchema;
|
|
1125
|
-
try {
|
|
1126
|
-
inputJsonSchema = buildToolPresentationSchema({
|
|
1127
|
-
paramsSchema: mountable.method.paramsSchema,
|
|
1128
|
-
inputSchema: mountable.method.inputSchema,
|
|
1129
|
-
extendSchema: mountable.shouldExtend && config.extend ? config.extend.schema : undefined,
|
|
1130
|
-
flattenUnionInput: config.flattenUnionInput,
|
|
1131
|
-
unrepresentable: "throw"
|
|
1132
|
-
});
|
|
1133
|
-
} catch (err) {
|
|
1134
|
-
reportIncompatible(`MCP tool "${mountable.name}" — input schema is not JSON Schema-compatible: ${err instanceof Error ? err.message : String(err)}`, policy, logger, failures);
|
|
1135
|
-
return null;
|
|
1136
|
-
}
|
|
1137
|
-
if (!isObjectPresentationSchema(inputJsonSchema)) {
|
|
1138
|
-
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);
|
|
1139
|
-
return null;
|
|
1140
|
-
}
|
|
1141
|
-
if (validation.requireTypedProperties) {
|
|
1142
|
-
const allowed = new Set(validation.allowUntyped ?? []);
|
|
1143
|
-
for (const untyped of findUntypedProperties(inputJsonSchema)) {
|
|
1144
|
-
const path = `${mountable.name}.${untyped.path}`;
|
|
1145
|
-
if (allowed.has(path))
|
|
1146
|
-
continue;
|
|
1147
|
-
const clue = untyped.description ? ` (only a description: "${untyped.description}")` : "";
|
|
1148
|
-
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);
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
if (validation.requirePortableFormats) {
|
|
1152
|
-
for (const finding of findNonPortableFormats(inputJsonSchema, validation.allowFormats)) {
|
|
1153
|
-
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);
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
const resolved = resolveOutputSchema(mountable.method.outputSchema);
|
|
1157
|
-
if (!resolved)
|
|
1158
|
-
return { mountable, inputSchema: inputJsonSchema, outputMode: "none" };
|
|
1159
|
-
const outputError = probeSchema(resolved.schema, "output");
|
|
1160
|
-
if (outputError) {
|
|
1161
|
-
reportIncompatible(`MCP tool "${mountable.name}" — output schema is not JSON Schema-compatible: ${outputError}`, policy, logger, failures);
|
|
1162
|
-
return { mountable, inputSchema: inputJsonSchema, outputMode: "none" };
|
|
1163
|
-
}
|
|
1164
|
-
if (validation.requirePortableFormats) {
|
|
1165
|
-
for (const finding of findNonPortableFormats(toJsonSchema(resolved.schema, "output"), validation.allowFormats)) {
|
|
1166
|
-
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);
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
return {
|
|
1170
|
-
mountable,
|
|
1171
|
-
inputSchema: inputJsonSchema,
|
|
1172
|
-
outputSchema: resolved.schema,
|
|
1173
|
-
outputMode: resolved.mode
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
function prepareMcpSurface(services, config = {}) {
|
|
1177
|
-
const serviceList = Array.isArray(services) ? services : [services];
|
|
1178
|
-
const tools = serviceList.flatMap((service) => collectTools(service, "MCP", config));
|
|
1179
|
-
return prepareMcpTools(tools, config);
|
|
1180
|
-
}
|
|
1181
|
-
function prepareMcpTools(tools, config = {}) {
|
|
1182
|
-
const seen = new Set;
|
|
1183
|
-
const failures = [];
|
|
1184
|
-
const prepared = [];
|
|
1185
|
-
for (const mountable of tools) {
|
|
1186
|
-
const tool2 = prepareMcpTool(mountable, config, config.schemaValidation ?? {}, config.logger, failures, seen);
|
|
1187
|
-
if (tool2) {
|
|
1188
|
-
Object.freeze(tool2.mountable);
|
|
1189
|
-
prepared.push(Object.freeze(tool2));
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
throwIfFailures(failures);
|
|
1193
|
-
return Object.freeze(prepared);
|
|
1194
|
-
}
|
|
1195
|
-
function prepareMcpServerSurface(surface, config = {}) {
|
|
1196
|
-
const contractMountables = [];
|
|
1197
|
-
const definitions = [];
|
|
1198
|
-
const runtimeMountables = [];
|
|
1199
|
-
for (const entry of collectToolSurface({
|
|
1200
|
-
surface,
|
|
1201
|
-
transport: "MCP",
|
|
1202
|
-
extend: config.extend,
|
|
1203
|
-
flattenUnionInput: config.flattenUnionInput
|
|
1204
|
-
})) {
|
|
1205
|
-
if (entry.kind === "contract") {
|
|
1206
|
-
contractMountables.push(entry.mountable);
|
|
1207
|
-
} else {
|
|
1208
|
-
definitions.push(entry.definition);
|
|
1209
|
-
runtimeMountables.push(entry.mountable);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
const contractTools = prepareMcpTools(contractMountables, config);
|
|
1213
|
-
const runtimeDescriptors = prepareMcpTools(runtimeMountables, {
|
|
1214
|
-
schemaValidation: config.schemaValidation,
|
|
1215
|
-
logger: config.logger,
|
|
1216
|
-
multiRound: config.multiRound
|
|
1217
|
-
});
|
|
1218
|
-
const descriptorsByName = new Map(runtimeDescriptors.map((descriptor) => [descriptor.mountable.name, descriptor]));
|
|
1219
|
-
const runtimeTools = [];
|
|
1220
|
-
for (const definition of definitions) {
|
|
1221
|
-
const descriptor = descriptorsByName.get(definition.name);
|
|
1222
|
-
if (descriptor)
|
|
1223
|
-
runtimeTools.push(Object.freeze({ definition, descriptor }));
|
|
1224
|
-
}
|
|
1225
|
-
return Object.freeze({ contractTools, runtimeTools: Object.freeze(runtimeTools) });
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
1384
|
// src/tools/mcp-trace.ts
|
|
1229
1385
|
import {
|
|
1230
1386
|
BAGGAGE_META_KEY,
|
|
@@ -1610,8 +1766,8 @@ function createMcpHandler(config) {
|
|
|
1610
1766
|
const authByCarrier = new WeakMap;
|
|
1611
1767
|
let closed = false;
|
|
1612
1768
|
let resolveClosed;
|
|
1613
|
-
const closedSignal = new Promise((
|
|
1614
|
-
resolveClosed =
|
|
1769
|
+
const closedSignal = new Promise((resolve) => {
|
|
1770
|
+
resolveClosed = resolve;
|
|
1615
1771
|
});
|
|
1616
1772
|
const sdkHandler = createSdkMcpHandler(({ authInfo }) => {
|
|
1617
1773
|
const resolved = authInfo ? authByCarrier.get(authInfo) : undefined;
|
|
@@ -1714,8 +1870,8 @@ function bindStdioProcessSignals(handle, options = {}) {
|
|
|
1714
1870
|
let rejectChain = () => {
|
|
1715
1871
|
return;
|
|
1716
1872
|
};
|
|
1717
|
-
const promise = new Promise((
|
|
1718
|
-
resolveChain =
|
|
1873
|
+
const promise = new Promise((resolve, reject) => {
|
|
1874
|
+
resolveChain = resolve;
|
|
1719
1875
|
rejectChain = reject;
|
|
1720
1876
|
});
|
|
1721
1877
|
promise.catch(() => {
|
|
@@ -1811,44 +1967,51 @@ function mountDownload(server, config) {
|
|
|
1811
1967
|
return textResult("Nothing to download.", true);
|
|
1812
1968
|
const result = await runDownloadOperation({
|
|
1813
1969
|
url,
|
|
1814
|
-
|
|
1970
|
+
files: config.files,
|
|
1971
|
+
path: config.pathFromArgs?.(args),
|
|
1815
1972
|
allowPrivateHosts: config.allowPrivateHosts,
|
|
1816
1973
|
maxBytes: config.maxBytes,
|
|
1817
1974
|
timeoutMs: config.timeoutMs
|
|
1818
1975
|
});
|
|
1819
1976
|
return textResult(JSON.stringify(result, null, 2));
|
|
1820
|
-
} catch (
|
|
1821
|
-
|
|
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);
|
|
1822
1983
|
}
|
|
1823
1984
|
});
|
|
1824
1985
|
}
|
|
1825
1986
|
// src/tools/mount-upload.ts
|
|
1826
|
-
import { z as z7 } from "zod";
|
|
1827
1987
|
function mountUpload(server, config) {
|
|
1828
1988
|
const name = config.name ?? "upload";
|
|
1829
1989
|
assertToolName(name, "<native>", "upload");
|
|
1830
1990
|
server.registerTool(name, {
|
|
1831
1991
|
description: config.description,
|
|
1832
|
-
inputSchema: {
|
|
1992
|
+
inputSchema: {
|
|
1993
|
+
path: ManagedFilePathSchema.describe("Path relative to the configured file boundary")
|
|
1994
|
+
}
|
|
1833
1995
|
}, async (rawArgs) => {
|
|
1834
1996
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
1835
1997
|
const path = typeof args.path === "string" ? args.path : "";
|
|
1836
1998
|
if (!path)
|
|
1837
1999
|
return textResult("Provide `path`.", true);
|
|
1838
2000
|
try {
|
|
1839
|
-
const uploaded = await runUploadOperation(path, config.upload);
|
|
2001
|
+
const uploaded = await runUploadOperation(config.files, path, config.upload);
|
|
1840
2002
|
return textResult(JSON.stringify(uploaded ?? null, null, 2));
|
|
1841
|
-
} catch (
|
|
1842
|
-
|
|
2003
|
+
} catch (error) {
|
|
2004
|
+
const normalized = normalizeFileToolError(error);
|
|
2005
|
+
return textResult(`Upload failed [${normalized.code}]: ${normalized.message}`, true);
|
|
1843
2006
|
}
|
|
1844
2007
|
});
|
|
1845
2008
|
}
|
|
1846
2009
|
// src/tools/mount-wait.ts
|
|
1847
|
-
import { z as
|
|
2010
|
+
import { z as z7 } from "zod";
|
|
1848
2011
|
function mountWait(server, config) {
|
|
1849
2012
|
const name = config.name ?? "wait";
|
|
1850
2013
|
assertToolName(name, "<native>", "wait");
|
|
1851
|
-
server.registerTool(name, { description: config.description, inputSchema:
|
|
2014
|
+
server.registerTool(name, { description: config.description, inputSchema: z7.object(config.inputSchema) }, async (rawArgs) => {
|
|
1852
2015
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
1853
2016
|
try {
|
|
1854
2017
|
const { state, timedOut } = await runWaitOperation({
|
|
@@ -1867,19 +2030,19 @@ function mountWait(server, config) {
|
|
|
1867
2030
|
}
|
|
1868
2031
|
// src/tools/oauth-provider.ts
|
|
1869
2032
|
import { randomUUID } from "node:crypto";
|
|
1870
|
-
import { z as
|
|
2033
|
+
import { z as z8 } from "zod";
|
|
1871
2034
|
function isPlainDisplayCharacter(character) {
|
|
1872
2035
|
const codePoint = character.codePointAt(0);
|
|
1873
2036
|
return codePoint !== undefined && codePoint >= 32 && codePoint !== 127 && character !== "<" && character !== ">";
|
|
1874
2037
|
}
|
|
1875
|
-
var CimdClientMetadataSchema =
|
|
1876
|
-
client_id:
|
|
1877
|
-
redirect_uris:
|
|
1878
|
-
client_name:
|
|
2038
|
+
var CimdClientMetadataSchema = z8.object({
|
|
2039
|
+
client_id: z8.url(),
|
|
2040
|
+
redirect_uris: z8.array(z8.url()).min(1),
|
|
2041
|
+
client_name: z8.string().trim().min(1).max(200).refine((value) => Array.from(value).every(isPlainDisplayCharacter), {
|
|
1879
2042
|
message: "client_name must be plain display text"
|
|
1880
2043
|
}),
|
|
1881
|
-
token_endpoint_auth_method:
|
|
1882
|
-
application_type:
|
|
2044
|
+
token_endpoint_auth_method: z8.literal("none"),
|
|
2045
|
+
application_type: z8.enum(["native", "web"]).optional()
|
|
1883
2046
|
});
|
|
1884
2047
|
function createSecureClientMetadataFetcher(policy = {}) {
|
|
1885
2048
|
assertPositiveInteger("cimd.maxBytes", policy.maxBytes ?? DEFAULT_CIMD_MAX_BYTES);
|
|
@@ -2512,67 +2675,6 @@ function mountOAuthProvider(config) {
|
|
|
2512
2675
|
};
|
|
2513
2676
|
return dcrEnabled ? [metadataRoute, registerRoute, authorizeRoute, tokenRoute] : [metadataRoute, authorizeRoute, tokenRoute];
|
|
2514
2677
|
}
|
|
2515
|
-
// src/tools/remote.ts
|
|
2516
|
-
var HTTP_ONLY = Object.freeze(["HTTP"]);
|
|
2517
|
-
function toArgs(ctx) {
|
|
2518
|
-
const { params, input } = ctx;
|
|
2519
|
-
return {
|
|
2520
|
-
...isRecord(params) ? params : {},
|
|
2521
|
-
...isRecord(input) ? input : {}
|
|
2522
|
-
};
|
|
2523
|
-
}
|
|
2524
|
-
function implementRemote(contract, http, options) {
|
|
2525
|
-
const client = createClient(contract, http);
|
|
2526
|
-
const groupScope = contract.meta.scope ?? "public";
|
|
2527
|
-
const methods = {};
|
|
2528
|
-
for (const [key, endpoint] of Object.entries(contract.endpoints)) {
|
|
2529
|
-
methods[key] = {
|
|
2530
|
-
method: endpoint.method,
|
|
2531
|
-
path: endpoint.path,
|
|
2532
|
-
desc: endpoint.desc,
|
|
2533
|
-
serviceName: contract.meta.prefix,
|
|
2534
|
-
key,
|
|
2535
|
-
toolName: "toolName" in endpoint ? endpoint.toolName : undefined,
|
|
2536
|
-
expose: endpoint.rawResponse || endpoint.rawBody || endpoint.responseMeta ? HTTP_ONLY : endpoint.expose,
|
|
2537
|
-
ui: "ui" in endpoint ? endpoint.ui : undefined,
|
|
2538
|
-
annotations: "annotations" in endpoint ? endpoint.annotations : undefined,
|
|
2539
|
-
meta: mergeMeta(contract.meta.meta, endpoint.meta),
|
|
2540
|
-
scope: endpoint.scope ?? groupScope,
|
|
2541
|
-
paramsSchema: endpoint.params,
|
|
2542
|
-
inputSchema: endpoint.input,
|
|
2543
|
-
outputSchema: endpoint.output,
|
|
2544
|
-
multipart: endpoint.multipart,
|
|
2545
|
-
maxJsonBodyBytes: endpoint.maxJsonBodyBytes,
|
|
2546
|
-
idempotent: endpoint.idempotent,
|
|
2547
|
-
rawResponse: endpoint.rawResponse,
|
|
2548
|
-
rawBody: endpoint.rawBody,
|
|
2549
|
-
responseMeta: endpoint.responseMeta,
|
|
2550
|
-
contentType: "contentType" in endpoint ? endpoint.contentType : undefined,
|
|
2551
|
-
handler: async (ctx) => {
|
|
2552
|
-
const call = client[key];
|
|
2553
|
-
if (!call) {
|
|
2554
|
-
throw new Error(`implementRemote: endpoint "${key}" is not exposed over HTTP`);
|
|
2555
|
-
}
|
|
2556
|
-
const args = toArgs(ctx);
|
|
2557
|
-
try {
|
|
2558
|
-
const finalArgs = options?.transformArgs ? await options.transformArgs(key, args) : args;
|
|
2559
|
-
return await call(finalArgs);
|
|
2560
|
-
} catch (err) {
|
|
2561
|
-
if (ApiError.is(err)) {
|
|
2562
|
-
throw new AppError(err.code, err.message, err.status, isRecord(err.details) ? err.details : undefined, err.hint, err.traceId);
|
|
2563
|
-
}
|
|
2564
|
-
throw err;
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
};
|
|
2568
|
-
}
|
|
2569
|
-
return {
|
|
2570
|
-
name: contract.meta.prefix,
|
|
2571
|
-
prefix: contract.meta.prefix,
|
|
2572
|
-
scope: groupScope,
|
|
2573
|
-
methods
|
|
2574
|
-
};
|
|
2575
|
-
}
|
|
2576
2678
|
// src/tools/tool-logger.ts
|
|
2577
2679
|
function createToolLogger(config = {}) {
|
|
2578
2680
|
const log = config.log ?? ((line) => console.error(line));
|
|
@@ -2673,56 +2775,60 @@ function summarizeTransports(surface) {
|
|
|
2673
2775
|
};
|
|
2674
2776
|
}
|
|
2675
2777
|
export {
|
|
2676
|
-
|
|
2677
|
-
validateMcpSchemas,
|
|
2678
|
-
summarizeTransports,
|
|
2679
|
-
resolveMedia,
|
|
2680
|
-
protectedResourceMetadataUrl,
|
|
2681
|
-
oauthProtectedResourceRoute,
|
|
2682
|
-
mountWait,
|
|
2683
|
-
mountViewFile,
|
|
2684
|
-
mountUpload,
|
|
2685
|
-
mountOAuthProvider,
|
|
2686
|
-
mountMcpResource,
|
|
2687
|
-
mountMcp,
|
|
2688
|
-
mountDownload,
|
|
2689
|
-
mountAgent,
|
|
2690
|
-
listToolNames,
|
|
2691
|
-
listContractToolNames,
|
|
2692
|
-
inlineMcpAppBundle,
|
|
2693
|
-
implementRemote,
|
|
2694
|
-
flattenToolJsonSchema,
|
|
2695
|
-
findUntypedProperties,
|
|
2696
|
-
findNonPortableFormats,
|
|
2697
|
-
defineWaitTool,
|
|
2698
|
-
defineViewFileTool,
|
|
2699
|
-
defineUploadTool,
|
|
2700
|
-
defineRuntimeTool,
|
|
2701
|
-
defineDownloadTool,
|
|
2702
|
-
defineCliCommand,
|
|
2703
|
-
createToolkit,
|
|
2704
|
-
createToolLogger,
|
|
2705
|
-
createToolInvoker,
|
|
2706
|
-
createStdioMcpServer,
|
|
2707
|
-
createSecureClientMetadataFetcher,
|
|
2708
|
-
createRuntimeToolFactory,
|
|
2709
|
-
createMcpHttpRoute,
|
|
2710
|
-
createMcpHandler,
|
|
2711
|
-
createCli,
|
|
2712
|
-
collectTools,
|
|
2713
|
-
coerceJsonArgs,
|
|
2714
|
-
buildToolManifest,
|
|
2715
|
-
buildMcpServer,
|
|
2716
|
-
bindStdioProcessSignals,
|
|
2717
|
-
ViewFileOutputSchema,
|
|
2718
|
-
ViewFileInputSchema,
|
|
2719
|
-
ViewFileErrorSchema,
|
|
2720
|
-
UploadToolInputSchema,
|
|
2721
|
-
RESOURCE_MIME_TYPE,
|
|
2722
|
-
PROTECTED_RESOURCE_PATH,
|
|
2723
|
-
PORTABLE_JSON_SCHEMA_FORMATS,
|
|
2724
|
-
McpMediaContentSchema,
|
|
2725
|
-
McpAnnotationsSchema,
|
|
2778
|
+
AsyncOperationCancelResultSchema,
|
|
2726
2779
|
EXT_APPS_BUNDLE_PLACEHOLDER,
|
|
2727
|
-
|
|
2780
|
+
McpAnnotationsSchema,
|
|
2781
|
+
McpMediaContentSchema,
|
|
2782
|
+
PORTABLE_JSON_SCHEMA_FORMATS,
|
|
2783
|
+
PROTECTED_RESOURCE_PATH,
|
|
2784
|
+
RESOURCE_MIME_TYPE,
|
|
2785
|
+
UploadToolInputSchema,
|
|
2786
|
+
ViewFileErrorSchema,
|
|
2787
|
+
ViewFileInputSchema,
|
|
2788
|
+
ViewFileOutputSchema,
|
|
2789
|
+
bindContractAsyncOperation,
|
|
2790
|
+
bindStdioProcessSignals,
|
|
2791
|
+
buildMcpServer,
|
|
2792
|
+
buildToolManifest,
|
|
2793
|
+
coerceJsonArgs,
|
|
2794
|
+
collectTools,
|
|
2795
|
+
composeToolLifecycle,
|
|
2796
|
+
createAsyncOperationSnapshotSchema,
|
|
2797
|
+
createCli,
|
|
2798
|
+
createMcpHandler,
|
|
2799
|
+
createMcpHttpRoute,
|
|
2800
|
+
createRuntimeToolFactory,
|
|
2801
|
+
createSecureClientMetadataFetcher,
|
|
2802
|
+
createStdioMcpServer,
|
|
2803
|
+
createToolInvoker,
|
|
2804
|
+
createToolLogger,
|
|
2805
|
+
createToolkit,
|
|
2806
|
+
defineAsyncOperation,
|
|
2807
|
+
defineAsyncOperationContract,
|
|
2808
|
+
defineCliCommand,
|
|
2809
|
+
defineDownloadTool,
|
|
2810
|
+
defineRuntimeTool,
|
|
2811
|
+
defineUploadTool,
|
|
2812
|
+
defineViewFileTool,
|
|
2813
|
+
defineWaitTool,
|
|
2814
|
+
findNonPortableFormats,
|
|
2815
|
+
findUntypedProperties,
|
|
2816
|
+
flattenToolJsonSchema,
|
|
2817
|
+
inlineMcpAppBundle,
|
|
2818
|
+
listContractToolNames,
|
|
2819
|
+
listToolNames,
|
|
2820
|
+
mountAgent,
|
|
2821
|
+
mountDownload,
|
|
2822
|
+
mountMcp,
|
|
2823
|
+
mountMcpResource,
|
|
2824
|
+
mountOAuthProvider,
|
|
2825
|
+
mountUpload,
|
|
2826
|
+
mountViewFile,
|
|
2827
|
+
mountWait,
|
|
2828
|
+
oauthProtectedResourceRoute,
|
|
2829
|
+
protectedResourceMetadataUrl,
|
|
2830
|
+
resolveMedia,
|
|
2831
|
+
summarizeTransports,
|
|
2832
|
+
validateMcpSchemas,
|
|
2833
|
+
wwwAuthenticateHeader
|
|
2728
2834
|
};
|