stitchkit 0.53.1 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/contract/define.d.ts +27 -0
- package/dist/contract/define.d.ts.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/{index-6j4j5wf9.js → index-7vb44hy8.js} +404 -72
- package/dist/{index-yydqk4fh.js → index-b15rtha1.js} +30 -1
- package/dist/{index-pdv1mjjr.js → index-z7761jk2.js} +15 -38
- package/dist/node.js +2 -2
- package/dist/observability/event.d.ts +2 -16
- package/dist/observability/event.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/process-signal-common.d.ts +6 -0
- package/dist/server/process-signal-common.d.ts.map +1 -0
- package/dist/server/process-signals.d.ts.map +1 -1
- package/dist/tools/cli-command.d.ts +29 -0
- package/dist/tools/cli-command.d.ts.map +1 -0
- package/dist/tools/cli.d.ts +11 -6
- package/dist/tools/cli.d.ts.map +1 -1
- package/dist/tools/define-download-tool.d.ts +17 -0
- package/dist/tools/define-download-tool.d.ts.map +1 -0
- package/dist/tools/define-upload-tool.d.ts +14 -0
- package/dist/tools/define-upload-tool.d.ts.map +1 -0
- package/dist/tools/define-view-file-tool.d.ts +10 -0
- package/dist/tools/define-view-file-tool.d.ts.map +1 -0
- package/dist/tools/define-wait-tool.d.ts +22 -0
- package/dist/tools/define-wait-tool.d.ts.map +1 -0
- package/dist/tools/download-core.d.ts +25 -0
- package/dist/tools/download-core.d.ts.map +1 -0
- package/dist/tools/execute.d.ts +3 -1
- package/dist/tools/execute.d.ts.map +1 -1
- package/dist/tools/mcp-round.d.ts +0 -1
- package/dist/tools/mcp-round.d.ts.map +1 -1
- package/dist/tools/mcp-stdio-signals.d.ts +33 -0
- package/dist/tools/mcp-stdio-signals.d.ts.map +1 -0
- package/dist/tools/mount-download.d.ts +5 -0
- package/dist/tools/mount-download.d.ts.map +1 -1
- package/dist/tools/mount-upload.d.ts.map +1 -1
- package/dist/tools/mount-wait.d.ts.map +1 -1
- package/dist/tools/native-definition.d.ts +15 -0
- package/dist/tools/native-definition.d.ts.map +1 -0
- package/dist/tools/runtime-tool.d.ts +5 -4
- package/dist/tools/runtime-tool.d.ts.map +1 -1
- package/dist/tools/surface.d.ts.map +1 -1
- package/dist/tools/upload-core.d.ts +3 -0
- package/dist/tools/upload-core.d.ts.map +1 -0
- package/dist/tools/view-file.d.ts +80 -20
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools/wait-core.d.ts +10 -1
- package/dist/tools/wait-core.d.ts.map +1 -1
- package/dist/tools.d.ts +10 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +570 -379
- package/llms-full.txt +312 -35
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -4,29 +4,37 @@ import {
|
|
|
4
4
|
} from "./index-yez419px.js";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_CORS_ALLOW_HEADERS,
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
DEFAULT_PROCESS_SIGNALS,
|
|
8
|
+
contractOnlyService,
|
|
9
|
+
defaultSignalSource,
|
|
10
|
+
guardSignalCallback,
|
|
11
|
+
reportSignalError
|
|
12
|
+
} from "./index-b15rtha1.js";
|
|
9
13
|
import {
|
|
10
14
|
assertToolName,
|
|
11
15
|
assertUniqueToolName,
|
|
12
16
|
buildToolPresentationSchema,
|
|
13
17
|
coerceJsonArgs,
|
|
18
|
+
collectToolSurface,
|
|
14
19
|
collectTools,
|
|
15
20
|
createCli,
|
|
21
|
+
createRuntimeToolFactory,
|
|
16
22
|
createToolRunner,
|
|
23
|
+
defineCliCommand,
|
|
24
|
+
defineRuntimeTool,
|
|
17
25
|
executeToolMethod,
|
|
18
26
|
fetchGuarded,
|
|
19
27
|
fetchPinnedDocument,
|
|
20
28
|
flattenToolJsonSchema,
|
|
21
29
|
formatToolError,
|
|
22
30
|
isObjectPresentationSchema,
|
|
23
|
-
pollUntil,
|
|
24
31
|
presentationMetadata,
|
|
25
32
|
readCapped,
|
|
33
|
+
runWaitOperation,
|
|
26
34
|
toolErrorFromResult,
|
|
27
35
|
toolResultFromError,
|
|
28
36
|
writeDownload
|
|
29
|
-
} from "./index-
|
|
37
|
+
} from "./index-7vb44hy8.js";
|
|
30
38
|
import {
|
|
31
39
|
isWithinDir,
|
|
32
40
|
realPathWithinDir,
|
|
@@ -54,148 +62,6 @@ import {
|
|
|
54
62
|
|
|
55
63
|
// src/tools/agent.ts
|
|
56
64
|
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
65
|
function mountAgent(services, config = {}) {
|
|
200
66
|
const serviceList = Array.isArray(services) ? services : [services];
|
|
201
67
|
const tools = {};
|
|
@@ -262,6 +128,426 @@ function mountAgent(services, config = {}) {
|
|
|
262
128
|
}
|
|
263
129
|
return tools;
|
|
264
130
|
}
|
|
131
|
+
// src/tools/download-core.ts
|
|
132
|
+
import { basename, extname, resolve } from "node:path";
|
|
133
|
+
import { z } from "zod";
|
|
134
|
+
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
135
|
+
var MIME_EXT = {
|
|
136
|
+
"image/png": ".png",
|
|
137
|
+
"image/jpeg": ".jpg",
|
|
138
|
+
"image/webp": ".webp",
|
|
139
|
+
"image/gif": ".gif",
|
|
140
|
+
"video/mp4": ".mp4",
|
|
141
|
+
"video/webm": ".webm",
|
|
142
|
+
"audio/mpeg": ".mp3",
|
|
143
|
+
"audio/wav": ".wav",
|
|
144
|
+
"audio/ogg": ".ogg"
|
|
145
|
+
};
|
|
146
|
+
var DownloadResultSchema = z.object({
|
|
147
|
+
path: z.string(),
|
|
148
|
+
size: z.number().int().nonnegative(),
|
|
149
|
+
mimeType: z.string()
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
class DownloadOperationError extends Error {
|
|
153
|
+
code;
|
|
154
|
+
status;
|
|
155
|
+
constructor(code, message, status) {
|
|
156
|
+
super(message);
|
|
157
|
+
this.code = code;
|
|
158
|
+
this.status = status;
|
|
159
|
+
this.name = "DownloadOperationError";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function extensionFor(url, mime) {
|
|
163
|
+
if (MIME_EXT[mime])
|
|
164
|
+
return MIME_EXT[mime];
|
|
165
|
+
try {
|
|
166
|
+
const ext = extname(new URL(url).pathname).toLowerCase();
|
|
167
|
+
if (ext)
|
|
168
|
+
return ext;
|
|
169
|
+
} catch {}
|
|
170
|
+
return ".bin";
|
|
171
|
+
}
|
|
172
|
+
function baseNameFor(url) {
|
|
173
|
+
try {
|
|
174
|
+
const path = new URL(url).pathname;
|
|
175
|
+
return (basename(path, extname(path)) || "download").slice(0, 60);
|
|
176
|
+
} catch {
|
|
177
|
+
return "download";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function runDownloadOperation(config) {
|
|
181
|
+
const res = await fetchGuarded(new URL(config.url), config.allowPrivateHosts ?? false, {
|
|
182
|
+
timeoutMs: config.timeoutMs,
|
|
183
|
+
signal: config.signal
|
|
184
|
+
});
|
|
185
|
+
if (!res.ok) {
|
|
186
|
+
throw new DownloadOperationError("DOWNLOAD_HTTP_ERROR", `HTTP ${res.status}`, 502);
|
|
187
|
+
}
|
|
188
|
+
const mimeType = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() || "application/octet-stream";
|
|
189
|
+
const max = config.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
190
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
191
|
+
if (declared > max) {
|
|
192
|
+
await res.body?.cancel();
|
|
193
|
+
throw new DownloadOperationError("DOWNLOAD_TOO_LARGE", `file exceeds the ${max}-byte cap`, 413);
|
|
194
|
+
}
|
|
195
|
+
const buffer = await readCapped(res, max);
|
|
196
|
+
if (!buffer) {
|
|
197
|
+
throw new DownloadOperationError("DOWNLOAD_TOO_LARGE", `file exceeds the ${max}-byte cap`, 413);
|
|
198
|
+
}
|
|
199
|
+
const dir = resolve(config.dir);
|
|
200
|
+
const filePath = resolve(dir, `${baseNameFor(config.url)}${extensionFor(config.url, mimeType)}`);
|
|
201
|
+
await writeDownload(dir, filePath, buffer);
|
|
202
|
+
return { path: filePath, size: buffer.length, mimeType };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/tools/native-definition.ts
|
|
206
|
+
function managedNativeIdentity(identity, method) {
|
|
207
|
+
return { ...identity, method };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/tools/define-download-tool.ts
|
|
211
|
+
function defineDownloadTool(config) {
|
|
212
|
+
return defineRuntimeTool({
|
|
213
|
+
name: config.name ?? "download",
|
|
214
|
+
description: config.description,
|
|
215
|
+
identity: managedNativeIdentity(config.identity, "POST"),
|
|
216
|
+
input: config.input,
|
|
217
|
+
output: DownloadResultSchema,
|
|
218
|
+
transports: config.transports,
|
|
219
|
+
annotations: config.annotations,
|
|
220
|
+
present: config.present,
|
|
221
|
+
handler: async (context) => {
|
|
222
|
+
const url = await config.resolveUrl(context.input, context);
|
|
223
|
+
if (!url) {
|
|
224
|
+
throw new AppError("DOWNLOAD_NOT_FOUND", "Nothing to download", 404);
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
return await runDownloadOperation({
|
|
228
|
+
url,
|
|
229
|
+
dir: config.dirFromInput?.(context.input) ?? config.defaultDir,
|
|
230
|
+
allowPrivateHosts: config.allowPrivateHosts,
|
|
231
|
+
maxBytes: config.maxBytes,
|
|
232
|
+
timeoutMs: config.timeoutMs,
|
|
233
|
+
signal: context.signal
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error instanceof DownloadOperationError) {
|
|
237
|
+
throw new AppError(error.code, `Download failed: ${error.message}`, error.status);
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// src/tools/define-upload-tool.ts
|
|
245
|
+
import { z as z2 } from "zod";
|
|
246
|
+
|
|
247
|
+
// src/tools/upload-core.ts
|
|
248
|
+
function runUploadOperation(path, upload, signal) {
|
|
249
|
+
signal?.throwIfAborted();
|
|
250
|
+
return upload(path, signal);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/tools/define-upload-tool.ts
|
|
254
|
+
var UploadToolInputSchema = z2.object({
|
|
255
|
+
path: z2.string().min(1).describe("Path to a local file on this machine")
|
|
256
|
+
});
|
|
257
|
+
function defineUploadTool(config) {
|
|
258
|
+
return defineRuntimeTool({
|
|
259
|
+
name: config.name ?? "upload",
|
|
260
|
+
description: config.description,
|
|
261
|
+
identity: managedNativeIdentity(config.identity, "POST"),
|
|
262
|
+
input: UploadToolInputSchema,
|
|
263
|
+
output: config.output,
|
|
264
|
+
transports: config.transports,
|
|
265
|
+
annotations: config.annotations,
|
|
266
|
+
present: config.present,
|
|
267
|
+
handler: async (context) => runUploadOperation(context.input.path, (path) => config.upload(path, context), context.signal)
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// src/tools/view-file.ts
|
|
271
|
+
import { readFile, stat } from "node:fs/promises";
|
|
272
|
+
import { extname as extname2, resolve as resolve2 } from "node:path";
|
|
273
|
+
import { z as z3 } from "zod";
|
|
274
|
+
var MAX_INLINE_BYTES = 20 * 1024 * 1024;
|
|
275
|
+
var MAX_VIEW_FILES = 20;
|
|
276
|
+
var EXT_MIME = {
|
|
277
|
+
".png": "image/png",
|
|
278
|
+
".jpg": "image/jpeg",
|
|
279
|
+
".jpeg": "image/jpeg",
|
|
280
|
+
".gif": "image/gif",
|
|
281
|
+
".webp": "image/webp",
|
|
282
|
+
".mp3": "audio/mpeg",
|
|
283
|
+
".wav": "audio/wav",
|
|
284
|
+
".ogg": "audio/ogg",
|
|
285
|
+
".m4a": "audio/mp4",
|
|
286
|
+
".mp4": "video/mp4",
|
|
287
|
+
".webm": "video/webm",
|
|
288
|
+
".mov": "video/quicktime"
|
|
289
|
+
};
|
|
290
|
+
var McpAnnotationsSchema = z3.object({
|
|
291
|
+
audience: z3.array(z3.enum(["user", "assistant"])).optional(),
|
|
292
|
+
priority: z3.number().optional()
|
|
293
|
+
});
|
|
294
|
+
var McpMediaContentSchema = z3.discriminatedUnion("type", [
|
|
295
|
+
z3.object({ type: z3.literal("text"), text: z3.string() }),
|
|
296
|
+
z3.object({
|
|
297
|
+
type: z3.literal("image"),
|
|
298
|
+
data: z3.string(),
|
|
299
|
+
mimeType: z3.string(),
|
|
300
|
+
annotations: McpAnnotationsSchema.optional()
|
|
301
|
+
}),
|
|
302
|
+
z3.object({
|
|
303
|
+
type: z3.literal("audio"),
|
|
304
|
+
data: z3.string(),
|
|
305
|
+
mimeType: z3.string(),
|
|
306
|
+
annotations: McpAnnotationsSchema.optional()
|
|
307
|
+
})
|
|
308
|
+
]);
|
|
309
|
+
var ViewFileErrorSchema = z3.object({
|
|
310
|
+
path: z3.string(),
|
|
311
|
+
message: z3.string()
|
|
312
|
+
});
|
|
313
|
+
var ViewFileOutputSchema = z3.object({
|
|
314
|
+
content: z3.array(McpMediaContentSchema),
|
|
315
|
+
errors: z3.array(ViewFileErrorSchema)
|
|
316
|
+
});
|
|
317
|
+
async function fetchSource(pathOrUrl, options, maxBytes) {
|
|
318
|
+
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
319
|
+
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
320
|
+
const url = new URL(pathOrUrl);
|
|
321
|
+
const res = await fetchGuarded(url, options.allowPrivateHosts ?? false, {
|
|
322
|
+
timeoutMs: options.timeoutMs,
|
|
323
|
+
signal: options.signal
|
|
324
|
+
});
|
|
325
|
+
if (!res.ok)
|
|
326
|
+
throw new Error(`HTTP ${res.status}`);
|
|
327
|
+
const headerMime = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
328
|
+
const mimeType = headerMime || extMime || "application/octet-stream";
|
|
329
|
+
if (mimeType.startsWith("video/")) {
|
|
330
|
+
await res.body?.cancel();
|
|
331
|
+
return { video: true, mimeType, bytesRead: 0 };
|
|
332
|
+
}
|
|
333
|
+
const declared = Number(res.headers.get("content-length") ?? 0);
|
|
334
|
+
if (declared > maxBytes) {
|
|
335
|
+
await res.body?.cancel();
|
|
336
|
+
return { tooLarge: true, mimeType, bytesRead: 0 };
|
|
337
|
+
}
|
|
338
|
+
const buffer2 = await readCapped(res, maxBytes);
|
|
339
|
+
return buffer2 ? { buffer: buffer2, mimeType, bytesRead: buffer2.length } : { tooLarge: true, mimeType, bytesRead: maxBytes };
|
|
340
|
+
}
|
|
341
|
+
if (!options.baseDir) {
|
|
342
|
+
throw new Error("local file paths are disabled — set baseDir to allow them");
|
|
343
|
+
}
|
|
344
|
+
options.signal?.throwIfAborted();
|
|
345
|
+
if (!extMime) {
|
|
346
|
+
throw new Error("refusing to read a non-media file");
|
|
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");
|
|
352
|
+
}
|
|
353
|
+
const realTarget = await realPathWithinDir(root, target);
|
|
354
|
+
if (realTarget === null) {
|
|
355
|
+
throw new Error("path escapes the allowed directory");
|
|
356
|
+
}
|
|
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
|
+
}
|
|
365
|
+
async function resolveMediaWithinBudget(pathOrUrl, options, maxBytes) {
|
|
366
|
+
if (maxBytes <= 0) {
|
|
367
|
+
return {
|
|
368
|
+
content: [
|
|
369
|
+
{ type: "text", text: `[media] total inline budget exhausted — ${pathOrUrl}` }
|
|
370
|
+
],
|
|
371
|
+
bytes: 0
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
const extMime = EXT_MIME[extname2(pathOrUrl).toLowerCase()];
|
|
375
|
+
if (extMime?.startsWith("video/")) {
|
|
376
|
+
return {
|
|
377
|
+
content: [{ type: "text", text: `[video] ${extMime} — ${pathOrUrl}` }],
|
|
378
|
+
bytes: 0
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const result = await fetchSource(pathOrUrl, options, maxBytes);
|
|
382
|
+
if ("video" in result) {
|
|
383
|
+
return {
|
|
384
|
+
content: [{ type: "text", text: `[video] ${result.mimeType} — ${pathOrUrl}` }],
|
|
385
|
+
bytes: result.bytesRead
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
if ("tooLarge" in result) {
|
|
389
|
+
return {
|
|
390
|
+
content: [
|
|
391
|
+
{ type: "text", text: `[${result.mimeType}] too large to inline — ${pathOrUrl}` }
|
|
392
|
+
],
|
|
393
|
+
bytes: result.bytesRead
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const { buffer, mimeType } = result;
|
|
397
|
+
const sizeKb = (buffer.length / 1024).toFixed(0);
|
|
398
|
+
if (mimeType.startsWith("image/")) {
|
|
399
|
+
return {
|
|
400
|
+
content: [
|
|
401
|
+
{
|
|
402
|
+
type: "image",
|
|
403
|
+
data: buffer.toString("base64"),
|
|
404
|
+
mimeType,
|
|
405
|
+
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
406
|
+
},
|
|
407
|
+
{ type: "text", text: `[image] ${mimeType}, ${sizeKb}KB` }
|
|
408
|
+
],
|
|
409
|
+
bytes: buffer.length
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (mimeType.startsWith("audio/")) {
|
|
413
|
+
return {
|
|
414
|
+
content: [
|
|
415
|
+
{
|
|
416
|
+
type: "audio",
|
|
417
|
+
data: buffer.toString("base64"),
|
|
418
|
+
mimeType,
|
|
419
|
+
annotations: { audience: ["user", "assistant"], priority: 0.9 }
|
|
420
|
+
},
|
|
421
|
+
{ type: "text", text: `[audio] ${mimeType}, ${sizeKb}KB` }
|
|
422
|
+
],
|
|
423
|
+
bytes: buffer.length
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
content: [{ type: "text", text: `[${mimeType}] ${sizeKb}KB — ${pathOrUrl}` }],
|
|
428
|
+
bytes: buffer.length
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
async function resolveMedia(pathOrUrl, options = {}) {
|
|
432
|
+
return (await resolveMediaWithinBudget(pathOrUrl, options, MAX_INLINE_BYTES)).content;
|
|
433
|
+
}
|
|
434
|
+
var ViewFileInputSchema = z3.object({
|
|
435
|
+
paths: z3.union([z3.string(), z3.array(z3.string()).max(MAX_VIEW_FILES)]).describe("Media URL(s) or file path(s) to view")
|
|
436
|
+
});
|
|
437
|
+
async function runViewFileOperation(paths, options = {}, signal) {
|
|
438
|
+
const list = Array.isArray(paths) ? paths : [paths];
|
|
439
|
+
const content = [];
|
|
440
|
+
const errors = [];
|
|
441
|
+
let remainingBytes = MAX_INLINE_BYTES;
|
|
442
|
+
for (const pathOrUrl of list) {
|
|
443
|
+
signal?.throwIfAborted();
|
|
444
|
+
try {
|
|
445
|
+
const resolved = await resolveMediaWithinBudget(pathOrUrl, { ...options, signal }, remainingBytes);
|
|
446
|
+
remainingBytes -= resolved.bytes;
|
|
447
|
+
content.push(...resolved.content);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (signal?.aborted)
|
|
450
|
+
throw signal.reason ?? error;
|
|
451
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
452
|
+
errors.push({ path: pathOrUrl, message });
|
|
453
|
+
content.push({ type: "text", text: `[${pathOrUrl}] Error: ${message}` });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return ViewFileOutputSchema.parse({ content, errors });
|
|
457
|
+
}
|
|
458
|
+
function mountViewFile(server, options = {}) {
|
|
459
|
+
server.registerTool("view_file", {
|
|
460
|
+
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.",
|
|
461
|
+
inputSchema: ViewFileInputSchema.shape,
|
|
462
|
+
annotations: { title: "View Media", readOnlyHint: true, idempotentHint: true }
|
|
463
|
+
}, async (args) => {
|
|
464
|
+
const result = await runViewFileOperation(args.paths, options);
|
|
465
|
+
return { content: result.content };
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/tools/define-view-file-tool.ts
|
|
470
|
+
function defineViewFileTool(config) {
|
|
471
|
+
const defaults = {
|
|
472
|
+
mcp: (output) => ({ content: output.content }),
|
|
473
|
+
agent: (output) => ({
|
|
474
|
+
type: "content",
|
|
475
|
+
value: output.content.map((part) => {
|
|
476
|
+
if (part.type === "text")
|
|
477
|
+
return part;
|
|
478
|
+
return {
|
|
479
|
+
type: "file",
|
|
480
|
+
data: { type: "data", data: part.data },
|
|
481
|
+
mediaType: part.mimeType
|
|
482
|
+
};
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
};
|
|
486
|
+
return defineRuntimeTool({
|
|
487
|
+
name: config.name ?? "view_file",
|
|
488
|
+
description: config.description,
|
|
489
|
+
identity: managedNativeIdentity(config.identity, "GET"),
|
|
490
|
+
input: ViewFileInputSchema,
|
|
491
|
+
output: ViewFileOutputSchema,
|
|
492
|
+
transports: config.transports,
|
|
493
|
+
annotations: config.annotations ?? {
|
|
494
|
+
title: "View Media",
|
|
495
|
+
readOnlyHint: true,
|
|
496
|
+
idempotentHint: true
|
|
497
|
+
},
|
|
498
|
+
present: { ...defaults, ...config.present },
|
|
499
|
+
handler: (context) => runViewFileOperation(context.input.paths, {
|
|
500
|
+
baseDir: config.baseDir,
|
|
501
|
+
allowPrivateHosts: config.allowPrivateHosts,
|
|
502
|
+
timeoutMs: config.timeoutMs
|
|
503
|
+
}, context.signal)
|
|
504
|
+
});
|
|
505
|
+
}
|
|
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
|
+
}
|
|
265
551
|
// src/tools/invoker.ts
|
|
266
552
|
function createToolInvoker(services, config) {
|
|
267
553
|
const serviceList = Array.isArray(services) ? services : [services];
|
|
@@ -358,7 +644,7 @@ import {
|
|
|
358
644
|
createRequestStateCodec,
|
|
359
645
|
McpServer
|
|
360
646
|
} from "@modelcontextprotocol/server";
|
|
361
|
-
import { z as
|
|
647
|
+
import { z as z5 } from "zod";
|
|
362
648
|
|
|
363
649
|
// src/tools/mcp-app.ts
|
|
364
650
|
import { readFileSync } from "node:fs";
|
|
@@ -975,13 +1261,13 @@ function runInMcpRequestContext(context, toolName, body) {
|
|
|
975
1261
|
import {
|
|
976
1262
|
isCallToolResult
|
|
977
1263
|
} from "@modelcontextprotocol/server";
|
|
978
|
-
import { z } from "zod";
|
|
1264
|
+
import { z as z4 } from "zod";
|
|
979
1265
|
function mountPreparedRuntimeMcp(server, tools, config) {
|
|
980
1266
|
for (const { definition, descriptor } of tools) {
|
|
981
1267
|
const { mountable } = descriptor;
|
|
982
1268
|
const toolConfig = {
|
|
983
1269
|
description: definition.description,
|
|
984
|
-
inputSchema:
|
|
1270
|
+
inputSchema: z4.looseObject({}).meta(presentationMetadata(descriptor.inputSchema))
|
|
985
1271
|
};
|
|
986
1272
|
if (descriptor.outputSchema)
|
|
987
1273
|
toolConfig.outputSchema = descriptor.outputSchema;
|
|
@@ -1070,7 +1356,7 @@ function mountPreparedMcp(mcpServer, prepared, config = {}) {
|
|
|
1070
1356
|
const { mountable } = descriptor;
|
|
1071
1357
|
const toolConfig = {
|
|
1072
1358
|
description: mountable.method.desc,
|
|
1073
|
-
inputSchema:
|
|
1359
|
+
inputSchema: z5.looseObject({}).meta(presentationMetadata(descriptor.inputSchema))
|
|
1074
1360
|
};
|
|
1075
1361
|
if (descriptor.outputSchema)
|
|
1076
1362
|
toolConfig.outputSchema = descriptor.outputSchema;
|
|
@@ -1324,8 +1610,8 @@ function createMcpHandler(config) {
|
|
|
1324
1610
|
const authByCarrier = new WeakMap;
|
|
1325
1611
|
let closed = false;
|
|
1326
1612
|
let resolveClosed;
|
|
1327
|
-
const closedSignal = new Promise((
|
|
1328
|
-
resolveClosed =
|
|
1613
|
+
const closedSignal = new Promise((resolve3) => {
|
|
1614
|
+
resolveClosed = resolve3;
|
|
1329
1615
|
});
|
|
1330
1616
|
const sdkHandler = createSdkMcpHandler(({ authInfo }) => {
|
|
1331
1617
|
const resolved = authInfo ? authByCarrier.get(authInfo) : undefined;
|
|
@@ -1409,9 +1695,103 @@ async function createStdioMcpServer(config) {
|
|
|
1409
1695
|
maxSubscriptions: 0
|
|
1410
1696
|
});
|
|
1411
1697
|
}
|
|
1698
|
+
// src/tools/mcp-stdio-signals.ts
|
|
1699
|
+
var bound = new WeakSet;
|
|
1700
|
+
function bindStdioProcessSignals(handle, options = {}) {
|
|
1701
|
+
if (bound.has(handle)) {
|
|
1702
|
+
throw new Error("[stitchkit] bindStdioProcessSignals: this stdio handle is already bound; close the first binding before creating another");
|
|
1703
|
+
}
|
|
1704
|
+
bound.add(handle);
|
|
1705
|
+
const signals = [...new Set(options.signals ?? DEFAULT_PROCESS_SIGNALS)];
|
|
1706
|
+
const source = options.signalSource ?? defaultSignalSource;
|
|
1707
|
+
const handlers = [];
|
|
1708
|
+
let started = false;
|
|
1709
|
+
let sameTurnAsStart = false;
|
|
1710
|
+
let closed = false;
|
|
1711
|
+
let resolveChain = () => {
|
|
1712
|
+
return;
|
|
1713
|
+
};
|
|
1714
|
+
let rejectChain = () => {
|
|
1715
|
+
return;
|
|
1716
|
+
};
|
|
1717
|
+
const promise = new Promise((resolve3, reject) => {
|
|
1718
|
+
resolveChain = resolve3;
|
|
1719
|
+
rejectChain = reject;
|
|
1720
|
+
});
|
|
1721
|
+
promise.catch(() => {
|
|
1722
|
+
return;
|
|
1723
|
+
});
|
|
1724
|
+
const removeListeners = () => {
|
|
1725
|
+
if (closed)
|
|
1726
|
+
return;
|
|
1727
|
+
closed = true;
|
|
1728
|
+
for (const [signal, handler] of handlers)
|
|
1729
|
+
source.off(signal, handler);
|
|
1730
|
+
if (!started)
|
|
1731
|
+
bound.delete(handle);
|
|
1732
|
+
};
|
|
1733
|
+
const close = () => {
|
|
1734
|
+
const hadStarted = started;
|
|
1735
|
+
removeListeners();
|
|
1736
|
+
if (!hadStarted)
|
|
1737
|
+
resolveChain();
|
|
1738
|
+
};
|
|
1739
|
+
const run = async (signal) => {
|
|
1740
|
+
try {
|
|
1741
|
+
await options.onClose?.(signal);
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
reportSignalError("prepare", error, options.onError);
|
|
1744
|
+
}
|
|
1745
|
+
try {
|
|
1746
|
+
await handle.close();
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
rejectChain(error);
|
|
1749
|
+
reportSignalError("close", error, options.onError);
|
|
1750
|
+
removeListeners();
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
resolveChain();
|
|
1754
|
+
try {
|
|
1755
|
+
await options.onComplete?.();
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
reportSignalError("complete", error, options.onError);
|
|
1758
|
+
} finally {
|
|
1759
|
+
removeListeners();
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
const onSignal = (signal) => {
|
|
1763
|
+
if (!started) {
|
|
1764
|
+
started = true;
|
|
1765
|
+
sameTurnAsStart = true;
|
|
1766
|
+
queueMicrotask(() => {
|
|
1767
|
+
sameTurnAsStart = false;
|
|
1768
|
+
});
|
|
1769
|
+
run(signal).catch((error) => reportSignalError("close", error, options.onError));
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
if (sameTurnAsStart)
|
|
1773
|
+
return;
|
|
1774
|
+
removeListeners();
|
|
1775
|
+
guardSignalCallback(() => options.onRepeatedSignal?.(signal), "close", options.onError);
|
|
1776
|
+
let restored = false;
|
|
1777
|
+
try {
|
|
1778
|
+
restored = source.raiseDefault(signal);
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
reportSignalError("close", error, options.onError);
|
|
1781
|
+
}
|
|
1782
|
+
if (!restored) {
|
|
1783
|
+
guardSignalCallback(() => options.onEscalationBlocked?.(signal), "close", options.onError);
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
for (const signal of signals) {
|
|
1787
|
+
const handler = () => onSignal(signal);
|
|
1788
|
+
handlers.push([signal, handler]);
|
|
1789
|
+
source.on(signal, handler);
|
|
1790
|
+
}
|
|
1791
|
+
return { promise, close };
|
|
1792
|
+
}
|
|
1412
1793
|
// src/tools/mount-download.ts
|
|
1413
|
-
import {
|
|
1414
|
-
import { z as z3 } from "zod";
|
|
1794
|
+
import { z as z6 } from "zod";
|
|
1415
1795
|
|
|
1416
1796
|
// src/tools/native-result.ts
|
|
1417
1797
|
function textResult(text, isError = false) {
|
|
@@ -1420,84 +1800,43 @@ function textResult(text, isError = false) {
|
|
|
1420
1800
|
}
|
|
1421
1801
|
|
|
1422
1802
|
// 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
1803
|
function mountDownload(server, config) {
|
|
1454
1804
|
const name = config.name ?? "download";
|
|
1455
1805
|
assertToolName(name, "<native>", "download");
|
|
1456
|
-
server.registerTool(name, { description: config.description, inputSchema:
|
|
1806
|
+
server.registerTool(name, { description: config.description, inputSchema: z6.object(config.inputSchema) }, async (rawArgs) => {
|
|
1457
1807
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
1458
1808
|
try {
|
|
1459
1809
|
const url = await config.resolveUrl(args);
|
|
1460
1810
|
if (!url)
|
|
1461
1811
|
return textResult("Nothing to download.", true);
|
|
1462
|
-
const
|
|
1812
|
+
const result = await runDownloadOperation({
|
|
1813
|
+
url,
|
|
1814
|
+
dir: config.dirFromArgs?.(args) ?? config.defaultDir,
|
|
1815
|
+
allowPrivateHosts: config.allowPrivateHosts,
|
|
1816
|
+
maxBytes: config.maxBytes,
|
|
1463
1817
|
timeoutMs: config.timeoutMs
|
|
1464
1818
|
});
|
|
1465
|
-
|
|
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));
|
|
1819
|
+
return textResult(JSON.stringify(result, null, 2));
|
|
1481
1820
|
} catch (err) {
|
|
1482
1821
|
return textResult(`Download failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
1483
1822
|
}
|
|
1484
1823
|
});
|
|
1485
1824
|
}
|
|
1486
1825
|
// src/tools/mount-upload.ts
|
|
1487
|
-
import { z as
|
|
1826
|
+
import { z as z7 } from "zod";
|
|
1488
1827
|
function mountUpload(server, config) {
|
|
1489
1828
|
const name = config.name ?? "upload";
|
|
1490
1829
|
assertToolName(name, "<native>", "upload");
|
|
1491
1830
|
server.registerTool(name, {
|
|
1492
1831
|
description: config.description,
|
|
1493
|
-
inputSchema: { path:
|
|
1832
|
+
inputSchema: { path: z7.string().describe("Path to a local file on this machine") }
|
|
1494
1833
|
}, async (rawArgs) => {
|
|
1495
1834
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
1496
1835
|
const path = typeof args.path === "string" ? args.path : "";
|
|
1497
1836
|
if (!path)
|
|
1498
1837
|
return textResult("Provide `path`.", true);
|
|
1499
1838
|
try {
|
|
1500
|
-
const uploaded = await config.upload
|
|
1839
|
+
const uploaded = await runUploadOperation(path, config.upload);
|
|
1501
1840
|
return textResult(JSON.stringify(uploaded ?? null, null, 2));
|
|
1502
1841
|
} catch (err) {
|
|
1503
1842
|
return textResult(`Upload failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
@@ -1505,15 +1844,16 @@ function mountUpload(server, config) {
|
|
|
1505
1844
|
});
|
|
1506
1845
|
}
|
|
1507
1846
|
// src/tools/mount-wait.ts
|
|
1508
|
-
import { z as
|
|
1847
|
+
import { z as z8 } from "zod";
|
|
1509
1848
|
function mountWait(server, config) {
|
|
1510
1849
|
const name = config.name ?? "wait";
|
|
1511
1850
|
assertToolName(name, "<native>", "wait");
|
|
1512
|
-
server.registerTool(name, { description: config.description, inputSchema:
|
|
1851
|
+
server.registerTool(name, { description: config.description, inputSchema: z8.object(config.inputSchema) }, async (rawArgs) => {
|
|
1513
1852
|
const args = isRecord(rawArgs) ? rawArgs : {};
|
|
1514
1853
|
try {
|
|
1515
|
-
const { state, timedOut } = await
|
|
1516
|
-
|
|
1854
|
+
const { state, timedOut } = await runWaitOperation({
|
|
1855
|
+
input: args,
|
|
1856
|
+
poll: (input) => config.poll(input),
|
|
1517
1857
|
done: config.done,
|
|
1518
1858
|
backoff: config.backoff,
|
|
1519
1859
|
timeoutSec: config.timeoutFromArgs?.(args) ?? config.defaultTimeout
|
|
@@ -1527,19 +1867,19 @@ function mountWait(server, config) {
|
|
|
1527
1867
|
}
|
|
1528
1868
|
// src/tools/oauth-provider.ts
|
|
1529
1869
|
import { randomUUID } from "node:crypto";
|
|
1530
|
-
import { z as
|
|
1870
|
+
import { z as z9 } from "zod";
|
|
1531
1871
|
function isPlainDisplayCharacter(character) {
|
|
1532
1872
|
const codePoint = character.codePointAt(0);
|
|
1533
1873
|
return codePoint !== undefined && codePoint >= 32 && codePoint !== 127 && character !== "<" && character !== ">";
|
|
1534
1874
|
}
|
|
1535
|
-
var CimdClientMetadataSchema =
|
|
1536
|
-
client_id:
|
|
1537
|
-
redirect_uris:
|
|
1538
|
-
client_name:
|
|
1875
|
+
var CimdClientMetadataSchema = z9.object({
|
|
1876
|
+
client_id: z9.url(),
|
|
1877
|
+
redirect_uris: z9.array(z9.url()).min(1),
|
|
1878
|
+
client_name: z9.string().trim().min(1).max(200).refine((value) => Array.from(value).every(isPlainDisplayCharacter), {
|
|
1539
1879
|
message: "client_name must be plain display text"
|
|
1540
1880
|
}),
|
|
1541
|
-
token_endpoint_auth_method:
|
|
1542
|
-
application_type:
|
|
1881
|
+
token_endpoint_auth_method: z9.literal("none"),
|
|
1882
|
+
application_type: z9.enum(["native", "web"]).optional()
|
|
1543
1883
|
});
|
|
1544
1884
|
function createSecureClientMetadataFetcher(policy = {}) {
|
|
1545
1885
|
assertPositiveInteger("cimd.maxBytes", policy.maxBytes ?? DEFAULT_CIMD_MAX_BYTES);
|
|
@@ -2332,168 +2672,6 @@ function summarizeTransports(surface) {
|
|
|
2332
2672
|
sources
|
|
2333
2673
|
};
|
|
2334
2674
|
}
|
|
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
2675
|
export {
|
|
2498
2676
|
wwwAuthenticateHeader,
|
|
2499
2677
|
validateMcpSchemas,
|
|
@@ -2516,7 +2694,12 @@ export {
|
|
|
2516
2694
|
flattenToolJsonSchema,
|
|
2517
2695
|
findUntypedProperties,
|
|
2518
2696
|
findNonPortableFormats,
|
|
2697
|
+
defineWaitTool,
|
|
2698
|
+
defineViewFileTool,
|
|
2699
|
+
defineUploadTool,
|
|
2519
2700
|
defineRuntimeTool,
|
|
2701
|
+
defineDownloadTool,
|
|
2702
|
+
defineCliCommand,
|
|
2520
2703
|
createToolkit,
|
|
2521
2704
|
createToolLogger,
|
|
2522
2705
|
createToolInvoker,
|
|
@@ -2530,8 +2713,16 @@ export {
|
|
|
2530
2713
|
coerceJsonArgs,
|
|
2531
2714
|
buildToolManifest,
|
|
2532
2715
|
buildMcpServer,
|
|
2716
|
+
bindStdioProcessSignals,
|
|
2717
|
+
ViewFileOutputSchema,
|
|
2718
|
+
ViewFileInputSchema,
|
|
2719
|
+
ViewFileErrorSchema,
|
|
2720
|
+
UploadToolInputSchema,
|
|
2533
2721
|
RESOURCE_MIME_TYPE,
|
|
2534
2722
|
PROTECTED_RESOURCE_PATH,
|
|
2535
2723
|
PORTABLE_JSON_SCHEMA_FORMATS,
|
|
2536
|
-
|
|
2724
|
+
McpMediaContentSchema,
|
|
2725
|
+
McpAnnotationsSchema,
|
|
2726
|
+
EXT_APPS_BUNDLE_PLACEHOLDER,
|
|
2727
|
+
DownloadResultSchema
|
|
2537
2728
|
};
|