@kolbo/mcp 1.86.0 → 1.86.4
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/package.json +2 -1
- package/skill/SKILL.md +435 -434
- package/skill/references/models/3d.md +51 -0
- package/skill/references/models/seedance.md +32 -4
- package/skill/references/workflows/cost-and-validation.md +1 -1
- package/src/apps/index.js +750 -737
- package/src/apps/theme.js +13 -0
- package/src/apps/widgets/generation.js +16 -11
- package/src/apps/widgets/mediaGrid.js +14 -1
- package/src/apps/widgets/upload.js +456 -457
- package/src/index.js +6 -1
- package/src/toolAnnotations.js +1 -1
- package/src/tools/_shared.js +183 -32
- package/src/tools/artifacts.js +5 -2
- package/src/tools/generate.js +141 -36
- package/src/tools/media.js +656 -656
- package/src/tools/projects.js +15 -0
- package/src/tools/visual_dna.js +459 -456
- package/src/tools/voices.js +1 -1
package/src/index.js
CHANGED
|
@@ -163,7 +163,12 @@ function createServer(opts = {}) {
|
|
|
163
163
|
// remote HTTP host enables it, so stdio clients (Kolbo Code / Desktop / Cursor)
|
|
164
164
|
// keep identical text-URL output. `apps` gates interactive widget results
|
|
165
165
|
// (MCP Apps) the same way — see src/apps/index.js.
|
|
166
|
-
const toolOptions = {
|
|
166
|
+
const toolOptions = {
|
|
167
|
+
inlineImages: !!opts.inlineImages,
|
|
168
|
+
remote: !!opts.remote || !!opts.apps,
|
|
169
|
+
apps: !!opts.apps,
|
|
170
|
+
asyncGenerations: !!opts.asyncGenerations,
|
|
171
|
+
};
|
|
167
172
|
registerGenerateTools(server, client, toolOptions);
|
|
168
173
|
registerModelTools(server, client, toolOptions);
|
|
169
174
|
registerVoiceTools(server, client, toolOptions);
|
package/src/toolAnnotations.js
CHANGED
|
@@ -51,7 +51,7 @@ const PRIVATE_WRITE = [
|
|
|
51
51
|
'move_session', 'bulk_move_sessions', 'move_generations_to_session',
|
|
52
52
|
'split_session', 'undo_session_organization',
|
|
53
53
|
'rename_session', 'restore_session',
|
|
54
|
-
'create_project', 'update_project',
|
|
54
|
+
'create_project', 'duplicate_project', 'update_project',
|
|
55
55
|
'archive_project', 'unarchive_project', 'add_project_context',
|
|
56
56
|
'link_project_asset', 'unlink_project_asset',
|
|
57
57
|
'create_agent',
|
package/src/tools/_shared.js
CHANGED
|
@@ -23,11 +23,14 @@
|
|
|
23
23
|
const fs = require('fs');
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const net = require('net');
|
|
26
|
+
const dns = require('dns').promises;
|
|
27
|
+
const { Agent, fetch: undiciFetch } = require('undici');
|
|
26
28
|
|
|
27
29
|
const MAX_FILE_BYTES = 500 * 1024 * 1024; // 500 MB — larger than visual_dna because
|
|
28
30
|
// lipsync/v2v/transcription accept full
|
|
29
31
|
// videos and long audio tracks.
|
|
30
32
|
const VISUAL_DNA_MAX_BYTES = 25 * 1024 * 1024; // kept for visual_dna backward-compat
|
|
33
|
+
const REMOTE_FETCH_MAX_BYTES = 100 * 1024 * 1024;
|
|
31
34
|
const MAX_REDIRECTS = 5;
|
|
32
35
|
|
|
33
36
|
// THE single statement of how a local file gets into Kolbo. It is repeated to
|
|
@@ -67,16 +70,21 @@ const REMOTE_FILE_HINT =
|
|
|
67
70
|
const LOCAL_FILE_HINT =
|
|
68
71
|
' LOCAL FILE? Absolute local paths work here (server and client share a filesystem). ' +
|
|
69
72
|
'Never reply that you cannot upload files — for a file you will reference more than once, call `upload_media` first and reuse the returned https:// URL.';
|
|
73
|
+
const REMOTE_TEXT_FILE_HINT =
|
|
74
|
+
' REMOTE FILE INPUT: This client cannot send local filesystem paths or render Kolbo\'s upload widget. ' +
|
|
75
|
+
'Use an existing public https:// URL. If the attachment has no public URL, ask the user to upload it in the Kolbo Media Library and paste the resulting URL; never invent a URL or claim a local path is usable here.';
|
|
70
76
|
|
|
71
77
|
/**
|
|
72
78
|
* Append the transport-correct local-file route to every media-input tool's
|
|
73
79
|
* description, post-registration (same pattern as attachToolWidgetMeta).
|
|
74
|
-
* `options.
|
|
75
|
-
*
|
|
76
|
-
*
|
|
80
|
+
* `options.remote === true` is set only by kolbo-api's remote per-request
|
|
81
|
+
* server. Do not use `appsEnabled()` as the transport signal: stdio hosts can
|
|
82
|
+
* render widgets while still sharing a filesystem with this process.
|
|
77
83
|
*/
|
|
78
84
|
function attachFileInputHints(server, options = {}) {
|
|
79
|
-
const hint = options.
|
|
85
|
+
const hint = options.asyncGenerations
|
|
86
|
+
? REMOTE_TEXT_FILE_HINT
|
|
87
|
+
: (options.remote === true || options.apps === true ? REMOTE_FILE_HINT : LOCAL_FILE_HINT);
|
|
80
88
|
const registered = server._registeredTools || {};
|
|
81
89
|
for (const name of FILE_INPUT_TOOLS) {
|
|
82
90
|
const t = registered[name];
|
|
@@ -99,6 +107,7 @@ function isPrivateIPv4(ip) {
|
|
|
99
107
|
if (parts.length !== 4 || parts.some(p => Number.isNaN(p) || p < 0 || p > 255)) return true;
|
|
100
108
|
const [a, b] = parts;
|
|
101
109
|
if (a === 10) return true;
|
|
110
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
102
111
|
if (a === 127) return true;
|
|
103
112
|
if (a === 0) return true;
|
|
104
113
|
if (a === 169 && b === 254) return true; // includes 169.254.169.254 cloud metadata
|
|
@@ -166,21 +175,100 @@ function assertSafeUrl(rawUrl) {
|
|
|
166
175
|
return u;
|
|
167
176
|
}
|
|
168
177
|
|
|
178
|
+
async function resolvePublicAddresses(hostname) {
|
|
179
|
+
let host = hostname.toLowerCase();
|
|
180
|
+
if (host.startsWith('[') && host.endsWith(']')) host = host.slice(1, -1);
|
|
181
|
+
const literalFamily = net.isIP(host);
|
|
182
|
+
if (literalFamily) return [{ address: host, family: literalFamily }];
|
|
183
|
+
|
|
184
|
+
let timer;
|
|
185
|
+
const timeout = new Promise((_, reject) => {
|
|
186
|
+
timer = setTimeout(() => reject(new Error(`DNS lookup timed out for ${host}`)), 3000);
|
|
187
|
+
});
|
|
188
|
+
let rows;
|
|
189
|
+
try {
|
|
190
|
+
rows = await Promise.race([dns.lookup(host, { all: true, verbatim: true }), timeout]);
|
|
191
|
+
} finally {
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
}
|
|
194
|
+
if (!rows.length) throw new Error(`DNS lookup returned no addresses for ${host}`);
|
|
195
|
+
for (const row of rows) {
|
|
196
|
+
const blocked = row.family === 4 ? isPrivateIPv4(row.address) : isPrivateIPv6(row.address);
|
|
197
|
+
if (blocked) throw new Error(`Refusing private / loopback / metadata DNS target for ${host}`);
|
|
198
|
+
}
|
|
199
|
+
return rows;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function pinnedDispatcher(addresses) {
|
|
203
|
+
let cursor = 0;
|
|
204
|
+
return new Agent({
|
|
205
|
+
connect: {
|
|
206
|
+
lookup(_hostname, options, callback) {
|
|
207
|
+
if (options?.all) return callback(null, addresses);
|
|
208
|
+
const row = addresses[cursor++ % addresses.length];
|
|
209
|
+
return callback(null, row.address, row.family);
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
169
215
|
async function safeFetch(rawUrl, opts = {}) {
|
|
170
216
|
let current = rawUrl;
|
|
171
217
|
for (let i = 0; i <= MAX_REDIRECTS; i++) {
|
|
172
|
-
assertSafeUrl(current);
|
|
173
|
-
const
|
|
218
|
+
const url = assertSafeUrl(current);
|
|
219
|
+
const addresses = await resolvePublicAddresses(url.hostname);
|
|
220
|
+
const dispatcher = pinnedDispatcher(addresses);
|
|
221
|
+
let res;
|
|
222
|
+
try {
|
|
223
|
+
res = await undiciFetch(current, { redirect: 'manual', signal: opts.signal, dispatcher });
|
|
224
|
+
} catch (err) {
|
|
225
|
+
await dispatcher.close().catch(() => {});
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
174
228
|
if (res.status >= 300 && res.status < 400 && res.headers.get('location')) {
|
|
175
229
|
const next = new URL(res.headers.get('location'), current).toString();
|
|
230
|
+
await res.body?.cancel().catch(() => {});
|
|
231
|
+
await dispatcher.close().catch(() => {});
|
|
176
232
|
current = next;
|
|
177
233
|
continue;
|
|
178
234
|
}
|
|
235
|
+
// close() waits for this response body to be consumed, so schedule it but
|
|
236
|
+
// do not await it before returning the Response to the caller.
|
|
237
|
+
dispatcher.close().catch(() => {});
|
|
179
238
|
return res;
|
|
180
239
|
}
|
|
181
240
|
throw new Error(`Too many redirects fetching ${rawUrl}`);
|
|
182
241
|
}
|
|
183
242
|
|
|
243
|
+
async function discardResponse(res) {
|
|
244
|
+
try { await res?.body?.cancel(); } catch (_) {}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function readResponseBuffer(res, maxBytes) {
|
|
248
|
+
if (!res?.body || typeof res.body.getReader !== 'function') {
|
|
249
|
+
throw new Error('Remote response has no readable body');
|
|
250
|
+
}
|
|
251
|
+
const reader = res.body.getReader();
|
|
252
|
+
const chunks = [];
|
|
253
|
+
let total = 0;
|
|
254
|
+
try {
|
|
255
|
+
while (true) {
|
|
256
|
+
const { done, value } = await reader.read();
|
|
257
|
+
if (done) break;
|
|
258
|
+
const chunk = Buffer.from(value);
|
|
259
|
+
total += chunk.length;
|
|
260
|
+
if (total > maxBytes) {
|
|
261
|
+
await reader.cancel().catch(() => {});
|
|
262
|
+
throw new Error(`Remote response exceeds ${maxBytes}-byte limit`);
|
|
263
|
+
}
|
|
264
|
+
chunks.push(chunk);
|
|
265
|
+
}
|
|
266
|
+
} finally {
|
|
267
|
+
try { reader.releaseLock(); } catch (_) {}
|
|
268
|
+
}
|
|
269
|
+
return Buffer.concat(chunks, total);
|
|
270
|
+
}
|
|
271
|
+
|
|
184
272
|
function guessFilename(source, fallbackExt) {
|
|
185
273
|
if (isHttpUrl(source)) {
|
|
186
274
|
try {
|
|
@@ -219,21 +307,24 @@ function guessContentType(filename) {
|
|
|
219
307
|
* @returns {Promise<{buffer: Buffer, filename: string, contentType: string, size: number}>}
|
|
220
308
|
*/
|
|
221
309
|
async function resolveToBuffer(source, kind, opts = {}) {
|
|
222
|
-
const
|
|
310
|
+
const requestedMaxBytes = opts.maxBytes || MAX_FILE_BYTES;
|
|
311
|
+
const maxBytes = opts.allowLocalFiles === false
|
|
312
|
+
? Math.min(requestedMaxBytes, REMOTE_FETCH_MAX_BYTES)
|
|
313
|
+
: requestedMaxBytes;
|
|
223
314
|
const defaultExt = kind === 'image' ? '.png' : kind === 'video' ? '.mp4' : '.mp3';
|
|
224
315
|
|
|
225
316
|
if (isHttpUrl(source)) {
|
|
226
317
|
const res = await safeFetch(source);
|
|
227
|
-
if (!res.ok)
|
|
318
|
+
if (!res.ok) {
|
|
319
|
+
await discardResponse(res);
|
|
320
|
+
throw new Error(`Failed to fetch ${source}: ${res.status} ${res.statusText}`);
|
|
321
|
+
}
|
|
228
322
|
const contentLen = parseInt(res.headers.get('content-length') || '0', 10);
|
|
229
323
|
if (contentLen && contentLen > maxBytes) {
|
|
324
|
+
await discardResponse(res);
|
|
230
325
|
throw new Error(`File at ${source} (${contentLen} bytes) exceeds ${maxBytes}-byte limit`);
|
|
231
326
|
}
|
|
232
|
-
const
|
|
233
|
-
const buffer = Buffer.from(arrayBuf);
|
|
234
|
-
if (buffer.length > maxBytes) {
|
|
235
|
-
throw new Error(`File at ${source} (${buffer.length} bytes) exceeds ${maxBytes}-byte limit`);
|
|
236
|
-
}
|
|
327
|
+
const buffer = await readResponseBuffer(res, maxBytes);
|
|
237
328
|
const filename = guessFilename(source, defaultExt);
|
|
238
329
|
return {
|
|
239
330
|
buffer,
|
|
@@ -243,6 +334,12 @@ async function resolveToBuffer(source, kind, opts = {}) {
|
|
|
243
334
|
};
|
|
244
335
|
}
|
|
245
336
|
|
|
337
|
+
if (opts.allowLocalFiles === false) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
'This remote connector accepts public https:// URLs only. Upload the file to the Kolbo Media Library and pass its public URL.'
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
246
343
|
if (!path.isAbsolute(source)) {
|
|
247
344
|
// `path.isAbsolute` is platform-specific: on a POSIX server (every remote
|
|
248
345
|
// connector deployment) a valid Windows path like `C:\Users\...` or
|
|
@@ -410,21 +507,32 @@ async function inlineImageBlocks(urls, opts = {}) {
|
|
|
410
507
|
// bounds concurrency, and this sits on the connector response path right
|
|
411
508
|
// after generation. Order is preserved by map-then-filter; any failure (size,
|
|
412
509
|
// type, timeout, network) returns null and falls back to URL-only.
|
|
510
|
+
const maxCount = Math.min(INLINE_IMG_MAX_COUNT, Math.max(1, Number(opts.maxCount) || INLINE_IMG_MAX_COUNT));
|
|
511
|
+
const maxBytes = Math.min(INLINE_IMG_MAX_BYTES, Math.max(1, Number(opts.maxBytes) || INLINE_IMG_MAX_BYTES));
|
|
512
|
+
const fetchTimeoutMs = Math.min(INLINE_IMG_FETCH_TIMEOUT_MS, Math.max(1, Number(opts.fetchTimeoutMs) || INLINE_IMG_FETCH_TIMEOUT_MS));
|
|
413
513
|
const blocks = await Promise.all(
|
|
414
|
-
urls.slice(0,
|
|
514
|
+
urls.slice(0, maxCount).map(async (url) => {
|
|
415
515
|
const controller = new AbortController();
|
|
416
|
-
const timer = setTimeout(() => controller.abort(),
|
|
516
|
+
const timer = setTimeout(() => controller.abort(), fetchTimeoutMs);
|
|
417
517
|
try {
|
|
418
518
|
if (typeof url !== 'string' || !isHttpUrl(url)) return null;
|
|
419
519
|
const res = await safeFetch(url, { signal: controller.signal });
|
|
420
|
-
if (!res.ok)
|
|
520
|
+
if (!res.ok) {
|
|
521
|
+
await discardResponse(res);
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
421
524
|
const contentType = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
|
|
422
|
-
if (!contentType.startsWith('image/'))
|
|
525
|
+
if (!contentType.startsWith('image/')) {
|
|
526
|
+
await discardResponse(res);
|
|
527
|
+
return null; // never embed non-images
|
|
528
|
+
}
|
|
423
529
|
const declaredLen = Number(res.headers.get('content-length') || 0);
|
|
424
|
-
if (declaredLen && declaredLen >
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
530
|
+
if (declaredLen && declaredLen > maxBytes) {
|
|
531
|
+
await discardResponse(res);
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
const buffer = await readResponseBuffer(res, maxBytes);
|
|
535
|
+
return { type: 'image', data: buffer.toString('base64'), mimeType: contentType };
|
|
428
536
|
} catch (_) {
|
|
429
537
|
return null;
|
|
430
538
|
} finally {
|
|
@@ -440,27 +548,32 @@ async function inlineImageBlocks(urls, opts = {}) {
|
|
|
440
548
|
// tool to the frontend page + tool slug whose session view can RESUME that
|
|
441
549
|
// session (mirrors kolbo-map src/constants/sessionTypes.js resumeUrl map — the
|
|
442
550
|
// route must match the SESSION MODEL the SDK created, per sdkSessionManager):
|
|
443
|
-
// ImageSession → /image-tools?tool=
|
|
444
|
-
// imgEditSession (
|
|
445
|
-
//
|
|
446
|
-
//
|
|
551
|
+
// ImageSession (image AND image_edit) → /image-tools?tool=create-image
|
|
552
|
+
// imgEditSession (edit_image / global_image_edit — the Canvas) → /image-tools?tool=canvas
|
|
553
|
+
// imgToVideoSession (video, video_from_image, elements, first_last_frame)
|
|
554
|
+
// → /video-tools?tool=create-video
|
|
447
555
|
// videoToVideoSession → /video-tools?tool=video-to-video
|
|
448
556
|
// lipsyncSession → /video-tools?tool=lipsync
|
|
449
557
|
// MusicGeneratorSession / TextToSpeechSession / textToSoundSession /
|
|
450
558
|
// speechToTextSession → /audio-tools with the matching slug
|
|
451
559
|
// CreativeDirectorSession → /creative-director?session=... (no tool param)
|
|
560
|
+
// RETIRED — do NOT reintroduce as separate destinations. "Image Editing" folded
|
|
561
|
+
// into Create Image and "Text to Video" folded into Create Video (a mode inside
|
|
562
|
+
// it); sdkSessionManager already routes image_edit → ImageSession and video →
|
|
563
|
+
// imgToVideoSession. The old ?tool=image-editing / ?tool=text-to-video slugs
|
|
564
|
+
// still redirect, but nothing new should emit them.
|
|
452
565
|
// Intentionally ABSENT (no deep-linkable session page — widget falls back to
|
|
453
566
|
// plain https://app.kolbo.ai): edit_video (GlobalVideoEditSession has no
|
|
454
567
|
// session deep-link), generate_3d (project-scoped, no session), shorts render.
|
|
455
568
|
const APP_BASE_URL = 'https://app.kolbo.ai';
|
|
456
569
|
const OPEN_URL_ROUTES = {
|
|
457
|
-
generate_image: { path: '/image-tools', tool: '
|
|
458
|
-
generate_image_edit: { path: '/image-tools', tool: 'image
|
|
459
|
-
edit_image: { path: '/image-tools', tool: '
|
|
460
|
-
generate_video: { path: '/video-tools', tool: '
|
|
461
|
-
generate_video_from_image: { path: '/video-tools', tool: '
|
|
462
|
-
generate_elements: { path: '/video-tools', tool: '
|
|
463
|
-
generate_first_last_frame: { path: '/video-tools', tool: '
|
|
570
|
+
generate_image: { path: '/image-tools', tool: 'create-image' },
|
|
571
|
+
generate_image_edit: { path: '/image-tools', tool: 'create-image' },
|
|
572
|
+
edit_image: { path: '/image-tools', tool: 'canvas' },
|
|
573
|
+
generate_video: { path: '/video-tools', tool: 'create-video' },
|
|
574
|
+
generate_video_from_image: { path: '/video-tools', tool: 'create-video' },
|
|
575
|
+
generate_elements: { path: '/video-tools', tool: 'create-video', mode: 'elements' },
|
|
576
|
+
generate_first_last_frame: { path: '/video-tools', tool: 'create-video', mode: 'first-last' },
|
|
464
577
|
generate_video_from_video: { path: '/video-tools', tool: 'video-to-video' },
|
|
465
578
|
generate_lipsync: { path: '/video-tools', tool: 'lipsync' },
|
|
466
579
|
generate_music: { path: '/audio-tools', tool: 'music-generator' },
|
|
@@ -796,6 +909,42 @@ async function uiGenerating(p) {
|
|
|
796
909
|
return uiResult(UI.generation, text, structured);
|
|
797
910
|
}
|
|
798
911
|
|
|
912
|
+
/**
|
|
913
|
+
* Return a paid generation immediately for hosts that cannot render MCP Apps
|
|
914
|
+
* and enforce short tool-call timeouts (for example Manus custom MCP). This is
|
|
915
|
+
* deliberately plain MCP content: no ui:// resource and no promise of a card.
|
|
916
|
+
* The existing get_generation_status tool is the durable status endpoint.
|
|
917
|
+
*/
|
|
918
|
+
function asyncGenerating(p) {
|
|
919
|
+
const ids = Array.isArray(p.generation_ids) && p.generation_ids.length
|
|
920
|
+
? p.generation_ids
|
|
921
|
+
: [p.gen.generation_id].filter(Boolean);
|
|
922
|
+
const defaultStatusArgs = ids.length > 1
|
|
923
|
+
? { generation_ids: ids, wait: false }
|
|
924
|
+
: { generation_id: ids[0], wait: false };
|
|
925
|
+
const pollTool = p.poll_tool || 'get_generation_status';
|
|
926
|
+
const statusArgs = { ...(p.status_args || defaultStatusArgs), wait: false };
|
|
927
|
+
const structured = {
|
|
928
|
+
status: 'submitted',
|
|
929
|
+
generation_id: p.gen.generation_id,
|
|
930
|
+
session_id: p.gen.session_id,
|
|
931
|
+
...(ids.length > 1 ? { batch: true, generation_ids: ids } : {}),
|
|
932
|
+
...(p.failed_submissions && p.failed_submissions.length
|
|
933
|
+
? { failed_submissions: p.failed_submissions } : {}),
|
|
934
|
+
...(p.warning ? { warning: p.warning } : {}),
|
|
935
|
+
poll_tool: pollTool,
|
|
936
|
+
status_args: statusArgs,
|
|
937
|
+
next_action: `The job is running. Do not submit it again. When the result is needed, call ${pollTool} with the supplied status_args. If it is still processing, tell the user and check again later; do not poll in a tight loop.`,
|
|
938
|
+
paid_job_notice: ids.length > 1
|
|
939
|
+
? `This is a paid batch. Only if the user asks to stop or approves a replacement, cancel every running generation first: ${ids.join(', ')}.`
|
|
940
|
+
: 'This is a paid generation. Only if the user asks to stop or approves a replacement, cancel this generation before starting the replacement.',
|
|
941
|
+
};
|
|
942
|
+
return {
|
|
943
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
944
|
+
structuredContent: structured,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
|
|
799
948
|
/**
|
|
800
949
|
* Wrap an already-completed generation result with the widget.
|
|
801
950
|
*
|
|
@@ -1019,6 +1168,7 @@ module.exports = {
|
|
|
1019
1168
|
isHttpUrl,
|
|
1020
1169
|
assertSafeUrl,
|
|
1021
1170
|
safeFetch,
|
|
1171
|
+
readResponseBuffer,
|
|
1022
1172
|
guessFilename,
|
|
1023
1173
|
guessContentType,
|
|
1024
1174
|
resolveToBuffer,
|
|
@@ -1032,6 +1182,7 @@ module.exports = {
|
|
|
1032
1182
|
linkFields,
|
|
1033
1183
|
buildProjectUrl,
|
|
1034
1184
|
uiGenerating,
|
|
1185
|
+
asyncGenerating,
|
|
1035
1186
|
uiCompleted,
|
|
1036
1187
|
insufficientCreditsResult,
|
|
1037
1188
|
appsEnabled,
|
package/src/tools/artifacts.js
CHANGED
|
@@ -9,7 +9,7 @@ const { resolveToBuffer } = require('./_shared');
|
|
|
9
9
|
// An HTML/SVG/Mermaid document is text — cap well below the media limit.
|
|
10
10
|
const MAX_ARTIFACT_BYTES = 5 * 1024 * 1024;
|
|
11
11
|
|
|
12
|
-
function registerArtifactTools(server, client) {
|
|
12
|
+
function registerArtifactTools(server, client, options = {}) {
|
|
13
13
|
// ─── publish_html_artifact ─────────────────────────────────────
|
|
14
14
|
server.tool(
|
|
15
15
|
'publish_html_artifact',
|
|
@@ -34,7 +34,10 @@ function registerArtifactTools(server, client) {
|
|
|
34
34
|
if (hasPath) {
|
|
35
35
|
// resolveToBuffer gives us the absolute-path check, the SSRF guard for
|
|
36
36
|
// https:// sources, and the remote-connector error message for free.
|
|
37
|
-
const { buffer } = await resolveToBuffer(file_path.trim(), 'html', {
|
|
37
|
+
const { buffer } = await resolveToBuffer(file_path.trim(), 'html', {
|
|
38
|
+
maxBytes: MAX_ARTIFACT_BYTES,
|
|
39
|
+
allowLocalFiles: !options.remote,
|
|
40
|
+
});
|
|
38
41
|
body_content = buffer.toString('utf8');
|
|
39
42
|
if (!body_content.trim()) throw new Error(`File is empty: ${file_path}`);
|
|
40
43
|
}
|