@buildinternet/uploads 0.41.1 → 0.42.1
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-args.d.ts +3 -1
- package/dist/cli-args.js +6 -4
- package/dist/cli-catalog.js +1 -0
- package/dist/commands/screenshot.js +28 -5
- package/dist/mcp/args.d.ts +3 -1
- package/dist/mcp/args.js +4 -3
- package/dist/mcp/output-schemas.d.ts +37 -0
- package/dist/mcp/output-schemas.js +338 -0
- package/dist/mcp/server.d.ts +52 -0
- package/dist/mcp/server.js +65 -0
- package/dist/mcp/tools.js +88 -3
- package/dist/screenshot-local.d.ts +18 -0
- package/dist/screenshot-local.js +36 -4
- package/dist/screenshot-remote.d.ts +19 -2
- package/dist/screenshot-remote.js +2 -2
- package/dist/screenshot.d.ts +37 -0
- package/dist/screenshot.js +33 -3
- package/package.json +3 -3
package/dist/cli-args.d.ts
CHANGED
|
@@ -79,4 +79,6 @@ export declare function flagBool(flags: CommandFlags["flags"], name: string): bo
|
|
|
79
79
|
export declare function flagValues(flags: CommandFlags["flags"], name: string): string[];
|
|
80
80
|
/** Command-level workspace override (`--workspace` / `-w`). */
|
|
81
81
|
export declare function commandWorkspace(flags: CommandFlags["flags"]): string | undefined;
|
|
82
|
-
export declare function flagInt(flags: CommandFlags["flags"], name: string, label: string
|
|
82
|
+
export declare function flagInt(flags: CommandFlags["flags"], name: string, label: string, options?: {
|
|
83
|
+
allowZero?: boolean;
|
|
84
|
+
}): number | undefined;
|
package/dist/cli-args.js
CHANGED
|
@@ -198,16 +198,18 @@ export function flagValues(flags, name) {
|
|
|
198
198
|
export function commandWorkspace(flags) {
|
|
199
199
|
return flagString(flags, "--workspace") ?? flagString(flags, "-w");
|
|
200
200
|
}
|
|
201
|
-
export function flagInt(flags, name, label) {
|
|
201
|
+
export function flagInt(flags, name, label, options) {
|
|
202
|
+
const allowZero = options?.allowZero ?? false;
|
|
203
|
+
const kind = allowZero ? "non-negative" : "positive";
|
|
202
204
|
const raw = flagString(flags, name);
|
|
203
205
|
if (raw === undefined)
|
|
204
206
|
return undefined;
|
|
205
207
|
if (!/^\d+$/.test(raw)) {
|
|
206
|
-
throw new UsageError(`invalid ${label}: must be a
|
|
208
|
+
throw new UsageError(`invalid ${label}: must be a ${kind} integer (got ${raw})`);
|
|
207
209
|
}
|
|
208
210
|
const n = Number.parseInt(raw, 10);
|
|
209
|
-
if (!Number.isFinite(n) || n <= 0) {
|
|
210
|
-
throw new UsageError(`invalid ${label}: must be a
|
|
211
|
+
if (!Number.isFinite(n) || (allowZero ? n < 0 : n <= 0)) {
|
|
212
|
+
throw new UsageError(`invalid ${label}: must be a ${kind} integer (got ${raw})`);
|
|
211
213
|
}
|
|
212
214
|
return n;
|
|
213
215
|
}
|
package/dist/cli-catalog.js
CHANGED
|
@@ -13,7 +13,7 @@ import { parseMetaFlags, validateMetaMap } from "../metadata.js";
|
|
|
13
13
|
import { mergeDerivedMeta } from "../metadata-vocab.js";
|
|
14
14
|
import { writeSidecarMeta } from "../sidecar.js";
|
|
15
15
|
import { readStdin, writeJson, writeStdout } from "../io.js";
|
|
16
|
-
import { assertHideSelector, captureScreenshot, parseViewport, parseWaitUntil, } from "../screenshot.js";
|
|
16
|
+
import { assertHideSelector, captureScreenshot, clipHintText, DEFAULT_FULL_PAGE_MAX_HEIGHT, parseViewport, parseWaitUntil, } from "../screenshot.js";
|
|
17
17
|
const SCREENSHOT_HELP = `uploads screenshot <target> [options]
|
|
18
18
|
|
|
19
19
|
Capture a URL or a local .html file and host it — a hosted, PR-embeddable
|
|
@@ -59,6 +59,10 @@ Options:
|
|
|
59
59
|
--viewport <WxH[@Sx]> Size + device scale factor (default: 1280x800@2)
|
|
60
60
|
--selector <css> Capture one element instead of the viewport
|
|
61
61
|
--full-page Capture the full scrollable page
|
|
62
|
+
--max-height <px> Cap on full-page capture height in CSS px (default: ${DEFAULT_FULL_PAGE_MAX_HEIGHT},
|
|
63
|
+
or 0 for uncapped). A page over the cap is clipped, with a note printed
|
|
64
|
+
to stderr and a --format json \`hint\`. Requires --full-page. Applied on
|
|
65
|
+
both --via local and --via remote so behavior matches.
|
|
62
66
|
--dark / --light Emulate prefers-color-scheme (full media-query emulation on --via local
|
|
63
67
|
only; --via remote just sets the CSS color-scheme property, so a page's
|
|
64
68
|
own prefers-color-scheme queries won't flip)
|
|
@@ -182,6 +186,14 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
182
186
|
const viewport = parseViewport(flagString(parsed.flags, "--viewport"));
|
|
183
187
|
const selector = flagString(parsed.flags, "--selector");
|
|
184
188
|
const fullPage = flagBool(parsed.flags, "--full-page");
|
|
189
|
+
// Unlike every other --…-px flag, 0 is a valid (uncapped) value here, not
|
|
190
|
+
// an error — flagInt's allowZero option covers it.
|
|
191
|
+
const maxHeightFlag = flagInt(parsed.flags, "--max-height", "--max-height", {
|
|
192
|
+
allowZero: true,
|
|
193
|
+
});
|
|
194
|
+
if (maxHeightFlag !== undefined && !fullPage) {
|
|
195
|
+
throw new UsageError("--max-height requires --full-page");
|
|
196
|
+
}
|
|
185
197
|
const colorScheme = colorSchemeFromFlags(parsed.flags);
|
|
186
198
|
const waitUntil = parseWaitUntil(flagString(parsed.flags, "--wait"));
|
|
187
199
|
const hide = flagValues(parsed.flags, "--hide");
|
|
@@ -387,6 +399,7 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
387
399
|
viewport,
|
|
388
400
|
selector,
|
|
389
401
|
fullPage,
|
|
402
|
+
maxHeight: maxHeightFlag,
|
|
390
403
|
colorScheme,
|
|
391
404
|
waitUntil,
|
|
392
405
|
hide,
|
|
@@ -403,6 +416,14 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
403
416
|
});
|
|
404
417
|
if (logHuman)
|
|
405
418
|
process.stderr.write(`>> captured via ${captured.backend} backend\n`);
|
|
419
|
+
// Full-page height cap note (issue #652): printed regardless of --format
|
|
420
|
+
// (only --quiet suppresses it) since it's directly actionable info about
|
|
421
|
+
// the image that was just captured, same as the upload-tail warnings below.
|
|
422
|
+
const clipHint = captured.capped?.clipped
|
|
423
|
+
? clipHintText(captured.capped.maxHeightPx, "--max-height")
|
|
424
|
+
: undefined;
|
|
425
|
+
if (clipHint && !ctx.quiet)
|
|
426
|
+
process.stderr.write(`${clipHint}\n`);
|
|
406
427
|
// Resolve selectors + render annotations before the frame/optimize/upload
|
|
407
428
|
// pipeline runs — everything downstream (the --out write, the sidecar
|
|
408
429
|
// hash, and the upload itself) should see the annotated bytes.
|
|
@@ -543,9 +564,11 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
543
564
|
process.stderr.write(`${bindingWarning}\n`);
|
|
544
565
|
process.stderr.write("\n");
|
|
545
566
|
}
|
|
546
|
-
// One JSON `hint` slot (mirrors bare put): the
|
|
547
|
-
//
|
|
548
|
-
//
|
|
567
|
+
// One JSON `hint` slot (mirrors bare put): the clip note (issue #652) wins
|
|
568
|
+
// first — it's about the just-captured image itself, more immediately
|
|
569
|
+
// actionable than the other three, which are about upload/staging
|
|
570
|
+
// mechanics. Then the binding warning, more actionable than the generic
|
|
571
|
+
// staging note; a replaced-object note (issue #618) is lowest priority —
|
|
549
572
|
// it only surfaces when nothing else already claimed the slot. Since state
|
|
550
573
|
// folds into the derived key, replaced + state means a same-side re-capture,
|
|
551
574
|
// which is the intended replace-in-place flow — word it as informational,
|
|
@@ -553,7 +576,7 @@ loadAnnotateModule = () => import("../annotate/index.js")) {
|
|
|
553
576
|
const replacedHint = result.replaced && explicitMeta.state
|
|
554
577
|
? `re-capture replaced the previous state=${explicitMeta.state} object at ${result.key} — expected for repeat captures of the same URL + state`
|
|
555
578
|
: undefined;
|
|
556
|
-
const jsonHint = bindingWarning ?? stagingNote ?? replacedHint;
|
|
579
|
+
const jsonHint = clipHint ?? bindingWarning ?? stagingNote ?? replacedHint;
|
|
557
580
|
switch (format) {
|
|
558
581
|
case "json":
|
|
559
582
|
await writeJson({
|
package/dist/mcp/args.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ export declare function usage(msg: string): never;
|
|
|
3
3
|
export declare function optString(args: ToolArgs, name: string): string | undefined;
|
|
4
4
|
/** A boolean flag argument; missing/null reads as `false`. */
|
|
5
5
|
export declare function optBool(args: ToolArgs, name: string): boolean;
|
|
6
|
-
export declare function optPosInt(args: ToolArgs, name: string
|
|
6
|
+
export declare function optPosInt(args: ToolArgs, name: string, options?: {
|
|
7
|
+
allowZero?: boolean;
|
|
8
|
+
}): number | undefined;
|
|
7
9
|
/** A JSON-object argument of string→string pairs (e.g. a `metadata` or `filters` param). */
|
|
8
10
|
export declare function optStringRecord(args: ToolArgs, name: string): Record<string, string> | undefined;
|
|
9
11
|
/** A JSON-array argument of strings (e.g. a `delete` or `files` param). */
|
package/dist/mcp/args.js
CHANGED
|
@@ -25,12 +25,13 @@ export function optBool(args, name) {
|
|
|
25
25
|
usage(`${name} must be a boolean`);
|
|
26
26
|
return v;
|
|
27
27
|
}
|
|
28
|
-
export function optPosInt(args, name) {
|
|
28
|
+
export function optPosInt(args, name, options) {
|
|
29
|
+
const allowZero = options?.allowZero ?? false;
|
|
29
30
|
const v = args[name];
|
|
30
31
|
if (v === undefined || v === null)
|
|
31
32
|
return undefined;
|
|
32
|
-
if (typeof v !== "number" || !Number.isInteger(v) || v <= 0) {
|
|
33
|
-
usage(`${name} must be a positive integer`);
|
|
33
|
+
if (typeof v !== "number" || !Number.isInteger(v) || (allowZero ? v < 0 : v <= 0)) {
|
|
34
|
+
usage(`${name} must be a ${allowZero ? "non-negative" : "positive"} integer`);
|
|
34
35
|
}
|
|
35
36
|
return v;
|
|
36
37
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema for MCP tool `structuredContent`. The SDK validates successful
|
|
3
|
+
* results against the advertised `outputSchema`, so every property a handler
|
|
4
|
+
* actually returns must be listed. Roots are always `type: object` — a
|
|
5
|
+
* oneOf/anyOf root is treated as non-object by the 2025-era codec and wraps
|
|
6
|
+
* the result as `{ result: … }`.
|
|
7
|
+
*/
|
|
8
|
+
export type JsonSchema = Record<string, unknown>;
|
|
9
|
+
/** Hosted + stdio `comment` / `put.comment` — all PostCommentResult variants. */
|
|
10
|
+
export declare const commentResultSchema: JsonSchema;
|
|
11
|
+
/** Single-file put (flat) or multi-file `{ uploads, failures }`, plus optional comment/promote extras. */
|
|
12
|
+
export declare const putResultSchema: JsonSchema;
|
|
13
|
+
export declare const listResultSchema: JsonSchema;
|
|
14
|
+
export declare const deleteResultSchema: JsonSchema;
|
|
15
|
+
export declare const metadataResultSchema: JsonSchema;
|
|
16
|
+
export declare const findFilesResultSchema: JsonSchema;
|
|
17
|
+
/** `meta keys` or `meta values <key>`. */
|
|
18
|
+
export declare const metadataFacetsResultSchema: JsonSchema;
|
|
19
|
+
export declare const repoLinkStatusResultSchema: JsonSchema;
|
|
20
|
+
export declare const usageResultSchema: JsonSchema;
|
|
21
|
+
export declare const reconcileResultSchema: JsonSchema;
|
|
22
|
+
export declare const purgeExpiredResultSchema: JsonSchema;
|
|
23
|
+
export declare const healthResultSchema: JsonSchema;
|
|
24
|
+
export declare const promoteToolResultSchema: JsonSchema;
|
|
25
|
+
export declare const galleryResultSchema: JsonSchema;
|
|
26
|
+
export declare const galleryFindResultSchema: JsonSchema;
|
|
27
|
+
/** Hosted catalog — every tool must have an entry. */
|
|
28
|
+
export declare const hostedOutputSchemas: Record<string, JsonSchema>;
|
|
29
|
+
/** Shared-shape stdio tools. Hosted-only tools (`promote`, `repo_link_status`) omitted. */
|
|
30
|
+
export declare const stdioOutputSchemas: Record<string, JsonSchema>;
|
|
31
|
+
export declare function withOutputSchemas<T extends {
|
|
32
|
+
name: string;
|
|
33
|
+
}>(tools: T[], schemas: Record<string, JsonSchema>, opts: {
|
|
34
|
+
required: boolean;
|
|
35
|
+
}): Array<T & {
|
|
36
|
+
outputSchema?: JsonSchema;
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
function objectSchema(properties, required = []) {
|
|
2
|
+
return {
|
|
3
|
+
type: "object",
|
|
4
|
+
properties,
|
|
5
|
+
...(required.length > 0 ? { required } : {}),
|
|
6
|
+
additionalProperties: false,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
const stringMap = {
|
|
10
|
+
type: "object",
|
|
11
|
+
additionalProperties: { type: "string" },
|
|
12
|
+
};
|
|
13
|
+
const nullableString = { type: ["string", "null"] };
|
|
14
|
+
const nullableNumber = { type: ["number", "null"] };
|
|
15
|
+
/** Hosted + stdio `comment` / `put.comment` — all PostCommentResult variants. */
|
|
16
|
+
export const commentResultSchema = objectSchema({
|
|
17
|
+
posted: { type: "boolean" },
|
|
18
|
+
reason: {
|
|
19
|
+
type: "string",
|
|
20
|
+
enum: [
|
|
21
|
+
"app_unconfigured",
|
|
22
|
+
"not_installed",
|
|
23
|
+
"not_authorized",
|
|
24
|
+
"actor_not_authorized",
|
|
25
|
+
"unavailable",
|
|
26
|
+
"forbidden",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
message: { type: "string" },
|
|
30
|
+
fixUrl: { type: "string" },
|
|
31
|
+
required: { type: "array", items: { type: "string" } },
|
|
32
|
+
action: { type: "string", enum: ["skipped", "created", "updated"] },
|
|
33
|
+
count: { type: "number" },
|
|
34
|
+
commentUrl: { type: "string" },
|
|
35
|
+
}, ["posted"]);
|
|
36
|
+
const promoteResultSchema = objectSchema({
|
|
37
|
+
promoted: { type: "array", items: { type: "string" } },
|
|
38
|
+
skipped: {
|
|
39
|
+
type: "array",
|
|
40
|
+
items: objectSchema({ key: { type: "string" }, reason: { type: "string" } }, [
|
|
41
|
+
"key",
|
|
42
|
+
"reason",
|
|
43
|
+
]),
|
|
44
|
+
},
|
|
45
|
+
}, ["promoted", "skipped"]);
|
|
46
|
+
const putObjectFields = {
|
|
47
|
+
key: { type: "string" },
|
|
48
|
+
url: nullableString,
|
|
49
|
+
embedUrl: nullableString,
|
|
50
|
+
size: { type: "number" },
|
|
51
|
+
contentType: { type: "string" },
|
|
52
|
+
replaced: { type: "boolean" },
|
|
53
|
+
markdown: { type: "string" },
|
|
54
|
+
provenance: stringMap,
|
|
55
|
+
metadata: stringMap,
|
|
56
|
+
visibility: { type: "string" },
|
|
57
|
+
};
|
|
58
|
+
/** Hosted `PostCommentResult` or stdio `AttachmentsCommentResult` (`via`/`action`). */
|
|
59
|
+
const putCommentSchema = objectSchema({
|
|
60
|
+
posted: { type: "boolean" },
|
|
61
|
+
reason: {
|
|
62
|
+
type: "string",
|
|
63
|
+
enum: [
|
|
64
|
+
"app_unconfigured",
|
|
65
|
+
"not_installed",
|
|
66
|
+
"not_authorized",
|
|
67
|
+
"actor_not_authorized",
|
|
68
|
+
"unavailable",
|
|
69
|
+
"forbidden",
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
message: { type: "string" },
|
|
73
|
+
fixUrl: { type: "string" },
|
|
74
|
+
required: { type: "array", items: { type: "string" } },
|
|
75
|
+
action: { type: "string", enum: ["skipped", "created", "updated"] },
|
|
76
|
+
count: { type: "number" },
|
|
77
|
+
commentUrl: { type: "string" },
|
|
78
|
+
via: { type: "string", enum: ["bot", "gh"] },
|
|
79
|
+
});
|
|
80
|
+
const putExtras = {
|
|
81
|
+
comment: putCommentSchema,
|
|
82
|
+
commentError: { type: "string" },
|
|
83
|
+
promotion: promoteResultSchema,
|
|
84
|
+
promoteError: { type: "string" },
|
|
85
|
+
};
|
|
86
|
+
const putFailure = objectSchema({
|
|
87
|
+
file: { type: "string" },
|
|
88
|
+
error: objectSchema({
|
|
89
|
+
message: { type: "string" },
|
|
90
|
+
code: { type: "string" },
|
|
91
|
+
status: { type: "number" },
|
|
92
|
+
}, ["message"]),
|
|
93
|
+
}, ["file", "error"]);
|
|
94
|
+
/** Single-file put (flat) or multi-file `{ uploads, failures }`, plus optional comment/promote extras. */
|
|
95
|
+
export const putResultSchema = objectSchema({
|
|
96
|
+
workspace: { type: "string" },
|
|
97
|
+
...putObjectFields,
|
|
98
|
+
file: { type: "string" },
|
|
99
|
+
uploads: {
|
|
100
|
+
type: "array",
|
|
101
|
+
items: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: { file: { type: "string" }, ...putObjectFields },
|
|
104
|
+
additionalProperties: true,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
failures: { type: "array", items: putFailure },
|
|
108
|
+
...putExtras,
|
|
109
|
+
// stdio put adds client-side optimize/frame provenance and dry-run flags.
|
|
110
|
+
optimize: { type: "object", additionalProperties: true },
|
|
111
|
+
frame: { type: "object", additionalProperties: true },
|
|
112
|
+
dryRun: { type: "boolean" },
|
|
113
|
+
hint: { type: "string" },
|
|
114
|
+
});
|
|
115
|
+
export const listResultSchema = objectSchema({
|
|
116
|
+
items: {
|
|
117
|
+
type: "array",
|
|
118
|
+
items: objectSchema({
|
|
119
|
+
key: { type: "string" },
|
|
120
|
+
url: nullableString,
|
|
121
|
+
embedUrl: nullableString,
|
|
122
|
+
size: { type: "number" },
|
|
123
|
+
contentType: { type: "string" },
|
|
124
|
+
uploaded: { type: "string" },
|
|
125
|
+
visibility: { type: "string" },
|
|
126
|
+
pageUrl: { type: "string" },
|
|
127
|
+
}),
|
|
128
|
+
},
|
|
129
|
+
cursor: nullableString,
|
|
130
|
+
prefixes: { type: "array", items: { type: "string" } },
|
|
131
|
+
});
|
|
132
|
+
export const deleteResultSchema = objectSchema({
|
|
133
|
+
key: { type: "string" },
|
|
134
|
+
deleted: { type: "boolean" },
|
|
135
|
+
dryRun: { type: "boolean" },
|
|
136
|
+
}, ["key"]);
|
|
137
|
+
export const metadataResultSchema = objectSchema({ metadata: stringMap }, ["metadata"]);
|
|
138
|
+
export const findFilesResultSchema = objectSchema({
|
|
139
|
+
items: {
|
|
140
|
+
type: "array",
|
|
141
|
+
items: objectSchema({
|
|
142
|
+
key: { type: "string" },
|
|
143
|
+
url: nullableString,
|
|
144
|
+
metadata: stringMap,
|
|
145
|
+
}, ["key"]),
|
|
146
|
+
},
|
|
147
|
+
cursor: nullableString,
|
|
148
|
+
truncated: { type: "boolean" },
|
|
149
|
+
}, ["items"]);
|
|
150
|
+
/** `meta keys` or `meta values <key>`. */
|
|
151
|
+
export const metadataFacetsResultSchema = objectSchema({
|
|
152
|
+
keys: {
|
|
153
|
+
type: "array",
|
|
154
|
+
items: objectSchema({
|
|
155
|
+
key: { type: "string" },
|
|
156
|
+
count: { type: "number" },
|
|
157
|
+
distinctValues: { type: "number" },
|
|
158
|
+
}, ["key", "count", "distinctValues"]),
|
|
159
|
+
},
|
|
160
|
+
truncated: { type: "boolean" },
|
|
161
|
+
key: { type: "string" },
|
|
162
|
+
values: {
|
|
163
|
+
type: "array",
|
|
164
|
+
items: objectSchema({ value: { type: "string" }, count: { type: "number" } }, [
|
|
165
|
+
"value",
|
|
166
|
+
"count",
|
|
167
|
+
]),
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
export const repoLinkStatusResultSchema = objectSchema({ binding: { type: "string", enum: ["self", "other", "none"] } }, ["binding"]);
|
|
171
|
+
export const usageResultSchema = objectSchema({
|
|
172
|
+
workspace: { type: "string" },
|
|
173
|
+
bytes: { type: "number" },
|
|
174
|
+
objects: { type: "number" },
|
|
175
|
+
uploadsInPeriod: { type: "number" },
|
|
176
|
+
periodStart: { type: "string" },
|
|
177
|
+
updatedAt: { type: "string" },
|
|
178
|
+
maxStorageBytes: { type: "number" },
|
|
179
|
+
storageRemainingBytes: { type: "number" },
|
|
180
|
+
maxUploadsPerPeriod: { type: "number" },
|
|
181
|
+
uploadsRemaining: { type: "number" },
|
|
182
|
+
});
|
|
183
|
+
export const reconcileResultSchema = objectSchema({
|
|
184
|
+
workspace: { type: "string" },
|
|
185
|
+
bytes: { type: "number" },
|
|
186
|
+
objects: { type: "number" },
|
|
187
|
+
previous: objectSchema({ bytes: { type: "number" }, objects: { type: "number" } }, [
|
|
188
|
+
"bytes",
|
|
189
|
+
"objects",
|
|
190
|
+
]),
|
|
191
|
+
changed: { type: "boolean" },
|
|
192
|
+
usage: usageResultSchema,
|
|
193
|
+
unprefixedBucket: { type: "boolean" },
|
|
194
|
+
});
|
|
195
|
+
export const purgeExpiredResultSchema = objectSchema({
|
|
196
|
+
skipped: { type: "boolean" },
|
|
197
|
+
reason: { type: "string" },
|
|
198
|
+
workspace: { type: "string" },
|
|
199
|
+
retentionDays: { type: "number" },
|
|
200
|
+
cutoff: { type: "string" },
|
|
201
|
+
deleted: { type: "number" },
|
|
202
|
+
freedBytes: { type: "number" },
|
|
203
|
+
keys: { type: "array", items: { type: "string" } },
|
|
204
|
+
keysTruncated: { type: "boolean" },
|
|
205
|
+
reconcile: reconcileResultSchema,
|
|
206
|
+
});
|
|
207
|
+
export const healthResultSchema = objectSchema({
|
|
208
|
+
ok: { type: "boolean" },
|
|
209
|
+
apiUrl: { type: "string" },
|
|
210
|
+
});
|
|
211
|
+
export const promoteToolResultSchema = objectSchema({
|
|
212
|
+
promotion: promoteResultSchema,
|
|
213
|
+
comment: commentResultSchema,
|
|
214
|
+
commentError: { type: "string" },
|
|
215
|
+
}, ["promotion"]);
|
|
216
|
+
const galleryItemSchema = objectSchema({
|
|
217
|
+
id: { type: "string" },
|
|
218
|
+
objectKey: { type: "string" },
|
|
219
|
+
filename: { type: "string" },
|
|
220
|
+
position: { type: "number" },
|
|
221
|
+
caption: nullableString,
|
|
222
|
+
altText: nullableString,
|
|
223
|
+
createdAt: { type: "string" },
|
|
224
|
+
status: { type: "string", enum: ["available", "missing", "withheld"] },
|
|
225
|
+
url: nullableString,
|
|
226
|
+
embedUrl: nullableString,
|
|
227
|
+
pageUrl: { type: "string" },
|
|
228
|
+
contentType: nullableString,
|
|
229
|
+
size: nullableNumber,
|
|
230
|
+
uploaded: nullableString,
|
|
231
|
+
modified: nullableString,
|
|
232
|
+
posterUrl: { type: "string" },
|
|
233
|
+
videoDimensions: {
|
|
234
|
+
type: "object",
|
|
235
|
+
additionalProperties: true,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const galleryReferenceSchema = objectSchema({
|
|
239
|
+
id: { type: "string" },
|
|
240
|
+
provider: { type: "string" },
|
|
241
|
+
resourceType: { type: "string" },
|
|
242
|
+
coordinate: { type: "string" },
|
|
243
|
+
canonicalUrl: nullableString,
|
|
244
|
+
createdAt: { type: "string" },
|
|
245
|
+
title: { type: "string" },
|
|
246
|
+
kind: { type: "string", enum: ["pull", "issue"] },
|
|
247
|
+
});
|
|
248
|
+
export const galleryResultSchema = objectSchema({
|
|
249
|
+
id: { type: "string" },
|
|
250
|
+
url: { type: "string" },
|
|
251
|
+
workspace: { type: "string" },
|
|
252
|
+
title: { type: "string" },
|
|
253
|
+
description: nullableString,
|
|
254
|
+
visibility: { type: "string" },
|
|
255
|
+
coverItemId: nullableString,
|
|
256
|
+
version: { type: "number" },
|
|
257
|
+
createdAt: { type: "string" },
|
|
258
|
+
updatedAt: { type: "string" },
|
|
259
|
+
items: { type: "array", items: galleryItemSchema },
|
|
260
|
+
itemCount: { type: "number" },
|
|
261
|
+
references: { type: "array", items: galleryReferenceSchema },
|
|
262
|
+
});
|
|
263
|
+
export const galleryFindResultSchema = objectSchema({
|
|
264
|
+
galleries: { type: "array", items: galleryResultSchema },
|
|
265
|
+
nextCursor: nullableString,
|
|
266
|
+
});
|
|
267
|
+
/** Hosted catalog — every tool must have an entry. */
|
|
268
|
+
export const hostedOutputSchemas = {
|
|
269
|
+
gallery_create: galleryResultSchema,
|
|
270
|
+
gallery_get: galleryResultSchema,
|
|
271
|
+
gallery_add: galleryItemSchema,
|
|
272
|
+
gallery_link: galleryReferenceSchema,
|
|
273
|
+
gallery_find_by_reference: galleryFindResultSchema,
|
|
274
|
+
put: putResultSchema,
|
|
275
|
+
list: listResultSchema,
|
|
276
|
+
delete: deleteResultSchema,
|
|
277
|
+
comment: commentResultSchema,
|
|
278
|
+
promote: promoteToolResultSchema,
|
|
279
|
+
get_metadata: metadataResultSchema,
|
|
280
|
+
set_metadata: metadataResultSchema,
|
|
281
|
+
find_files: findFilesResultSchema,
|
|
282
|
+
list_metadata_keys: metadataFacetsResultSchema,
|
|
283
|
+
repo_link_status: repoLinkStatusResultSchema,
|
|
284
|
+
usage: usageResultSchema,
|
|
285
|
+
reconcile: reconcileResultSchema,
|
|
286
|
+
purge_expired: purgeExpiredResultSchema,
|
|
287
|
+
health: healthResultSchema,
|
|
288
|
+
};
|
|
289
|
+
/** Shared-shape stdio tools. Hosted-only tools (`promote`, `repo_link_status`) omitted. */
|
|
290
|
+
export const stdioOutputSchemas = {
|
|
291
|
+
gallery_create: galleryResultSchema,
|
|
292
|
+
gallery_get: galleryResultSchema,
|
|
293
|
+
gallery_add: galleryItemSchema,
|
|
294
|
+
gallery_link: galleryReferenceSchema,
|
|
295
|
+
gallery_find_by_reference: galleryFindResultSchema,
|
|
296
|
+
put: putResultSchema,
|
|
297
|
+
list: listResultSchema,
|
|
298
|
+
delete: deleteResultSchema,
|
|
299
|
+
comment: objectSchema({
|
|
300
|
+
posted: { type: "boolean" },
|
|
301
|
+
reason: { type: "string" },
|
|
302
|
+
message: { type: "string" },
|
|
303
|
+
fixUrl: { type: "string" },
|
|
304
|
+
required: { type: "array", items: { type: "string" } },
|
|
305
|
+
action: { type: "string", enum: ["skipped", "created", "updated"] },
|
|
306
|
+
count: { type: "number" },
|
|
307
|
+
commentUrl: { type: "string" },
|
|
308
|
+
repo: { type: "string" },
|
|
309
|
+
kind: { type: "string" },
|
|
310
|
+
num: { type: "number" },
|
|
311
|
+
via: { type: "string", enum: ["bot", "gh"] },
|
|
312
|
+
}),
|
|
313
|
+
get_metadata: metadataResultSchema,
|
|
314
|
+
set_metadata: metadataResultSchema,
|
|
315
|
+
find_files: findFilesResultSchema,
|
|
316
|
+
list_metadata_keys: metadataFacetsResultSchema,
|
|
317
|
+
usage: usageResultSchema,
|
|
318
|
+
reconcile: reconcileResultSchema,
|
|
319
|
+
purge_expired: purgeExpiredResultSchema,
|
|
320
|
+
health: healthResultSchema,
|
|
321
|
+
report: objectSchema({
|
|
322
|
+
ok: { type: "boolean" },
|
|
323
|
+
id: { type: "string" },
|
|
324
|
+
hasAttachment: { type: "boolean" },
|
|
325
|
+
}, ["ok"]),
|
|
326
|
+
};
|
|
327
|
+
export function withOutputSchemas(tools, schemas, opts) {
|
|
328
|
+
return tools.map((tool) => {
|
|
329
|
+
const outputSchema = schemas[tool.name];
|
|
330
|
+
if (!outputSchema) {
|
|
331
|
+
if (opts.required) {
|
|
332
|
+
throw new Error(`missing output schema for MCP tool ${tool.name}`);
|
|
333
|
+
}
|
|
334
|
+
return tool;
|
|
335
|
+
}
|
|
336
|
+
return { ...tool, outputSchema };
|
|
337
|
+
});
|
|
338
|
+
}
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -22,11 +22,63 @@ export { appProp, canonicalMetaFromArgs, METADATA_DESCRIPTION, metadataArgWithCa
|
|
|
22
22
|
export { ToolBatchError, batchFailureMessage } from "./batch-error.js";
|
|
23
23
|
export { mapBounded } from "../async.js";
|
|
24
24
|
export { McpServer, type jsonSchemaValidator };
|
|
25
|
+
export { commentResultSchema, deleteResultSchema, findFilesResultSchema, galleryFindResultSchema, galleryResultSchema, healthResultSchema, hostedOutputSchemas, listResultSchema, metadataFacetsResultSchema, metadataResultSchema, promoteToolResultSchema, purgeExpiredResultSchema, putResultSchema, reconcileResultSchema, repoLinkStatusResultSchema, stdioOutputSchemas, usageResultSchema, withOutputSchemas, } from "./output-schemas.js";
|
|
26
|
+
/** MCP tool safety hints. Required so tools/list advertises them for review. */
|
|
27
|
+
export interface McpToolAnnotations {
|
|
28
|
+
readOnlyHint: boolean;
|
|
29
|
+
destructiveHint: boolean;
|
|
30
|
+
openWorldHint: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Lookup / list / health. Does not change workspace or public state. */
|
|
33
|
+
export declare const mcpRead: McpToolAnnotations;
|
|
34
|
+
/** Creates or updates a public object, gallery, or comment without deleting. */
|
|
35
|
+
export declare const mcpWritePublic: McpToolAnnotations;
|
|
36
|
+
/** Deletes or overwrites a public object or a public GitHub comment. */
|
|
37
|
+
export declare const mcpDestroyPublic: McpToolAnnotations;
|
|
38
|
+
/** Mutates first-party / internal state only (ledger, reports). */
|
|
39
|
+
export declare const mcpWriteInternal: McpToolAnnotations;
|
|
40
|
+
/**
|
|
41
|
+
* Per-tool auth policy for ChatGPT / Codex plugin review. Advertised on
|
|
42
|
+
* tools/list as `_meta.securitySchemes` (the SDK has no first-class field).
|
|
43
|
+
*/
|
|
44
|
+
export type McpSecurityScheme = {
|
|
45
|
+
type: "noauth";
|
|
46
|
+
} | {
|
|
47
|
+
type: "oauth2";
|
|
48
|
+
scopes: string[];
|
|
49
|
+
};
|
|
50
|
+
export declare const mcpOAuthRead: McpSecurityScheme[];
|
|
51
|
+
export declare const mcpOAuthWrite: McpSecurityScheme[];
|
|
52
|
+
export declare const mcpOAuthDelete: McpSecurityScheme[];
|
|
53
|
+
/** Authenticated, no particular file scope (hosted `health`). */
|
|
54
|
+
export declare const mcpOAuthAny: McpSecurityScheme[];
|
|
55
|
+
/** Callable without a token (stdio `health`). */
|
|
56
|
+
export declare const mcpNoAuth: McpSecurityScheme[];
|
|
57
|
+
/**
|
|
58
|
+
* Thrown when a presented token is missing a required scope. wrapHandler
|
|
59
|
+
* turns this into a tool error that carries `_meta["mcp/www_authenticate"]`
|
|
60
|
+
* so ChatGPT can prompt a re-consent.
|
|
61
|
+
*/
|
|
62
|
+
export declare class McpAuthError extends Error {
|
|
63
|
+
readonly challenge: string;
|
|
64
|
+
constructor(message: string, challenge: string);
|
|
65
|
+
}
|
|
66
|
+
/** Build an insufficient_scope challenge pointing at this resource's metadata. */
|
|
67
|
+
export declare function insufficientScopeError(resourceMetadataUrl: string, scope: string): McpAuthError;
|
|
25
68
|
export interface McpTool {
|
|
26
69
|
name: string;
|
|
27
70
|
description: string;
|
|
71
|
+
/** Short label for tools/list. Falls back to `name` when omitted. */
|
|
72
|
+
title?: string;
|
|
73
|
+
annotations: McpToolAnnotations;
|
|
74
|
+
securitySchemes: McpSecurityScheme[];
|
|
28
75
|
/** Hand-written JSON Schema for the tool's arguments. */
|
|
29
76
|
inputSchema: Record<string, unknown>;
|
|
77
|
+
/**
|
|
78
|
+
* Hand-written JSON Schema for successful `structuredContent`. Required
|
|
79
|
+
* whenever the handler returns structured data (OpenAI Scan Tools).
|
|
80
|
+
*/
|
|
81
|
+
outputSchema?: Record<string, unknown>;
|
|
30
82
|
handler: (args: Record<string, unknown>) => Promise<unknown>;
|
|
31
83
|
}
|
|
32
84
|
export declare function createMcpServer(opts: {
|
package/dist/mcp/server.js
CHANGED
|
@@ -25,6 +25,58 @@ export { appProp, canonicalMetaFromArgs, METADATA_DESCRIPTION, metadataArgWithCa
|
|
|
25
25
|
export { ToolBatchError, batchFailureMessage } from "./batch-error.js";
|
|
26
26
|
export { mapBounded } from "../async.js";
|
|
27
27
|
export { McpServer };
|
|
28
|
+
export { commentResultSchema, deleteResultSchema, findFilesResultSchema, galleryFindResultSchema, galleryResultSchema, healthResultSchema, hostedOutputSchemas, listResultSchema, metadataFacetsResultSchema, metadataResultSchema, promoteToolResultSchema, purgeExpiredResultSchema, putResultSchema, reconcileResultSchema, repoLinkStatusResultSchema, stdioOutputSchemas, usageResultSchema, withOutputSchemas, } from "./output-schemas.js";
|
|
29
|
+
/** Lookup / list / health. Does not change workspace or public state. */
|
|
30
|
+
export const mcpRead = {
|
|
31
|
+
readOnlyHint: true,
|
|
32
|
+
destructiveHint: false,
|
|
33
|
+
openWorldHint: false,
|
|
34
|
+
};
|
|
35
|
+
/** Creates or updates a public object, gallery, or comment without deleting. */
|
|
36
|
+
export const mcpWritePublic = {
|
|
37
|
+
readOnlyHint: false,
|
|
38
|
+
destructiveHint: false,
|
|
39
|
+
openWorldHint: true,
|
|
40
|
+
};
|
|
41
|
+
/** Deletes or overwrites a public object or a public GitHub comment. */
|
|
42
|
+
export const mcpDestroyPublic = {
|
|
43
|
+
readOnlyHint: false,
|
|
44
|
+
destructiveHint: true,
|
|
45
|
+
openWorldHint: true,
|
|
46
|
+
};
|
|
47
|
+
/** Mutates first-party / internal state only (ledger, reports). */
|
|
48
|
+
export const mcpWriteInternal = {
|
|
49
|
+
readOnlyHint: false,
|
|
50
|
+
destructiveHint: false,
|
|
51
|
+
openWorldHint: false,
|
|
52
|
+
};
|
|
53
|
+
function oauth(scopes) {
|
|
54
|
+
return [{ type: "oauth2", scopes }];
|
|
55
|
+
}
|
|
56
|
+
export const mcpOAuthRead = oauth(["files:read"]);
|
|
57
|
+
export const mcpOAuthWrite = oauth(["files:write"]);
|
|
58
|
+
export const mcpOAuthDelete = oauth(["files:delete"]);
|
|
59
|
+
/** Authenticated, no particular file scope (hosted `health`). */
|
|
60
|
+
export const mcpOAuthAny = oauth([]);
|
|
61
|
+
/** Callable without a token (stdio `health`). */
|
|
62
|
+
export const mcpNoAuth = [{ type: "noauth" }];
|
|
63
|
+
/**
|
|
64
|
+
* Thrown when a presented token is missing a required scope. wrapHandler
|
|
65
|
+
* turns this into a tool error that carries `_meta["mcp/www_authenticate"]`
|
|
66
|
+
* so ChatGPT can prompt a re-consent.
|
|
67
|
+
*/
|
|
68
|
+
export class McpAuthError extends Error {
|
|
69
|
+
challenge;
|
|
70
|
+
constructor(message, challenge) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = "McpAuthError";
|
|
73
|
+
this.challenge = challenge;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Build an insufficient_scope challenge pointing at this resource's metadata. */
|
|
77
|
+
export function insufficientScopeError(resourceMetadataUrl, scope) {
|
|
78
|
+
return new McpAuthError(`forbidden: requires ${scope} scope`, `Bearer resource_metadata="${resourceMetadataUrl}", error="insufficient_scope", error_description="This tool requires the ${scope} scope"`);
|
|
79
|
+
}
|
|
28
80
|
/**
|
|
29
81
|
* The tool catalog is fixed for the lifetime of a deploy, so a generous
|
|
30
82
|
* freshness hint is honest. `private` rather than `public` because the list is
|
|
@@ -75,6 +127,13 @@ function wrapHandler(tool, apiUrl) {
|
|
|
75
127
|
isError: true,
|
|
76
128
|
};
|
|
77
129
|
}
|
|
130
|
+
if (err instanceof McpAuthError) {
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: err.message }],
|
|
133
|
+
isError: true,
|
|
134
|
+
_meta: { "mcp/www_authenticate": [err.challenge] },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
78
137
|
return { content: [{ type: "text", text: toolErrorText(err) }], isError: true };
|
|
79
138
|
}
|
|
80
139
|
};
|
|
@@ -86,8 +145,14 @@ export function createMcpServer(opts) {
|
|
|
86
145
|
});
|
|
87
146
|
for (const tool of tools) {
|
|
88
147
|
server.registerTool(tool.name, {
|
|
148
|
+
...(tool.title ? { title: tool.title } : {}),
|
|
89
149
|
description: tool.description,
|
|
90
150
|
inputSchema: fromJsonSchema(tool.inputSchema, validator),
|
|
151
|
+
...(tool.outputSchema
|
|
152
|
+
? { outputSchema: fromJsonSchema(tool.outputSchema, validator) }
|
|
153
|
+
: {}),
|
|
154
|
+
annotations: tool.annotations,
|
|
155
|
+
_meta: { securitySchemes: tool.securitySchemes },
|
|
91
156
|
}, wrapHandler(tool, apiUrl));
|
|
92
157
|
}
|
|
93
158
|
return server;
|
package/dist/mcp/tools.js
CHANGED
|
@@ -10,7 +10,7 @@ import { validateMetaMap } from "../metadata.js";
|
|
|
10
10
|
import { mergeDerivedMeta } from "../metadata-vocab.js";
|
|
11
11
|
import { execRunner, ghMetadataFromTargetWithTitle, resolveCurrentBranch, resolveCurrentPullRequest, resolveRepo, } from "../github-gh.js";
|
|
12
12
|
import { appProp, canonicalMetaFromArgs, METADATA_DESCRIPTION, metadataArgWithCanonical, metadataProp, optBool, optPosInt, optString, optStringArray, optStringRecord, stateProp, usage, } from "./args.js";
|
|
13
|
-
import { batchFailureMessage, ToolBatchError } from "./server.js";
|
|
13
|
+
import { batchFailureMessage, mcpDestroyPublic, mcpNoAuth, mcpOAuthAny, mcpOAuthDelete, mcpOAuthRead, mcpOAuthWrite, mcpRead, mcpWriteInternal, mcpWritePublic, stdioOutputSchemas, withOutputSchemas, ToolBatchError, } from "./server.js";
|
|
14
14
|
import { attachmentFromText, buildReportPayload, parseReportType, REPORT_TYPES, submitReport, validateReportMessage, } from "../report.js";
|
|
15
15
|
import { resolveApiUrl } from "../config.js";
|
|
16
16
|
function mcpOptimizeOptions(args, defaults) {
|
|
@@ -130,9 +130,12 @@ export function createUploadsMcpTools(opts) {
|
|
|
130
130
|
}
|
|
131
131
|
return { comment, commentError };
|
|
132
132
|
};
|
|
133
|
-
|
|
133
|
+
const tools = [
|
|
134
134
|
{
|
|
135
135
|
name: "gallery_create",
|
|
136
|
+
title: "Create gallery",
|
|
137
|
+
annotations: mcpWritePublic,
|
|
138
|
+
securitySchemes: mcpOAuthWrite,
|
|
136
139
|
description: "Create a public ordered media gallery in the workspace. The returned canonical URL is safe to give users, but anyone who knows it can view the gallery and its media.",
|
|
137
140
|
inputSchema: {
|
|
138
141
|
type: "object",
|
|
@@ -154,6 +157,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
154
157
|
},
|
|
155
158
|
{
|
|
156
159
|
name: "gallery_get",
|
|
160
|
+
title: "Get gallery",
|
|
161
|
+
annotations: mcpRead,
|
|
162
|
+
securitySchemes: mcpOAuthRead,
|
|
157
163
|
description: "Get a workspace-owned gallery, including ordered media and its canonical public URL. Gallery media is public to anyone with the URL.",
|
|
158
164
|
inputSchema: {
|
|
159
165
|
type: "object",
|
|
@@ -171,6 +177,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
171
177
|
},
|
|
172
178
|
{
|
|
173
179
|
name: "gallery_add",
|
|
180
|
+
title: "Add gallery item",
|
|
181
|
+
annotations: mcpWritePublic,
|
|
182
|
+
securitySchemes: mcpOAuthWrite,
|
|
174
183
|
description: "Add one existing, publicly served workspace object to a gallery. Reads the latest gallery version before writing, so the optimistic API version is handled safely. Does not upload or delete the object.",
|
|
175
184
|
inputSchema: {
|
|
176
185
|
type: "object",
|
|
@@ -200,6 +209,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
200
209
|
},
|
|
201
210
|
{
|
|
202
211
|
name: "gallery_link",
|
|
212
|
+
title: "Link gallery",
|
|
213
|
+
annotations: mcpWritePublic,
|
|
214
|
+
securitySchemes: mcpOAuthWrite,
|
|
203
215
|
description: "Link a gallery to an external reference. References use provider-neutral fields; github currently accepts owner/repo#number or a strict GitHub issue/PR URL. No GitHub credentials or API calls are used.",
|
|
204
216
|
inputSchema: {
|
|
205
217
|
type: "object",
|
|
@@ -227,6 +239,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
227
239
|
},
|
|
228
240
|
{
|
|
229
241
|
name: "gallery_find_by_reference",
|
|
242
|
+
title: "Find galleries",
|
|
243
|
+
annotations: mcpRead,
|
|
244
|
+
securitySchemes: mcpOAuthRead,
|
|
230
245
|
description: "Find workspace galleries linked to an external reference. Returns gallery summaries and canonical public URLs without contacting the provider.",
|
|
231
246
|
inputSchema: {
|
|
232
247
|
type: "object",
|
|
@@ -254,6 +269,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
254
269
|
},
|
|
255
270
|
{
|
|
256
271
|
name: "put",
|
|
272
|
+
title: "Upload file",
|
|
273
|
+
annotations: mcpDestroyPublic,
|
|
274
|
+
securitySchemes: mcpOAuthWrite,
|
|
257
275
|
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.",
|
|
258
276
|
inputSchema: {
|
|
259
277
|
type: "object",
|
|
@@ -558,6 +576,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
558
576
|
},
|
|
559
577
|
{
|
|
560
578
|
name: "screenshot",
|
|
579
|
+
title: "Capture screenshot",
|
|
580
|
+
annotations: mcpDestroyPublic,
|
|
581
|
+
securitySchemes: mcpOAuthWrite,
|
|
561
582
|
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.",
|
|
562
583
|
inputSchema: {
|
|
563
584
|
type: "object",
|
|
@@ -584,6 +605,12 @@ export function createUploadsMcpTools(opts) {
|
|
|
584
605
|
},
|
|
585
606
|
selector: { type: "string", description: "Capture one element instead of the viewport." },
|
|
586
607
|
fullPage: { type: "boolean", description: "Capture the full scrollable page." },
|
|
608
|
+
maxHeight: {
|
|
609
|
+
type: "number",
|
|
610
|
+
description: "Cap on full-page capture height in CSS px (default: 5000, 0 = uncapped). A page over " +
|
|
611
|
+
"the cap is clipped, with a `hint` in the result. Requires fullPage. Applied on both " +
|
|
612
|
+
"via: local and via: remote so behavior matches.",
|
|
613
|
+
},
|
|
587
614
|
colorScheme: {
|
|
588
615
|
type: "string",
|
|
589
616
|
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.",
|
|
@@ -678,6 +705,12 @@ export function createUploadsMcpTools(opts) {
|
|
|
678
705
|
if (colorSchemeArg && colorSchemeArg !== "dark" && colorSchemeArg !== "light") {
|
|
679
706
|
usage("colorScheme must be dark or light");
|
|
680
707
|
}
|
|
708
|
+
const fullPageArg = optBool(args, "fullPage");
|
|
709
|
+
// Unlike other px args, 0 is valid here (uncapped) — optPosInt's
|
|
710
|
+
// allowZero option covers it.
|
|
711
|
+
const maxHeightArg = optPosInt(args, "maxHeight", { allowZero: true });
|
|
712
|
+
if (maxHeightArg !== undefined && !fullPageArg)
|
|
713
|
+
usage("maxHeight requires fullPage");
|
|
681
714
|
const target = ghTargetFromArgs(args, run);
|
|
682
715
|
const wantComment = optBool(args, "comment");
|
|
683
716
|
const dryRun = optBool(args, "dryRun");
|
|
@@ -778,7 +811,8 @@ export function createUploadsMcpTools(opts) {
|
|
|
778
811
|
cdp: optString(args, "cdp"),
|
|
779
812
|
viewport,
|
|
780
813
|
selector: optString(args, "selector"),
|
|
781
|
-
fullPage:
|
|
814
|
+
fullPage: fullPageArg,
|
|
815
|
+
maxHeight: maxHeightArg,
|
|
782
816
|
colorScheme: colorSchemeArg,
|
|
783
817
|
waitUntil: screenshotModule.parseWaitUntil(optString(args, "wait")),
|
|
784
818
|
hide: optStringArray(args, "hide"),
|
|
@@ -863,6 +897,11 @@ export function createUploadsMcpTools(opts) {
|
|
|
863
897
|
frame: prepared.frame,
|
|
864
898
|
gallery,
|
|
865
899
|
...(dryRun ? { dryRun: true } : {}),
|
|
900
|
+
// Full-page height cap note (issue #652), mirrors the CLI's stderr
|
|
901
|
+
// note + `hint` field.
|
|
902
|
+
...(captured.capped?.clipped
|
|
903
|
+
? { hint: screenshotModule.clipHintText(captured.capped.maxHeightPx, "maxHeight") }
|
|
904
|
+
: {}),
|
|
866
905
|
};
|
|
867
906
|
if (wantComment && target) {
|
|
868
907
|
const { comment, commentError } = await syncComment(client, target, config.workspace);
|
|
@@ -873,6 +912,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
873
912
|
},
|
|
874
913
|
{
|
|
875
914
|
name: "attach",
|
|
915
|
+
title: "Attach to GitHub",
|
|
916
|
+
annotations: mcpDestroyPublic,
|
|
917
|
+
securitySchemes: mcpOAuthWrite,
|
|
876
918
|
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.",
|
|
877
919
|
inputSchema: {
|
|
878
920
|
type: "object",
|
|
@@ -971,6 +1013,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
971
1013
|
},
|
|
972
1014
|
{
|
|
973
1015
|
name: "list",
|
|
1016
|
+
title: "List files",
|
|
1017
|
+
annotations: mcpRead,
|
|
1018
|
+
securitySchemes: mcpOAuthRead,
|
|
974
1019
|
description: "List uploaded objects in the workspace, filtered by key prefix or by a PR/issue's attachments. Paginate with cursor, or set all to fetch every page.",
|
|
975
1020
|
inputSchema: {
|
|
976
1021
|
type: "object",
|
|
@@ -1028,6 +1073,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1028
1073
|
},
|
|
1029
1074
|
{
|
|
1030
1075
|
name: "staged",
|
|
1076
|
+
title: "List staged files",
|
|
1077
|
+
annotations: mcpRead,
|
|
1078
|
+
securitySchemes: mcpOAuthRead,
|
|
1031
1079
|
description: "Read-only view of what's staged for a git branch (attach --branch / bare put on a non-default branch) and whether it will auto-attach once a PR opens. One list call against the branch staging prefix plus a repo-binding check (files:read only). Returns { repo, branch, files, binding }; binding.state is self/other/none/unknown and binding.autoAttach is true only for self.",
|
|
1032
1080
|
inputSchema: {
|
|
1033
1081
|
type: "object",
|
|
@@ -1053,6 +1101,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1053
1101
|
},
|
|
1054
1102
|
{
|
|
1055
1103
|
name: "delete",
|
|
1104
|
+
title: "Delete file",
|
|
1105
|
+
annotations: mcpDestroyPublic,
|
|
1106
|
+
securitySchemes: mcpOAuthDelete,
|
|
1056
1107
|
description: "Delete an uploaded object by key. Set dryRun to preview without deleting.",
|
|
1057
1108
|
inputSchema: {
|
|
1058
1109
|
type: "object",
|
|
@@ -1079,6 +1130,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1079
1130
|
},
|
|
1080
1131
|
{
|
|
1081
1132
|
name: "get_metadata",
|
|
1133
|
+
title: "Get metadata",
|
|
1134
|
+
annotations: mcpRead,
|
|
1135
|
+
securitySchemes: mcpOAuthRead,
|
|
1082
1136
|
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`.",
|
|
1083
1137
|
inputSchema: {
|
|
1084
1138
|
type: "object",
|
|
@@ -1098,6 +1152,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1098
1152
|
},
|
|
1099
1153
|
{
|
|
1100
1154
|
name: "set_metadata",
|
|
1155
|
+
title: "Set metadata",
|
|
1156
|
+
annotations: mcpWritePublic,
|
|
1157
|
+
securitySchemes: mcpOAuthWrite,
|
|
1101
1158
|
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. " +
|
|
1102
1159
|
METADATA_DESCRIPTION +
|
|
1103
1160
|
" Requires at least one of `set` or `delete`. Same as `uploads meta set`.",
|
|
@@ -1133,6 +1190,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1133
1190
|
},
|
|
1134
1191
|
{
|
|
1135
1192
|
name: "find_files",
|
|
1193
|
+
title: "Find files",
|
|
1194
|
+
annotations: mcpRead,
|
|
1195
|
+
securitySchemes: mcpOAuthRead,
|
|
1136
1196
|
description: "Find objects whose queryable custom metadata matches ALL of `filters` (ANDed equality) and/or whose key contains `name` (case-insensitive substring). At least one of `filters` or `name` is required. Returns each match's key, public URL, full metadata map, and optional `truncated`. Same as `uploads find k=v...` / `uploads find --name <term>`.",
|
|
1137
1197
|
inputSchema: {
|
|
1138
1198
|
type: "object",
|
|
@@ -1173,6 +1233,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1173
1233
|
},
|
|
1174
1234
|
{
|
|
1175
1235
|
name: "list_metadata_keys",
|
|
1236
|
+
title: "List metadata keys",
|
|
1237
|
+
annotations: mcpRead,
|
|
1238
|
+
securitySchemes: mcpOAuthRead,
|
|
1176
1239
|
description: "List the distinct queryable metadata keys present in the workspace, with file counts and distinct-value counts. Use this to discover what is filterable before calling find_files — keys are user/agent-defined, not a fixed schema. Same as `uploads meta keys`. Pass optional `key` to list that key's values instead (`uploads meta values <key>`).",
|
|
1177
1240
|
inputSchema: {
|
|
1178
1241
|
type: "object",
|
|
@@ -1193,6 +1256,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1193
1256
|
},
|
|
1194
1257
|
{
|
|
1195
1258
|
name: "usage",
|
|
1259
|
+
title: "Show usage",
|
|
1260
|
+
annotations: mcpRead,
|
|
1261
|
+
securitySchemes: mcpOAuthRead,
|
|
1196
1262
|
description: "Workspace storage and monthly upload counters (and remaining headroom when budgets are configured). Same as `uploads usage`.",
|
|
1197
1263
|
inputSchema: {
|
|
1198
1264
|
type: "object",
|
|
@@ -1206,6 +1272,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1206
1272
|
},
|
|
1207
1273
|
{
|
|
1208
1274
|
name: "reconcile",
|
|
1275
|
+
title: "Reconcile usage",
|
|
1276
|
+
annotations: mcpWriteInternal,
|
|
1277
|
+
securitySchemes: mcpOAuthWrite,
|
|
1209
1278
|
description: "Rebuild usage ledger bytes/objects from storage (source of truth). Preserves the monthly upload counter. Requires files:write. Same as `uploads reconcile`.",
|
|
1210
1279
|
inputSchema: {
|
|
1211
1280
|
type: "object",
|
|
@@ -1219,6 +1288,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1219
1288
|
},
|
|
1220
1289
|
{
|
|
1221
1290
|
name: "purge_expired",
|
|
1291
|
+
title: "Purge expired files",
|
|
1292
|
+
annotations: mcpDestroyPublic,
|
|
1293
|
+
securitySchemes: mcpOAuthDelete,
|
|
1222
1294
|
description: "Delete objects older than the workspace retentionDays setting, then reconcile. Skips if retention is unset. Requires files:delete. Same as `uploads purge-expired`.",
|
|
1223
1295
|
inputSchema: {
|
|
1224
1296
|
type: "object",
|
|
@@ -1232,6 +1304,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1232
1304
|
},
|
|
1233
1305
|
{
|
|
1234
1306
|
name: "comment",
|
|
1307
|
+
title: "Sync attachments comment",
|
|
1308
|
+
annotations: mcpDestroyPublic,
|
|
1309
|
+
securitySchemes: mcpOAuthWrite,
|
|
1235
1310
|
description: "Create or update the managed attachments comment on a GitHub PR or issue, listing everything uploaded for it. Posts as uploads-sh[bot] when the GitHub App is installed on the repo; otherwise via local gh auth. Edits its own prior comment in place and never touches other comments.",
|
|
1236
1311
|
inputSchema: {
|
|
1237
1312
|
type: "object",
|
|
@@ -1255,6 +1330,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1255
1330
|
},
|
|
1256
1331
|
{
|
|
1257
1332
|
name: "health",
|
|
1333
|
+
title: "Check health",
|
|
1334
|
+
annotations: mcpRead,
|
|
1335
|
+
securitySchemes: mcpNoAuth,
|
|
1258
1336
|
description: "Check uploads.sh API liveness. No auth or arguments required.",
|
|
1259
1337
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1260
1338
|
async handler(args) {
|
|
@@ -1265,6 +1343,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1265
1343
|
},
|
|
1266
1344
|
{
|
|
1267
1345
|
name: "doctor",
|
|
1346
|
+
title: "Diagnose setup",
|
|
1347
|
+
annotations: mcpRead,
|
|
1348
|
+
securitySchemes: mcpOAuthAny,
|
|
1268
1349
|
description: "Diagnose the configuration: API health, token auth, and workspace/token alignment. Returns the same report as `uploads doctor --json`, including hints.",
|
|
1269
1350
|
inputSchema: {
|
|
1270
1351
|
type: "object",
|
|
@@ -1278,6 +1359,9 @@ export function createUploadsMcpTools(opts) {
|
|
|
1278
1359
|
},
|
|
1279
1360
|
{
|
|
1280
1361
|
name: "report",
|
|
1362
|
+
title: "Send diagnostic report",
|
|
1363
|
+
annotations: mcpWriteInternal,
|
|
1364
|
+
securitySchemes: mcpOAuthWrite,
|
|
1281
1365
|
description: "Send an explicit diagnostic report to the uploads team (message + optional text log). " +
|
|
1282
1366
|
"Only call this when the user asked to submit feedback, a bug report, or error logs — " +
|
|
1283
1367
|
"never automatically. Do not include tokens, secrets, or private file contents. " +
|
|
@@ -1359,4 +1443,5 @@ export function createUploadsMcpTools(opts) {
|
|
|
1359
1443
|
},
|
|
1360
1444
|
},
|
|
1361
1445
|
];
|
|
1446
|
+
return withOutputSchemas(tools, stdioOutputSchemas, { required: false });
|
|
1362
1447
|
}
|
|
@@ -48,6 +48,13 @@ export interface LocalCaptureOptions {
|
|
|
48
48
|
};
|
|
49
49
|
selector?: string;
|
|
50
50
|
fullPage?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Cap (CSS px) on full-page capture height (issue #652). Only consulted
|
|
53
|
+
* when `fullPage` is true; 0 or undefined means uncapped. When the page's
|
|
54
|
+
* measured scroll height exceeds this, the capture is clipped to exactly
|
|
55
|
+
* this height instead of the full scroll height.
|
|
56
|
+
*/
|
|
57
|
+
maxHeightPx?: number;
|
|
51
58
|
colorScheme?: "dark" | "light";
|
|
52
59
|
/** "load" | "domcontentloaded" | "networkidle", or a millisecond settle delay. */
|
|
53
60
|
waitUntil: "load" | "domcontentloaded" | "networkidle" | number;
|
|
@@ -86,6 +93,16 @@ export interface MeasuredBox {
|
|
|
86
93
|
interface EvaluatablePage {
|
|
87
94
|
evaluate<T>(fn: (selectors: string[]) => T, arg: string[]): Promise<T>;
|
|
88
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Measures the page's full scrollable height in CSS px — the same quantity
|
|
98
|
+
* a `fullPage: true` screenshot would otherwise capture in its entirety.
|
|
99
|
+
* `document.documentElement.scrollHeight` is the standard measure; `body`'s
|
|
100
|
+
* is included too since some pages (quirks-mode, non-standard layouts) only
|
|
101
|
+
* grow one or the other.
|
|
102
|
+
*/
|
|
103
|
+
export declare function measureFullPageHeight(page: {
|
|
104
|
+
evaluate<T>(fn: () => T): Promise<T>;
|
|
105
|
+
}): Promise<number>;
|
|
89
106
|
/**
|
|
90
107
|
* Measures each selector's getBoundingClientRect on the page in a single
|
|
91
108
|
* `page.evaluate` round-trip, scaling CSS pixels to device (raster) pixels so
|
|
@@ -99,5 +116,6 @@ export declare function measureSelectorBoxes(page: EvaluatablePage, selectors: r
|
|
|
99
116
|
export declare function captureLocal(opts: LocalCaptureOptions): Promise<{
|
|
100
117
|
png: Uint8Array;
|
|
101
118
|
measures?: Record<string, MeasuredBox>;
|
|
119
|
+
clipped?: boolean;
|
|
102
120
|
}>;
|
|
103
121
|
export {};
|
package/dist/screenshot-local.js
CHANGED
|
@@ -203,6 +203,16 @@ export function detectLocalBrowser(roots = {}) {
|
|
|
203
203
|
const winner = [...candidates].toSorted((a, b) => rank(a) - rank(b))[0];
|
|
204
204
|
return { envOverride, candidates, winner };
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Measures the page's full scrollable height in CSS px — the same quantity
|
|
208
|
+
* a `fullPage: true` screenshot would otherwise capture in its entirety.
|
|
209
|
+
* `document.documentElement.scrollHeight` is the standard measure; `body`'s
|
|
210
|
+
* is included too since some pages (quirks-mode, non-standard layouts) only
|
|
211
|
+
* grow one or the other.
|
|
212
|
+
*/
|
|
213
|
+
export async function measureFullPageHeight(page) {
|
|
214
|
+
return page.evaluate(() => Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0));
|
|
215
|
+
}
|
|
206
216
|
/**
|
|
207
217
|
* Measures each selector's getBoundingClientRect on the page in a single
|
|
208
218
|
* `page.evaluate` round-trip, scaling CSS pixels to device (raster) pixels so
|
|
@@ -349,11 +359,33 @@ export async function captureLocal(opts) {
|
|
|
349
359
|
const measures = opts.measureSelectors && opts.measureSelectors.length > 0
|
|
350
360
|
? await measureSelectorBoxes(page, opts.measureSelectors, opts.viewport.deviceScaleFactor)
|
|
351
361
|
: undefined;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
362
|
+
// Full-page height cap (issue #652): measure the actual scroll height
|
|
363
|
+
// first so a page under the cap keeps its normal exact-content-height
|
|
364
|
+
// fullPage capture — only a page that genuinely exceeds the cap gets
|
|
365
|
+
// clipped to it. `clip` (CSS px, captures beyond the current viewport)
|
|
366
|
+
// is the mechanism; it can't be combined with `fullPage: true`.
|
|
367
|
+
let clipped = false;
|
|
368
|
+
let png;
|
|
369
|
+
if (opts.selector) {
|
|
370
|
+
png = await page.locator(opts.selector).screenshot({ timeout: opts.timeoutMs ?? 30_000 });
|
|
371
|
+
}
|
|
372
|
+
else if (opts.fullPage && opts.maxHeightPx && opts.maxHeightPx > 0) {
|
|
373
|
+
const fullHeight = await measureFullPageHeight(page);
|
|
374
|
+
if (fullHeight > opts.maxHeightPx) {
|
|
375
|
+
clipped = true;
|
|
376
|
+
png = await page.screenshot({
|
|
377
|
+
clip: { x: 0, y: 0, width: opts.viewport.width, height: opts.maxHeightPx },
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
png = await page.screenshot({ fullPage: true });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
png = await page.screenshot({ fullPage: opts.fullPage === true });
|
|
386
|
+
}
|
|
355
387
|
// Buffer extends Uint8Array — return it as-is rather than copying.
|
|
356
|
-
return { png, measures };
|
|
388
|
+
return { png, measures, clipped };
|
|
357
389
|
}
|
|
358
390
|
finally {
|
|
359
391
|
await browser.close();
|
|
@@ -10,6 +10,14 @@ export interface RemoteRenderRequest {
|
|
|
10
10
|
};
|
|
11
11
|
selector?: string;
|
|
12
12
|
fullPage?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Cap (CSS px) on full-page capture height (issue #652). Only sent when
|
|
15
|
+
* `fullPage` is true and the cap is active (nonzero); the server applies a
|
|
16
|
+
* best-effort clamp (see apps/api's render.ts) and reports whether it
|
|
17
|
+
* actually kicked in via the `X-Uploads-Full-Page-Clipped` response header,
|
|
18
|
+
* surfaced back on the result as `clipped`.
|
|
19
|
+
*/
|
|
20
|
+
maxHeight?: number;
|
|
13
21
|
colorScheme?: "dark" | "light";
|
|
14
22
|
waitUntil?: "load" | "domcontentloaded" | "networkidle" | number;
|
|
15
23
|
/** CSS selectors hidden (display:none) server-side before capture. */
|
|
@@ -27,5 +35,14 @@ export interface RemoteRenderOptions {
|
|
|
27
35
|
fetchImpl?: typeof fetch;
|
|
28
36
|
timeoutMs?: number;
|
|
29
37
|
}
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
export interface RemoteRenderResult {
|
|
39
|
+
png: Uint8Array;
|
|
40
|
+
/**
|
|
41
|
+
* True when the server's best-effort full-page height clamp actually
|
|
42
|
+
* kicked in (read from the `X-Uploads-Full-Page-Clipped` response header).
|
|
43
|
+
* Always false when `maxHeight` wasn't sent.
|
|
44
|
+
*/
|
|
45
|
+
clipped: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** POST the render request; resolves with raw PNG bytes (+ clip status) on 200. */
|
|
48
|
+
export declare function captureRemote(body: RemoteRenderRequest, opts: RemoteRenderOptions): Promise<RemoteRenderResult>;
|
|
@@ -32,7 +32,7 @@ function mapRenderError(status, message, code) {
|
|
|
32
32
|
return new UploadsError(message, "RATE_LIMITED", status);
|
|
33
33
|
return new UploadsError(message, "API_ERROR", status);
|
|
34
34
|
}
|
|
35
|
-
/** POST the render request; resolves with raw PNG bytes on 200. */
|
|
35
|
+
/** POST the render request; resolves with raw PNG bytes (+ clip status) on 200. */
|
|
36
36
|
export async function captureRemote(body, opts) {
|
|
37
37
|
if (body.html !== undefined) {
|
|
38
38
|
const htmlBytes = new TextEncoder().encode(body.html).byteLength;
|
|
@@ -65,7 +65,7 @@ export async function captureRemote(body, opts) {
|
|
|
65
65
|
if (bytes.byteLength === 0) {
|
|
66
66
|
throw new UploadsError("render endpoint returned an empty response", "RENDER_FAILED");
|
|
67
67
|
}
|
|
68
|
-
return bytes;
|
|
68
|
+
return { png: bytes, clipped: res.headers.get("x-uploads-full-page-clipped") === "true" };
|
|
69
69
|
}
|
|
70
70
|
catch (err) {
|
|
71
71
|
if (err instanceof UploadsError)
|
package/dist/screenshot.d.ts
CHANGED
|
@@ -25,6 +25,16 @@ export interface ScreenshotViewport {
|
|
|
25
25
|
deviceScaleFactor: number;
|
|
26
26
|
}
|
|
27
27
|
export declare const DEFAULT_SCREENSHOT_VIEWPORT: ScreenshotViewport;
|
|
28
|
+
/**
|
|
29
|
+
* Default cap (CSS px) on `--full-page` capture height (issue #652). Picked
|
|
30
|
+
* from the 4000-6000px range the issue proposed: generous enough to cover a
|
|
31
|
+
* long single-viewport marketing/docs page without clipping, but well short
|
|
32
|
+
* of the multi-thousand-entry-list case that motivated this (a 53-entry
|
|
33
|
+
* changelog page, PR #651) — those still clip, with a note pointing at
|
|
34
|
+
* `--max-height` to raise or remove the cap. `0` (via `--max-height 0`) means
|
|
35
|
+
* uncapped, for the rare case the full strip is genuinely wanted.
|
|
36
|
+
*/
|
|
37
|
+
export declare const DEFAULT_FULL_PAGE_MAX_HEIGHT = 5000;
|
|
28
38
|
/** Parses `WIDTHxHEIGHT[@SCALEx]`, e.g. "1280x800", "1280x800@2x", "1280x800@2". */
|
|
29
39
|
export declare function parseViewport(raw: string | undefined): ScreenshotViewport;
|
|
30
40
|
/** Parses `--wait`: "load" | "domcontentloaded" | "networkidle" | a millisecond count. */
|
|
@@ -63,6 +73,13 @@ export interface CaptureScreenshotOptions {
|
|
|
63
73
|
viewport?: ScreenshotViewport;
|
|
64
74
|
selector?: string;
|
|
65
75
|
fullPage?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Cap (CSS px) on `--full-page` capture height. Only meaningful with
|
|
78
|
+
* `fullPage: true`; ignored otherwise. `undefined` applies
|
|
79
|
+
* `DEFAULT_FULL_PAGE_MAX_HEIGHT`; `0` means uncapped. Wired through both
|
|
80
|
+
* capture backends so behavior matches regardless of `via` (issue #652).
|
|
81
|
+
*/
|
|
82
|
+
maxHeight?: number;
|
|
66
83
|
colorScheme?: "dark" | "light";
|
|
67
84
|
waitUntil?: WaitUntil;
|
|
68
85
|
/**
|
|
@@ -104,6 +121,8 @@ export interface CaptureScreenshotOptions {
|
|
|
104
121
|
viewport: ScreenshotViewport;
|
|
105
122
|
selector?: string;
|
|
106
123
|
fullPage?: boolean;
|
|
124
|
+
/** CSS px cap on full-page height; 0 or undefined = uncapped. */
|
|
125
|
+
maxHeightPx?: number;
|
|
107
126
|
colorScheme?: "dark" | "light";
|
|
108
127
|
waitUntil: WaitUntil;
|
|
109
128
|
hide?: string[];
|
|
@@ -117,6 +136,8 @@ export interface CaptureScreenshotOptions {
|
|
|
117
136
|
}) => Promise<{
|
|
118
137
|
png: Uint8Array;
|
|
119
138
|
measures?: Record<string, MeasuredBox>;
|
|
139
|
+
/** Present when `fullPage` + a positive `maxHeightPx` were given. */
|
|
140
|
+
clipped?: boolean;
|
|
120
141
|
}>;
|
|
121
142
|
/** Injectable for tests: replaces the remote capture implementation. */
|
|
122
143
|
captureRemoteImpl?: typeof captureRemote;
|
|
@@ -127,7 +148,23 @@ export interface CaptureScreenshotResult {
|
|
|
127
148
|
backend: "local" | "remote";
|
|
128
149
|
/** Present when `measureSelectors` was given and the local backend ran. */
|
|
129
150
|
measures?: Record<string, MeasuredBox>;
|
|
151
|
+
/**
|
|
152
|
+
* Present when `fullPage` was requested with a nonzero max-height cap
|
|
153
|
+
* (explicit or default) — regardless of backend. `clipped` tells the
|
|
154
|
+
* caller whether the cap actually kicked in, so it can print the
|
|
155
|
+
* "exceeds Npx; clipped" note and JSON `hint` (issue #652).
|
|
156
|
+
*/
|
|
157
|
+
capped?: {
|
|
158
|
+
maxHeightPx: number;
|
|
159
|
+
clipped: boolean;
|
|
160
|
+
};
|
|
130
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* The "clipped by the max-height cap" note shared by the CLI's stderr
|
|
164
|
+
* message and the MCP tool's JSON `hint` field (issue #652) — identical
|
|
165
|
+
* except for how each surface names the flag to raise the cap.
|
|
166
|
+
*/
|
|
167
|
+
export declare function clipHintText(maxHeightPx: number, flagName: string): string;
|
|
131
168
|
/**
|
|
132
169
|
* Folds a `--state` value into an auto-derived filename's stem, e.g.
|
|
133
170
|
* `localhost-docs-mcp.png` + "before" → `localhost-docs-mcp-before.png`. This
|
package/dist/screenshot.js
CHANGED
|
@@ -48,6 +48,16 @@ export const DEFAULT_SCREENSHOT_VIEWPORT = {
|
|
|
48
48
|
height: 800,
|
|
49
49
|
deviceScaleFactor: 2,
|
|
50
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* Default cap (CSS px) on `--full-page` capture height (issue #652). Picked
|
|
53
|
+
* from the 4000-6000px range the issue proposed: generous enough to cover a
|
|
54
|
+
* long single-viewport marketing/docs page without clipping, but well short
|
|
55
|
+
* of the multi-thousand-entry-list case that motivated this (a 53-entry
|
|
56
|
+
* changelog page, PR #651) — those still clip, with a note pointing at
|
|
57
|
+
* `--max-height` to raise or remove the cap. `0` (via `--max-height 0`) means
|
|
58
|
+
* uncapped, for the rare case the full strip is genuinely wanted.
|
|
59
|
+
*/
|
|
60
|
+
export const DEFAULT_FULL_PAGE_MAX_HEIGHT = 5000;
|
|
51
61
|
/** Parses `WIDTHxHEIGHT[@SCALEx]`, e.g. "1280x800", "1280x800@2x", "1280x800@2". */
|
|
52
62
|
export function parseViewport(raw) {
|
|
53
63
|
if (!raw)
|
|
@@ -146,6 +156,14 @@ export function classifyTarget(target) {
|
|
|
146
156
|
}
|
|
147
157
|
return { kind: "html-file", path: abs, html: readFileSync(abs, "utf8") };
|
|
148
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* The "clipped by the max-height cap" note shared by the CLI's stderr
|
|
161
|
+
* message and the MCP tool's JSON `hint` field (issue #652) — identical
|
|
162
|
+
* except for how each surface names the flag to raise the cap.
|
|
163
|
+
*/
|
|
164
|
+
export function clipHintText(maxHeightPx, flagName) {
|
|
165
|
+
return `full page exceeds ${maxHeightPx}px; clipped — use ${flagName} to raise`;
|
|
166
|
+
}
|
|
149
167
|
/** Derives a filename from a URL (host+path) or the source .html filename. */
|
|
150
168
|
function deriveFilename(target) {
|
|
151
169
|
if (target.kind === "html-file") {
|
|
@@ -225,6 +243,14 @@ export async function captureScreenshot(opts) {
|
|
|
225
243
|
for (const sel of opts.hide ?? [])
|
|
226
244
|
assertHideSelector(sel);
|
|
227
245
|
const hide = [...(opts.hide ?? []), ...(autoHideDevTools ? DEV_TOOLBAR_SELECTORS : [])];
|
|
246
|
+
// Full-page height cap (issue #652): only meaningful with fullPage. 0
|
|
247
|
+
// (explicit --max-height 0) means uncapped; undefined applies the default.
|
|
248
|
+
const effectiveMaxHeight = opts.fullPage ? (opts.maxHeight ?? DEFAULT_FULL_PAGE_MAX_HEIGHT) : 0;
|
|
249
|
+
// Shared by both backends below — only meaningful when fullPage capped at
|
|
250
|
+
// a positive height; `clipped` is the one thing that differs per backend.
|
|
251
|
+
const cappedFrom = (clipped) => opts.fullPage && effectiveMaxHeight > 0
|
|
252
|
+
? { maxHeightPx: effectiveMaxHeight, clipped }
|
|
253
|
+
: undefined;
|
|
228
254
|
// Populated only when auto-routing actually probes the filesystem, so it
|
|
229
255
|
// can be threaded into captureLocalImpl below to avoid a second scan.
|
|
230
256
|
let detected;
|
|
@@ -284,6 +310,7 @@ export async function captureScreenshot(opts) {
|
|
|
284
310
|
viewport,
|
|
285
311
|
selector: opts.selector,
|
|
286
312
|
fullPage: opts.fullPage,
|
|
313
|
+
maxHeightPx: effectiveMaxHeight,
|
|
287
314
|
colorScheme: opts.colorScheme,
|
|
288
315
|
waitUntil,
|
|
289
316
|
hide,
|
|
@@ -294,7 +321,8 @@ export async function captureScreenshot(opts) {
|
|
|
294
321
|
detectRoots: opts.detectRoots,
|
|
295
322
|
detectResult: detected,
|
|
296
323
|
});
|
|
297
|
-
|
|
324
|
+
const capped = cappedFrom(localResult.clipped === true);
|
|
325
|
+
return { png: localResult.png, filename, backend, measures: localResult.measures, capped };
|
|
298
326
|
}
|
|
299
327
|
if (target.kind === "html-file") {
|
|
300
328
|
const bytes = new TextEncoder().encode(target.html).byteLength;
|
|
@@ -303,15 +331,17 @@ export async function captureScreenshot(opts) {
|
|
|
303
331
|
}
|
|
304
332
|
}
|
|
305
333
|
const captureRemoteImpl = opts.captureRemoteImpl ?? captureRemote;
|
|
306
|
-
const
|
|
334
|
+
const remoteResult = await captureRemoteImpl({
|
|
307
335
|
...(target.kind === "html-file" ? { html: target.html } : { url: target.url }),
|
|
308
336
|
viewport,
|
|
309
337
|
selector: opts.selector,
|
|
310
338
|
fullPage: opts.fullPage,
|
|
339
|
+
...(opts.fullPage && effectiveMaxHeight > 0 ? { maxHeight: effectiveMaxHeight } : {}),
|
|
311
340
|
colorScheme: opts.colorScheme,
|
|
312
341
|
waitUntil,
|
|
313
342
|
...(hide.length > 0 ? { hide } : {}),
|
|
314
343
|
...(opts.reducedMotion ? { reducedMotion: true } : {}),
|
|
315
344
|
}, { apiUrl: opts.apiUrl, token: opts.token });
|
|
316
|
-
|
|
345
|
+
const capped = cappedFrom(remoteResult.clipped === true);
|
|
346
|
+
return { png: remoteResult.png, filename, backend, capped };
|
|
317
347
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildinternet/uploads",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.1",
|
|
4
4
|
"description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"node": ">=22"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"files-sdk": "^2.2.
|
|
41
|
+
"files-sdk": "^2.2.4"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"files-sdk": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^26.1.0",
|
|
50
50
|
"ai": "^6.0.0",
|
|
51
|
-
"files-sdk": "^2.2.
|
|
51
|
+
"files-sdk": "^2.2.4",
|
|
52
52
|
"typescript": "^7.0.2",
|
|
53
53
|
"vitest": "^4.1.10"
|
|
54
54
|
},
|