@buildinternet/uploads 0.11.0 → 0.12.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 +14 -3
- package/dist/async.d.ts +5 -0
- package/dist/async.js +21 -0
- package/dist/cli-catalog.d.ts +7 -0
- package/dist/cli-catalog.js +52 -1
- package/dist/cli.js +12 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +13 -7
- package/dist/commands/completion.js +15 -5
- package/dist/commands/install.js +5 -0
- package/dist/commands/screenshot.d.ts +6 -0
- package/dist/commands/screenshot.js +317 -0
- package/dist/commands.d.ts +162 -2
- package/dist/commands.js +440 -143
- package/dist/config-file.d.ts +28 -2
- package/dist/config-file.js +44 -6
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/mcp/batch-error.d.ts +16 -0
- package/dist/mcp/batch-error.js +24 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +16 -0
- package/dist/mcp/tools.d.ts +8 -1
- package/dist/mcp/tools.js +393 -94
- package/dist/screenshot-local.d.ts +64 -0
- package/dist/screenshot-local.js +310 -0
- package/dist/screenshot-remote.d.ts +23 -0
- package/dist/screenshot-remote.js +79 -0
- package/dist/screenshot.d.ts +74 -0
- package/dist/screenshot.js +231 -0
- package/package.json +4 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP tool set mirroring the CLI commands (put, attach, list, delete,
|
|
3
|
-
* usage, reconcile, purge_expired, comment, health, doctor). Config is
|
|
4
|
-
* resolved fresh per tool call so a
|
|
5
|
-
* per-call `workspace` argument behaves like the CLI's --workspace flag, and
|
|
6
|
-
* a missing token surfaces as a tool error rather than a startup failure.
|
|
7
|
-
*/
|
|
8
|
-
import { basename } from "node:path";
|
|
9
1
|
import { createUploadsClient } from "../client.js";
|
|
10
|
-
import { buildDoctorReport, makeGhTarget,
|
|
2
|
+
import { buildDoctorReport, makeGhTarget, syncAttachmentsComment, uploadAttachments, uploadPreparedImage, uploadPuts, } from "../commands.js";
|
|
11
3
|
import { resolveFrameId } from "../frame.js";
|
|
12
4
|
import { resolveConfig, resolvePutDefaults, } from "../config.js";
|
|
13
|
-
import { buildMarkdown } from "../embed.js";
|
|
14
|
-
import { urlForGithubEmbed } from "../public-urls.js";
|
|
15
5
|
import { resolvePutPrefix } from "../destinations.js";
|
|
16
|
-
import {
|
|
6
|
+
import { ghKeyPrefix, ghMetadataFromTarget } from "../github.js";
|
|
17
7
|
import { validateMetaMap } from "../metadata.js";
|
|
18
|
-
import { rewriteKeyExtension } from "../optimize.js";
|
|
19
|
-
import { buildCliProvenance } from "../provenance.js";
|
|
20
8
|
import { execRunner, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
21
9
|
import { METADATA_DESCRIPTION, metadataProp, optPosInt, optString, optStringArray, optStringRecord, usage, } from "./args.js";
|
|
10
|
+
import { batchFailureMessage, ToolBatchError } from "./server.js";
|
|
22
11
|
import { attachmentFromText, buildReportPayload, parseReportType, REPORT_TYPES, submitReport, validateReportMessage, } from "../report.js";
|
|
23
12
|
import { resolveApiUrl } from "../config.js";
|
|
24
13
|
function optBool(args, name) {
|
|
@@ -270,13 +259,18 @@ export function createUploadsMcpTools(opts) {
|
|
|
270
259
|
},
|
|
271
260
|
{
|
|
272
261
|
name: "put",
|
|
273
|
-
description: "Upload
|
|
262
|
+
description: "Upload one or more files to uploads.sh and get public URL(s) plus GitHub-ready embed markdown. Single-file: pass `file` or `contentBase64`+`filename` (flat result with `url`/`embedUrl`/`markdown`). Multi-file: pass `files` (paths; parallel; returns `uploads`+`failures`). Prefer `embedUrl` in PR/issue markdown. With `pr`/`issue` keys are stable and `comment` syncs the managed attachments comment. All uploads are public; pr/issue keys are predictable — upload only non-sensitive media.",
|
|
274
263
|
inputSchema: {
|
|
275
264
|
type: "object",
|
|
276
265
|
properties: {
|
|
277
266
|
file: {
|
|
278
267
|
type: "string",
|
|
279
|
-
description: "Path of
|
|
268
|
+
description: "Path of a single file to upload. Exactly one of file, files, or contentBase64 is required.",
|
|
269
|
+
},
|
|
270
|
+
files: {
|
|
271
|
+
type: "array",
|
|
272
|
+
items: { type: "string" },
|
|
273
|
+
description: "Paths of multiple files to upload in parallel. Returns { uploads, failures }. Cannot combine with file, contentBase64, key, or filename.",
|
|
280
274
|
},
|
|
281
275
|
contentBase64: {
|
|
282
276
|
type: "string",
|
|
@@ -284,11 +278,11 @@ export function createUploadsMcpTools(opts) {
|
|
|
284
278
|
},
|
|
285
279
|
filename: {
|
|
286
280
|
type: "string",
|
|
287
|
-
description: "Filename for contentBase64 content (drives the key and content type). With `file`, overrides the key's leaf (clean name) while keeping the pr/default path.",
|
|
281
|
+
description: "Filename for contentBase64 content (drives the key and content type). With single `file`, overrides the key's leaf (clean name) while keeping the pr/default path.",
|
|
288
282
|
},
|
|
289
283
|
key: {
|
|
290
284
|
type: "string",
|
|
291
|
-
description: "Explicit object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>).
|
|
285
|
+
description: "Explicit object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.<ext>). Single file only; cannot be combined with pr/issue.",
|
|
292
286
|
},
|
|
293
287
|
destination: {
|
|
294
288
|
type: "string",
|
|
@@ -308,7 +302,10 @@ export function createUploadsMcpTools(opts) {
|
|
|
308
302
|
type: "string",
|
|
309
303
|
description: "PR/issue/branch key segment (default: today, or UPLOADS_DEFAULT_REF). Cannot be combined with pr/issue.",
|
|
310
304
|
},
|
|
311
|
-
alt: {
|
|
305
|
+
alt: {
|
|
306
|
+
type: "string",
|
|
307
|
+
description: "Alt text for the markdown (default: each file's name; with multiple files applies to all).",
|
|
308
|
+
},
|
|
312
309
|
width: {
|
|
313
310
|
type: "number",
|
|
314
311
|
description: "Emit <img width=…> markdown instead of a plain image embed.",
|
|
@@ -350,14 +347,24 @@ export function createUploadsMcpTools(opts) {
|
|
|
350
347
|
},
|
|
351
348
|
async handler(args) {
|
|
352
349
|
const file = optString(args, "file");
|
|
350
|
+
const filesArg = optStringArray(args, "files");
|
|
353
351
|
const contentBase64 = optString(args, "contentBase64");
|
|
354
|
-
if (
|
|
355
|
-
usage("
|
|
352
|
+
if (filesArg !== undefined && filesArg.length === 0) {
|
|
353
|
+
usage("files must be a non-empty array of paths");
|
|
354
|
+
}
|
|
355
|
+
const multi = filesArg !== undefined;
|
|
356
|
+
const sources = [file !== undefined, multi, contentBase64 !== undefined];
|
|
357
|
+
if (sources.filter(Boolean).length !== 1) {
|
|
358
|
+
usage("exactly one of file, files, or contentBase64 is required");
|
|
356
359
|
}
|
|
357
360
|
const filenameArg = optString(args, "filename");
|
|
358
361
|
if (contentBase64 !== undefined && !filenameArg) {
|
|
359
362
|
usage("filename is required with contentBase64");
|
|
360
363
|
}
|
|
364
|
+
if (multi && filenameArg)
|
|
365
|
+
usage("filename cannot be combined with files");
|
|
366
|
+
if (multi && optString(args, "key"))
|
|
367
|
+
usage("key cannot be combined with files");
|
|
361
368
|
const target = ghTargetFromArgs(args, run);
|
|
362
369
|
const wantComment = optBool(args, "comment");
|
|
363
370
|
const dryRun = optBool(args, "dryRun");
|
|
@@ -396,67 +403,357 @@ export function createUploadsMcpTools(opts) {
|
|
|
396
403
|
usage(err instanceof Error ? err.message : String(err));
|
|
397
404
|
}
|
|
398
405
|
const { client } = clientFor(args);
|
|
399
|
-
const bytes = file !== undefined
|
|
400
|
-
? readFileArg(file)
|
|
401
|
-
: new Uint8Array(Buffer.from(contentBase64, "base64"));
|
|
402
|
-
const sourceName = file !== undefined ? (filenameArg ?? basename(file)) : filenameArg;
|
|
403
406
|
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
404
407
|
const frameOpts = mcpFrameOptions(args);
|
|
405
408
|
const optimizeOpts = mcpOptimizeOptions(args, defaults);
|
|
406
|
-
const prepared = await prepareImageForUpload(bytes, sourceName, {
|
|
407
|
-
...frameOpts,
|
|
408
|
-
optimize: optimizeOpts,
|
|
409
|
-
});
|
|
410
|
-
const filename = prepared.filename;
|
|
411
|
-
let key = target ? ghAttachmentKey(target, filename) : keyArg;
|
|
412
|
-
if (key && prepared.optimized)
|
|
413
|
-
key = rewriteKeyExtension(key, filename);
|
|
414
409
|
const noGit = optBool(args, "noGit") || defaults.noGit === true;
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
410
|
+
const alt = optString(args, "alt");
|
|
411
|
+
const width = optPosInt(args, "width") ?? defaults.width;
|
|
412
|
+
const contentType = optString(args, "contentType");
|
|
413
|
+
const putShared = {
|
|
414
|
+
client,
|
|
415
|
+
ghTarget: target,
|
|
418
416
|
prefix: resolvedPrefix ?? defaults.prefix,
|
|
419
417
|
repo: optString(args, "repo") ?? defaults.repo,
|
|
420
418
|
ref: refArg ?? defaults.ref,
|
|
421
|
-
contentType: prepared.optimized ? prepared.contentType : optString(args, "contentType"),
|
|
422
419
|
deriveRepoFromGit: !noGit,
|
|
420
|
+
contentType,
|
|
423
421
|
dryRun,
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
client: "uploads-mcp",
|
|
427
|
-
optimized: prepared.optimized,
|
|
428
|
-
frameId: prepared.frame?.framed ? prepared.frame.frameId : undefined,
|
|
429
|
-
keepExif: optimizeOpts.keepExif === true,
|
|
430
|
-
}),
|
|
422
|
+
optimize: optimizeOpts,
|
|
423
|
+
frame: frameOpts,
|
|
431
424
|
metadata,
|
|
425
|
+
provenanceClient: "uploads-mcp",
|
|
426
|
+
alt,
|
|
427
|
+
width,
|
|
428
|
+
};
|
|
429
|
+
// Multi-file path (paths only — no base64 batch).
|
|
430
|
+
if (multi) {
|
|
431
|
+
const { uploads, failures } = await uploadPuts({
|
|
432
|
+
...putShared,
|
|
433
|
+
files: filesArg,
|
|
434
|
+
});
|
|
435
|
+
if (uploads.length === 0 && failures.length > 0) {
|
|
436
|
+
throw new ToolBatchError(batchFailureMessage(failures), { uploads, failures });
|
|
437
|
+
}
|
|
438
|
+
if (wantComment && target && uploads.length > 0) {
|
|
439
|
+
const { comment, commentError } = await syncComment(client, target);
|
|
440
|
+
return { uploads, failures, comment, commentError };
|
|
441
|
+
}
|
|
442
|
+
return { uploads, failures };
|
|
443
|
+
}
|
|
444
|
+
// Single-file: contentBase64 still supported; paths go through uploadPuts.
|
|
445
|
+
if (contentBase64 !== undefined) {
|
|
446
|
+
const sourceName = filenameArg;
|
|
447
|
+
const bytes = new Uint8Array(Buffer.from(contentBase64, "base64"));
|
|
448
|
+
const { result, prepared, markdown } = await uploadPreparedImage(client, bytes, sourceName, {
|
|
449
|
+
frame: frameOpts,
|
|
450
|
+
optimize: optimizeOpts,
|
|
451
|
+
ghTarget: target,
|
|
452
|
+
key: keyArg,
|
|
453
|
+
prefix: resolvedPrefix ?? defaults.prefix,
|
|
454
|
+
repo: optString(args, "repo") ?? defaults.repo,
|
|
455
|
+
ref: refArg ?? defaults.ref,
|
|
456
|
+
contentType,
|
|
457
|
+
deriveRepoFromGit: !noGit,
|
|
458
|
+
dryRun,
|
|
459
|
+
metadata,
|
|
460
|
+
provenanceClient: "uploads-mcp",
|
|
461
|
+
alt: () => alt ?? sourceName,
|
|
462
|
+
width,
|
|
463
|
+
});
|
|
464
|
+
const optimize = {
|
|
465
|
+
optimized: prepared.optimized,
|
|
466
|
+
skippedReason: prepared.skippedReason,
|
|
467
|
+
originalBytes: prepared.originalBytes,
|
|
468
|
+
outputBytes: prepared.outputBytes,
|
|
469
|
+
filename: prepared.filename,
|
|
470
|
+
};
|
|
471
|
+
if (wantComment && target) {
|
|
472
|
+
const { comment, commentError } = await syncComment(client, target);
|
|
473
|
+
return { ...result, markdown, optimize, frame: prepared.frame, comment, commentError };
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
...result,
|
|
477
|
+
markdown,
|
|
478
|
+
optimize,
|
|
479
|
+
frame: prepared.frame,
|
|
480
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
const { uploads, failures, firstError } = await uploadPuts({
|
|
484
|
+
...putShared,
|
|
485
|
+
files: [file],
|
|
486
|
+
nameOverride: filenameArg,
|
|
487
|
+
explicitKey: keyArg,
|
|
432
488
|
});
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
489
|
+
if (uploads.length === 0 && failures.length > 0) {
|
|
490
|
+
throw firstError instanceof Error ? firstError : new Error(String(firstError));
|
|
491
|
+
}
|
|
492
|
+
const u = uploads[0];
|
|
493
|
+
const flat = {
|
|
494
|
+
workspace: u.workspace,
|
|
495
|
+
key: u.key,
|
|
496
|
+
url: u.url,
|
|
497
|
+
embedUrl: u.embedUrl,
|
|
498
|
+
size: u.size,
|
|
499
|
+
contentType: u.contentType,
|
|
500
|
+
replaced: u.replaced,
|
|
501
|
+
markdown: u.markdown,
|
|
502
|
+
optimize: u.optimize,
|
|
503
|
+
frame: u.frame,
|
|
504
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
443
505
|
};
|
|
444
506
|
if (wantComment && target) {
|
|
445
507
|
const { comment, commentError } = await syncComment(client, target);
|
|
446
|
-
return { ...
|
|
508
|
+
return { ...flat, comment, commentError };
|
|
447
509
|
}
|
|
448
|
-
return
|
|
510
|
+
return flat;
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "screenshot",
|
|
515
|
+
description: "Capture a URL or a local .html file and host it — a hosted, PR-embeddable image in one call. Backend `local` drives an already-installed Chrome/Chromium (dynamically loaded; unavailable in some runtimes); `remote` renders server-side via the workspace's render endpoint and counts against the monthly upload budget. Default via=auto prefers local when found, else remote. localhost/private-network URLs and .html files are local-only — via=remote (or auto falling back to remote) fails fast instead of a doomed request. Shares the put upload pipeline: optional frame, optimize-by-default, pr/issue attachment + comment, gallery, metadata. Uploads are public.",
|
|
516
|
+
inputSchema: {
|
|
517
|
+
type: "object",
|
|
518
|
+
properties: {
|
|
519
|
+
target: {
|
|
520
|
+
type: "string",
|
|
521
|
+
description: "http(s) URL, or a path to a local .html file.",
|
|
522
|
+
},
|
|
523
|
+
via: {
|
|
524
|
+
type: "string",
|
|
525
|
+
description: "Capture backend: auto (default) | local | remote.",
|
|
526
|
+
},
|
|
527
|
+
browser: {
|
|
528
|
+
type: "string",
|
|
529
|
+
description: "Explicit local browser executable path (local backend only).",
|
|
530
|
+
},
|
|
531
|
+
cdp: {
|
|
532
|
+
type: "string",
|
|
533
|
+
description: "Attach to a running Chrome via CDP instead of launching one (local backend only).",
|
|
534
|
+
},
|
|
535
|
+
viewport: {
|
|
536
|
+
type: "string",
|
|
537
|
+
description: "WIDTHxHEIGHT[@SCALEx], e.g. 1280x800@2x (default: 1280x800@2).",
|
|
538
|
+
},
|
|
539
|
+
selector: { type: "string", description: "Capture one element instead of the viewport." },
|
|
540
|
+
fullPage: { type: "boolean", description: "Capture the full scrollable page." },
|
|
541
|
+
colorScheme: {
|
|
542
|
+
type: "string",
|
|
543
|
+
description: "Emulate prefers-color-scheme: dark | light. Full media-query emulation requires via: \"local\" — the remote backend only sets the CSS color-scheme property and won't flip a page's own prefers-color-scheme queries.",
|
|
544
|
+
},
|
|
545
|
+
wait: {
|
|
546
|
+
type: "string",
|
|
547
|
+
description: 'Settle strategy: load (default) | domcontentloaded | networkidle | a millisecond count (millisecond counts are local-only — via: "local").',
|
|
548
|
+
},
|
|
549
|
+
key: {
|
|
550
|
+
type: "string",
|
|
551
|
+
description: "Explicit object key (default: <prefix>/<repo>/<ref>/<name>-<hash>.png). Cannot be combined with pr/issue.",
|
|
552
|
+
},
|
|
553
|
+
destination: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "Typed destination root: screenshots | gh | f. With pr/issue must be gh or omitted.",
|
|
556
|
+
},
|
|
557
|
+
prefix: {
|
|
558
|
+
type: "string",
|
|
559
|
+
description: "Key prefix (default: screenshots, or UPLOADS_DEFAULT_PREFIX).",
|
|
560
|
+
},
|
|
561
|
+
...ghTargetProps("Attach to"),
|
|
562
|
+
repo: {
|
|
563
|
+
type: "string",
|
|
564
|
+
description: "owner/name repo segment (default: git remote, or UPLOADS_DEFAULT_REPO).",
|
|
565
|
+
},
|
|
566
|
+
ref: {
|
|
567
|
+
type: "string",
|
|
568
|
+
description: "PR/issue/branch key segment (default: today, or UPLOADS_DEFAULT_REF).",
|
|
569
|
+
},
|
|
570
|
+
alt: {
|
|
571
|
+
type: "string",
|
|
572
|
+
description: "Alt text for the markdown (default: derived filename).",
|
|
573
|
+
},
|
|
574
|
+
width: {
|
|
575
|
+
type: "number",
|
|
576
|
+
description: "Emit <img width=…> markdown instead of a plain embed.",
|
|
577
|
+
},
|
|
578
|
+
noOptimize: {
|
|
579
|
+
type: "boolean",
|
|
580
|
+
description: "Skip client-side image optimization (default: optimize to WebP).",
|
|
581
|
+
},
|
|
582
|
+
optimizeMaxEdge: {
|
|
583
|
+
type: "number",
|
|
584
|
+
description: "Max long edge in pixels when optimizing.",
|
|
585
|
+
},
|
|
586
|
+
optimizeQuality: { type: "number", description: "WebP quality 1-100 when optimizing." },
|
|
587
|
+
keepExif: { type: "boolean", description: "Keep EXIF/XMP/ICC when optimizing." },
|
|
588
|
+
...frameProps,
|
|
589
|
+
noGit: { type: "boolean", description: "Don't derive the repo segment from git." },
|
|
590
|
+
comment: {
|
|
591
|
+
type: "boolean",
|
|
592
|
+
description: "With pr/issue: create/update the managed attachments comment (best-effort).",
|
|
593
|
+
},
|
|
594
|
+
galleryId: {
|
|
595
|
+
type: "string",
|
|
596
|
+
description: "Add the uploaded object to this public gallery.",
|
|
597
|
+
},
|
|
598
|
+
dryRun: {
|
|
599
|
+
type: "boolean",
|
|
600
|
+
description: "Capture + resolve key/URL without uploading. Not with comment or galleryId.",
|
|
601
|
+
},
|
|
602
|
+
metadata: metadataProp,
|
|
603
|
+
workspace: workspaceProp,
|
|
604
|
+
},
|
|
605
|
+
required: ["target"],
|
|
606
|
+
additionalProperties: false,
|
|
607
|
+
},
|
|
608
|
+
async handler(args) {
|
|
609
|
+
const targetArg = optString(args, "target");
|
|
610
|
+
if (!targetArg)
|
|
611
|
+
usage("target is required");
|
|
612
|
+
const viaArg = optString(args, "via") ?? "auto";
|
|
613
|
+
if (viaArg !== "auto" && viaArg !== "local" && viaArg !== "remote") {
|
|
614
|
+
usage("via must be auto, local, or remote");
|
|
615
|
+
}
|
|
616
|
+
const colorSchemeArg = optString(args, "colorScheme");
|
|
617
|
+
if (colorSchemeArg && colorSchemeArg !== "dark" && colorSchemeArg !== "light") {
|
|
618
|
+
usage("colorScheme must be dark or light");
|
|
619
|
+
}
|
|
620
|
+
const target = ghTargetFromArgs(args, run);
|
|
621
|
+
const wantComment = optBool(args, "comment");
|
|
622
|
+
const dryRun = optBool(args, "dryRun");
|
|
623
|
+
const keyArg = optString(args, "key");
|
|
624
|
+
const destArg = optString(args, "destination");
|
|
625
|
+
const prefixArg = optString(args, "prefix");
|
|
626
|
+
const refArg = optString(args, "ref");
|
|
627
|
+
const galleryIdArg = optString(args, "galleryId");
|
|
628
|
+
if (wantComment && !target)
|
|
629
|
+
usage("comment requires pr or issue");
|
|
630
|
+
if (dryRun && wantComment)
|
|
631
|
+
usage("dryRun cannot be combined with comment");
|
|
632
|
+
if (dryRun && galleryIdArg)
|
|
633
|
+
usage("dryRun cannot be combined with galleryId");
|
|
634
|
+
if (target) {
|
|
635
|
+
if (keyArg)
|
|
636
|
+
usage("key cannot be combined with pr/issue");
|
|
637
|
+
if (refArg)
|
|
638
|
+
usage("ref cannot be combined with pr/issue");
|
|
639
|
+
if (prefixArg)
|
|
640
|
+
usage("prefix cannot be combined with pr/issue");
|
|
641
|
+
}
|
|
642
|
+
const metadata = optStringRecord(args, "metadata");
|
|
643
|
+
if (metadata)
|
|
644
|
+
validateMetaMap(metadata);
|
|
645
|
+
let resolvedPrefix;
|
|
646
|
+
try {
|
|
647
|
+
resolvedPrefix = resolvePutPrefix({
|
|
648
|
+
destination: destArg,
|
|
649
|
+
prefix: prefixArg,
|
|
650
|
+
key: keyArg,
|
|
651
|
+
ghAttachment: Boolean(target),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
usage(err instanceof Error ? err.message : String(err));
|
|
656
|
+
}
|
|
657
|
+
const { config, client } = clientFor(args);
|
|
658
|
+
const defaults = resolvePutDefaults({ envFile: globals.envFile });
|
|
659
|
+
const frameOpts = mcpFrameOptions(args);
|
|
660
|
+
const optimizeOpts = mcpOptimizeOptions(args, defaults);
|
|
661
|
+
const noGit = optBool(args, "noGit") || defaults.noGit === true;
|
|
662
|
+
const alt = optString(args, "alt");
|
|
663
|
+
const width = optPosInt(args, "width") ?? defaults.width;
|
|
664
|
+
// Dynamic import only: keeps mcp/tools.ts (and therefore anything
|
|
665
|
+
// that statically imports it) free of a static reference to the
|
|
666
|
+
// local-backend chain. If this fails, the runtime can't do Node-side
|
|
667
|
+
// capture at all — point the caller at the remote backend instead.
|
|
668
|
+
let screenshotModule;
|
|
669
|
+
try {
|
|
670
|
+
screenshotModule = await import("../screenshot.js");
|
|
671
|
+
}
|
|
672
|
+
catch (err) {
|
|
673
|
+
usage(`screenshot capture is unavailable in this runtime; try via: "remote" instead (${err instanceof Error ? err.message : String(err)})`);
|
|
674
|
+
}
|
|
675
|
+
let captured;
|
|
676
|
+
try {
|
|
677
|
+
captured = await screenshotModule.captureScreenshot({
|
|
678
|
+
target: targetArg,
|
|
679
|
+
via: viaArg,
|
|
680
|
+
browserPath: optString(args, "browser"),
|
|
681
|
+
cdp: optString(args, "cdp"),
|
|
682
|
+
viewport: screenshotModule.parseViewport(optString(args, "viewport")),
|
|
683
|
+
selector: optString(args, "selector"),
|
|
684
|
+
fullPage: optBool(args, "fullPage"),
|
|
685
|
+
colorScheme: colorSchemeArg,
|
|
686
|
+
waitUntil: screenshotModule.parseWaitUntil(optString(args, "wait")),
|
|
687
|
+
apiUrl: config.apiUrl,
|
|
688
|
+
token: config.token,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
if (err instanceof Error &&
|
|
693
|
+
"code" in err &&
|
|
694
|
+
err.code === "BROWSER_NOT_FOUND" &&
|
|
695
|
+
viaArg === "local") {
|
|
696
|
+
usage(`${err.message} — try via: "remote" instead`);
|
|
697
|
+
}
|
|
698
|
+
throw err;
|
|
699
|
+
}
|
|
700
|
+
const { result, prepared, markdown } = await uploadPreparedImage(client, captured.png, captured.filename, {
|
|
701
|
+
frame: frameOpts,
|
|
702
|
+
optimize: optimizeOpts,
|
|
703
|
+
ghTarget: target,
|
|
704
|
+
key: keyArg,
|
|
705
|
+
prefix: resolvedPrefix ?? defaults.prefix,
|
|
706
|
+
repo: optString(args, "repo") ?? defaults.repo,
|
|
707
|
+
ref: refArg ?? defaults.ref,
|
|
708
|
+
deriveRepoFromGit: !noGit,
|
|
709
|
+
dryRun,
|
|
710
|
+
metadata,
|
|
711
|
+
provenanceClient: "uploads-mcp-screenshot",
|
|
712
|
+
alt: (p) => alt ?? p.filename,
|
|
713
|
+
width,
|
|
714
|
+
});
|
|
715
|
+
let gallery;
|
|
716
|
+
if (galleryIdArg) {
|
|
717
|
+
try {
|
|
718
|
+
const current = await client.getGallery(galleryIdArg);
|
|
719
|
+
await client.addGalleryItem(galleryIdArg, result.key, {
|
|
720
|
+
expectedVersion: current.version,
|
|
721
|
+
altText: alt ?? prepared.filename,
|
|
722
|
+
});
|
|
723
|
+
gallery = { id: galleryIdArg, url: current.url };
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
gallery = {
|
|
727
|
+
id: galleryIdArg,
|
|
728
|
+
error: err instanceof Error ? err.message : String(err),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const flat = {
|
|
449
733
|
...result,
|
|
450
734
|
markdown,
|
|
451
|
-
|
|
735
|
+
backend: captured.backend,
|
|
736
|
+
optimize: {
|
|
737
|
+
optimized: prepared.optimized,
|
|
738
|
+
skippedReason: prepared.skippedReason,
|
|
739
|
+
originalBytes: prepared.originalBytes,
|
|
740
|
+
outputBytes: prepared.outputBytes,
|
|
741
|
+
filename: prepared.filename,
|
|
742
|
+
},
|
|
452
743
|
frame: prepared.frame,
|
|
744
|
+
gallery,
|
|
453
745
|
...(dryRun ? { dryRun: true } : {}),
|
|
454
746
|
};
|
|
747
|
+
if (wantComment && target) {
|
|
748
|
+
const { comment, commentError } = await syncComment(client, target);
|
|
749
|
+
return { ...flat, comment, commentError };
|
|
750
|
+
}
|
|
751
|
+
return flat;
|
|
455
752
|
},
|
|
456
753
|
},
|
|
457
754
|
{
|
|
458
755
|
name: "attach",
|
|
459
|
-
description: "Upload one or more files as stable PR/issue attachments and maintain a
|
|
756
|
+
description: "Upload one or more files as stable PR/issue attachments (in parallel) and maintain a managed GitHub comment. Returns `uploads` and `failures` (one bad file does not abort the batch). Each success has `url`, `embedUrl`, and `markdown` (prefer embedUrl for GitHub). With no pr/issue, targets the current branch PR. Attachments are public and keys are predictable; upload only non-sensitive media.",
|
|
460
757
|
inputSchema: {
|
|
461
758
|
type: "object",
|
|
462
759
|
properties: {
|
|
@@ -523,45 +820,28 @@ export function createUploadsMcpTools(opts) {
|
|
|
523
820
|
const metadata = { ...metaExtras, ...ghMetadataFromTarget(target) };
|
|
524
821
|
if (Object.keys(metadata).length > 0)
|
|
525
822
|
validateMetaMap(metadata);
|
|
526
|
-
const uploads =
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
keepExif: optimizeOpts.keepExif === true,
|
|
543
|
-
}),
|
|
544
|
-
metadata,
|
|
545
|
-
});
|
|
546
|
-
uploads.push({
|
|
547
|
-
...result,
|
|
548
|
-
markdown: buildMarkdown(urlForGithubEmbed(result.url, result.embedUrl), {
|
|
549
|
-
alt: sourceName,
|
|
550
|
-
}),
|
|
551
|
-
frame: prepared.frame,
|
|
552
|
-
optimize: {
|
|
553
|
-
optimized: prepared.optimized,
|
|
554
|
-
skippedReason: prepared.skippedReason,
|
|
555
|
-
originalBytes: prepared.originalBytes,
|
|
556
|
-
outputBytes: prepared.outputBytes,
|
|
557
|
-
filename: prepared.filename,
|
|
558
|
-
},
|
|
823
|
+
const { uploads, failures } = await uploadAttachments({
|
|
824
|
+
client,
|
|
825
|
+
target,
|
|
826
|
+
files,
|
|
827
|
+
contentType,
|
|
828
|
+
optimize: optimizeOpts,
|
|
829
|
+
frame: frameOpts,
|
|
830
|
+
metadata,
|
|
831
|
+
provenanceClient: "uploads-mcp",
|
|
832
|
+
});
|
|
833
|
+
// Total failure → isError with full failures[] for agents.
|
|
834
|
+
if (uploads.length === 0 && failures.length > 0) {
|
|
835
|
+
throw new ToolBatchError(batchFailureMessage(failures), {
|
|
836
|
+
target,
|
|
837
|
+
uploads,
|
|
838
|
+
failures,
|
|
559
839
|
});
|
|
560
840
|
}
|
|
561
841
|
if (optBool(args, "noComment"))
|
|
562
|
-
return { target, uploads };
|
|
842
|
+
return { target, uploads, failures };
|
|
563
843
|
const { comment, commentError } = await syncComment(client, target);
|
|
564
|
-
return { target, uploads, comment, commentError };
|
|
844
|
+
return { target, uploads, failures, comment, commentError };
|
|
565
845
|
},
|
|
566
846
|
},
|
|
567
847
|
{
|
|
@@ -628,9 +908,28 @@ export function createUploadsMcpTools(opts) {
|
|
|
628
908
|
return client.delete(key);
|
|
629
909
|
},
|
|
630
910
|
},
|
|
911
|
+
{
|
|
912
|
+
name: "get_metadata",
|
|
913
|
+
description: "Read an object's queryable custom metadata (D1 key-value pairs, not R2 provenance). Returns `{ metadata }` (empty when none). Object must exist. Same as `uploads meta get`.",
|
|
914
|
+
inputSchema: {
|
|
915
|
+
type: "object",
|
|
916
|
+
properties: {
|
|
917
|
+
key: { type: "string", description: "Object key to inspect." },
|
|
918
|
+
workspace: workspaceProp,
|
|
919
|
+
},
|
|
920
|
+
required: ["key"],
|
|
921
|
+
additionalProperties: false,
|
|
922
|
+
},
|
|
923
|
+
async handler(args) {
|
|
924
|
+
const key = optString(args, "key");
|
|
925
|
+
if (!key)
|
|
926
|
+
usage("key is required");
|
|
927
|
+
return clientFor(args).client.getMetadata(key);
|
|
928
|
+
},
|
|
929
|
+
},
|
|
631
930
|
{
|
|
632
931
|
name: "set_metadata",
|
|
633
|
-
description: "Merge-set and/or delete an object's queryable custom metadata (D1
|
|
932
|
+
description: "Merge-set and/or delete an object's queryable custom metadata (D1 key-value pairs, not R2 provenance). `set` wins over `delete` for the same key. " +
|
|
634
933
|
METADATA_DESCRIPTION +
|
|
635
934
|
" Requires at least one of `set` or `delete`. Same as `uploads meta set`.",
|
|
636
935
|
inputSchema: {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type BrowserCandidateSource = "env" | "system" | "playwright-cache" | "puppeteer-cache";
|
|
2
|
+
export interface BrowserCandidate {
|
|
3
|
+
source: BrowserCandidateSource;
|
|
4
|
+
/** e.g. "chrome", "chromium", "chromium_headless_shell", "edge". */
|
|
5
|
+
kind: string;
|
|
6
|
+
executablePath: string;
|
|
7
|
+
/** Cache revision/build string, when applicable. */
|
|
8
|
+
revision?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DetectRoots {
|
|
11
|
+
platform?: NodeJS.Platform;
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
playwrightCacheDir?: string;
|
|
14
|
+
puppeteerCacheDir?: string;
|
|
15
|
+
/** Injectable for tests: overrides the fixed system-install paths. */
|
|
16
|
+
systemCandidates?: readonly {
|
|
17
|
+
kind: string;
|
|
18
|
+
path: string;
|
|
19
|
+
}[];
|
|
20
|
+
/** Injectable for tests: replaces fs.existsSync. */
|
|
21
|
+
exists?: (path: string) => boolean;
|
|
22
|
+
/** Injectable for tests: replaces fs.readdirSync. */
|
|
23
|
+
readdir?: (path: string) => string[];
|
|
24
|
+
}
|
|
25
|
+
export interface DetectResult {
|
|
26
|
+
/** Explicit --browser / UPLOADS_CHROME_PATH / CHROME_PATH override, if any. */
|
|
27
|
+
envOverride?: string;
|
|
28
|
+
candidates: BrowserCandidate[];
|
|
29
|
+
/** Best candidate by the documented ranking, or undefined if none found. */
|
|
30
|
+
winner?: BrowserCandidate;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Scan for a usable local Chromium-family executable. Pure fs/env — never
|
|
34
|
+
* launches a browser. Roots are injectable so tests can fake a cache layout.
|
|
35
|
+
*/
|
|
36
|
+
export declare function detectLocalBrowser(roots?: DetectRoots): DetectResult;
|
|
37
|
+
export interface LocalCaptureOptions {
|
|
38
|
+
/** URL to navigate to, or a `file://` URL for local .html targets. */
|
|
39
|
+
url: string;
|
|
40
|
+
/** Explicit browser executable (--browser / UPLOADS_CHROME_PATH / CHROME_PATH). */
|
|
41
|
+
browserPath?: string;
|
|
42
|
+
/** Attach to a running Chrome instead of launching one. */
|
|
43
|
+
cdp?: string;
|
|
44
|
+
viewport: {
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
deviceScaleFactor: number;
|
|
48
|
+
};
|
|
49
|
+
selector?: string;
|
|
50
|
+
fullPage?: boolean;
|
|
51
|
+
colorScheme?: "dark" | "light";
|
|
52
|
+
/** "load" | "domcontentloaded" | "networkidle", or a millisecond settle delay. */
|
|
53
|
+
waitUntil: "load" | "domcontentloaded" | "networkidle" | number;
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
detectRoots?: DetectRoots;
|
|
56
|
+
/**
|
|
57
|
+
* Pre-computed detection result from a caller that already scanned the
|
|
58
|
+
* filesystem (e.g. `auto`-routing's probe) — avoids re-running
|
|
59
|
+
* `detectLocalBrowser` a second time for the same capture.
|
|
60
|
+
*/
|
|
61
|
+
detectResult?: DetectResult;
|
|
62
|
+
}
|
|
63
|
+
/** Capture a PNG screenshot using a local (already-installed) browser. */
|
|
64
|
+
export declare function captureLocal(opts: LocalCaptureOptions): Promise<Uint8Array>;
|