@screenshotscout/mcp 0.1.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.
@@ -0,0 +1,387 @@
1
+ import { Buffer } from "node:buffer";
2
+ export const MAX_INLINE_IMAGE_DIMENSION = 8_000;
3
+ export const MAX_INLINE_IMAGE_RAW_BYTES = 5 * 1_024 * 1_024;
4
+ export const DEFAULT_MAX_SERIALIZED_RESULT_BYTES = 128_000;
5
+ export const DEFAULT_MAX_IMAGE_HEADER_BYTES = 64 * 1_024;
6
+ export const INLINE_IMAGE_MIME_TYPES = [
7
+ "image/png",
8
+ "image/jpeg",
9
+ "image/webp",
10
+ "image/gif",
11
+ ];
12
+ export const INLINE_IMAGE_OMISSION_REASONS = {
13
+ RESULT_MODE_URL_ONLY: "result_mode_url_only",
14
+ FORMAT_NOT_INLINE_ELIGIBLE: "format_not_inline_eligible",
15
+ UNSUPPORTED_MIME_TYPE: "unsupported_mime_type",
16
+ MIME_TYPE_MISMATCH: "mime_type_mismatch",
17
+ RAW_SIZE_EXCEEDS_LIMIT: "raw_size_exceeds_limit",
18
+ DIMENSIONS_UNAVAILABLE: "dimensions_unavailable",
19
+ DIMENSIONS_EXCEED_LIMIT: "dimensions_exceed_limit",
20
+ CLIENT_NOT_COMPATIBLE: "client_not_compatible",
21
+ SERIALIZED_SIZE_UNAVAILABLE: "serialized_size_unavailable",
22
+ SERIALIZED_SIZE_EXCEEDS_LIMIT: "serialized_size_exceeds_limit",
23
+ };
24
+ const inlineMimeTypeSet = new Set(INLINE_IMAGE_MIME_TYPES);
25
+ const inlineFormatMimeTypes = Object.freeze({
26
+ png: "image/png",
27
+ jpg: "image/jpeg",
28
+ jpeg: "image/jpeg",
29
+ webp: "image/webp",
30
+ gif: "image/gif",
31
+ });
32
+ export function normalizeMimeType(value) {
33
+ const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
34
+ return mediaType === undefined || mediaType.length === 0
35
+ ? undefined
36
+ : mediaType;
37
+ }
38
+ export function asInlineImageMimeType(value) {
39
+ const normalized = normalizeMimeType(value);
40
+ return normalized !== undefined && inlineMimeTypeSet.has(normalized)
41
+ ? normalized
42
+ : undefined;
43
+ }
44
+ export function resolveImageDimensions(bytes, mimeType, trustedDimensions, maxHeaderBytes = DEFAULT_MAX_IMAGE_HEADER_BYTES) {
45
+ const trusted = normalizeDimensions(trustedDimensions?.imageWidth, trustedDimensions?.imageHeight);
46
+ return trusted ?? inspectImageDimensions(bytes, mimeType, maxHeaderBytes);
47
+ }
48
+ /**
49
+ * Reads only bounded format headers. It does not decode pixels or transform
50
+ * the payload.
51
+ */
52
+ export function inspectImageDimensions(bytes, mimeType, maxHeaderBytes = DEFAULT_MAX_IMAGE_HEADER_BYTES) {
53
+ const normalizedMimeType = normalizeMimeType(mimeType);
54
+ const inspectionLength = boundedInspectionLength(bytes.byteLength, maxHeaderBytes);
55
+ switch (normalizedMimeType) {
56
+ case "image/png":
57
+ return inspectPng(bytes, inspectionLength);
58
+ case "image/jpeg":
59
+ return inspectJpeg(bytes, inspectionLength);
60
+ case "image/gif":
61
+ return inspectGif(bytes, inspectionLength);
62
+ case "image/webp":
63
+ return inspectWebp(bytes, inspectionLength);
64
+ default:
65
+ return undefined;
66
+ }
67
+ }
68
+ export function evaluateInlineImage(candidate) {
69
+ const serializedSizeLimitBytes = resolveSerializedSizeLimit(candidate.serializedSizeLimitBytes);
70
+ const baseOmission = {
71
+ rawBytes: candidate.bytes.byteLength,
72
+ serializedSizeLimitBytes,
73
+ };
74
+ if (candidate.resultMode === "url_only") {
75
+ return {
76
+ included: false,
77
+ reason: INLINE_IMAGE_OMISSION_REASONS.RESULT_MODE_URL_ONLY,
78
+ ...baseOmission,
79
+ };
80
+ }
81
+ const expectedMimeType = inlineFormatMimeTypes[candidate.format];
82
+ if (expectedMimeType === undefined) {
83
+ return {
84
+ included: false,
85
+ reason: INLINE_IMAGE_OMISSION_REASONS.FORMAT_NOT_INLINE_ELIGIBLE,
86
+ ...baseOmission,
87
+ };
88
+ }
89
+ const mimeType = asInlineImageMimeType(candidate.mimeType);
90
+ if (mimeType === undefined) {
91
+ return {
92
+ included: false,
93
+ reason: INLINE_IMAGE_OMISSION_REASONS.UNSUPPORTED_MIME_TYPE,
94
+ ...baseOmission,
95
+ };
96
+ }
97
+ if (mimeType !== expectedMimeType) {
98
+ return {
99
+ included: false,
100
+ reason: INLINE_IMAGE_OMISSION_REASONS.MIME_TYPE_MISMATCH,
101
+ ...baseOmission,
102
+ };
103
+ }
104
+ if (candidate.bytes.byteLength > MAX_INLINE_IMAGE_RAW_BYTES) {
105
+ return {
106
+ included: false,
107
+ reason: INLINE_IMAGE_OMISSION_REASONS.RAW_SIZE_EXCEEDS_LIMIT,
108
+ ...baseOmission,
109
+ };
110
+ }
111
+ const dimensions = resolveImageDimensions(candidate.bytes, mimeType, candidate.trustedDimensions);
112
+ if (dimensions === undefined) {
113
+ return {
114
+ included: false,
115
+ reason: INLINE_IMAGE_OMISSION_REASONS.DIMENSIONS_UNAVAILABLE,
116
+ ...baseOmission,
117
+ };
118
+ }
119
+ if (dimensions.width > MAX_INLINE_IMAGE_DIMENSION ||
120
+ dimensions.height > MAX_INLINE_IMAGE_DIMENSION) {
121
+ return {
122
+ included: false,
123
+ reason: INLINE_IMAGE_OMISSION_REASONS.DIMENSIONS_EXCEED_LIMIT,
124
+ dimensions,
125
+ ...baseOmission,
126
+ };
127
+ }
128
+ const clientSupportsInlineImage = safelyCheckClientImageSupport(candidate.clientSupportsInlineImage, mimeType);
129
+ if (!clientSupportsInlineImage) {
130
+ return {
131
+ included: false,
132
+ reason: INLINE_IMAGE_OMISSION_REASONS.CLIENT_NOT_COMPATIBLE,
133
+ dimensions,
134
+ ...baseOmission,
135
+ };
136
+ }
137
+ const minimumBase64Bytes = 4 * Math.ceil(candidate.bytes.byteLength / 3);
138
+ if (minimumBase64Bytes > serializedSizeLimitBytes) {
139
+ return {
140
+ included: false,
141
+ reason: INLINE_IMAGE_OMISSION_REASONS.SERIALIZED_SIZE_EXCEEDS_LIMIT,
142
+ dimensions,
143
+ ...baseOmission,
144
+ };
145
+ }
146
+ const image = {
147
+ type: "image",
148
+ data: Buffer.from(candidate.bytes.buffer, candidate.bytes.byteOffset, candidate.bytes.byteLength).toString("base64"),
149
+ mimeType,
150
+ };
151
+ let serialized;
152
+ try {
153
+ serialized = (candidate.stringify ?? JSON.stringify)(candidate.buildSerializedResult(image, dimensions));
154
+ }
155
+ catch {
156
+ serialized = undefined;
157
+ }
158
+ if (serialized === undefined) {
159
+ return {
160
+ included: false,
161
+ reason: INLINE_IMAGE_OMISSION_REASONS.SERIALIZED_SIZE_UNAVAILABLE,
162
+ dimensions,
163
+ ...baseOmission,
164
+ };
165
+ }
166
+ const serializedBytes = new TextEncoder().encode(serialized).byteLength;
167
+ if (serializedBytes > serializedSizeLimitBytes) {
168
+ return {
169
+ included: false,
170
+ reason: INLINE_IMAGE_OMISSION_REASONS.SERIALIZED_SIZE_EXCEEDS_LIMIT,
171
+ dimensions,
172
+ ...baseOmission,
173
+ };
174
+ }
175
+ return {
176
+ included: true,
177
+ image,
178
+ dimensions,
179
+ rawBytes: candidate.bytes.byteLength,
180
+ serializedBytes,
181
+ serializedSizeLimitBytes,
182
+ };
183
+ }
184
+ export function resolveSerializedSizeLimit(requestedLimitBytes) {
185
+ if (requestedLimitBytes === undefined) {
186
+ return DEFAULT_MAX_SERIALIZED_RESULT_BYTES;
187
+ }
188
+ if (!Number.isSafeInteger(requestedLimitBytes) || requestedLimitBytes <= 0) {
189
+ return 0;
190
+ }
191
+ return Math.min(requestedLimitBytes, DEFAULT_MAX_SERIALIZED_RESULT_BYTES);
192
+ }
193
+ function normalizeDimensions(width, height) {
194
+ return isPositiveSafeInteger(width) && isPositiveSafeInteger(height)
195
+ ? { width, height }
196
+ : undefined;
197
+ }
198
+ function safelyCheckClientImageSupport(supportsInlineImage, mimeType) {
199
+ try {
200
+ return supportsInlineImage(mimeType);
201
+ }
202
+ catch {
203
+ return false;
204
+ }
205
+ }
206
+ function isPositiveSafeInteger(value) {
207
+ return value !== undefined && Number.isSafeInteger(value) && value > 0;
208
+ }
209
+ function boundedInspectionLength(byteLength, maxHeaderBytes) {
210
+ if (!Number.isSafeInteger(maxHeaderBytes) || maxHeaderBytes <= 0)
211
+ return 0;
212
+ return Math.min(byteLength, maxHeaderBytes, DEFAULT_MAX_IMAGE_HEADER_BYTES);
213
+ }
214
+ function inspectPng(bytes, inspectionLength) {
215
+ const signature = [137, 80, 78, 71, 13, 10, 26, 10];
216
+ if (inspectionLength < 24 ||
217
+ !matchesBytes(bytes, 0, signature) ||
218
+ readUint32BigEndian(bytes, 8) !== 13 ||
219
+ !matchesAscii(bytes, 12, "IHDR")) {
220
+ return undefined;
221
+ }
222
+ return normalizeDimensions(readUint32BigEndian(bytes, 16), readUint32BigEndian(bytes, 20));
223
+ }
224
+ function inspectGif(bytes, inspectionLength) {
225
+ if (inspectionLength < 10 ||
226
+ (!matchesAscii(bytes, 0, "GIF87a") && !matchesAscii(bytes, 0, "GIF89a"))) {
227
+ return undefined;
228
+ }
229
+ return normalizeDimensions(readUint16LittleEndian(bytes, 6), readUint16LittleEndian(bytes, 8));
230
+ }
231
+ function inspectJpeg(bytes, inspectionLength) {
232
+ if (inspectionLength < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) {
233
+ return undefined;
234
+ }
235
+ let offset = 2;
236
+ while (offset + 1 < inspectionLength) {
237
+ if (bytes[offset] !== 0xff)
238
+ return undefined;
239
+ while (offset < inspectionLength && bytes[offset] === 0xff)
240
+ offset += 1;
241
+ if (offset >= inspectionLength)
242
+ return undefined;
243
+ const marker = bytes[offset];
244
+ offset += 1;
245
+ if (marker === undefined || marker === 0x00)
246
+ return undefined;
247
+ if (marker === 0xd9 || marker === 0xda)
248
+ return undefined;
249
+ if (marker === 0x01 ||
250
+ marker === 0xd8 ||
251
+ (marker >= 0xd0 && marker <= 0xd7)) {
252
+ continue;
253
+ }
254
+ if (offset + 2 > inspectionLength)
255
+ return undefined;
256
+ const segmentLength = readUint16BigEndian(bytes, offset);
257
+ if (segmentLength === undefined || segmentLength < 2)
258
+ return undefined;
259
+ if (isStartOfFrameMarker(marker)) {
260
+ if (segmentLength < 7 || offset + 7 > inspectionLength)
261
+ return undefined;
262
+ return normalizeDimensions(readUint16BigEndian(bytes, offset + 5), readUint16BigEndian(bytes, offset + 3));
263
+ }
264
+ const nextOffset = offset + segmentLength;
265
+ if (nextOffset <= offset || nextOffset > inspectionLength)
266
+ return undefined;
267
+ offset = nextOffset;
268
+ }
269
+ return undefined;
270
+ }
271
+ function inspectWebp(bytes, inspectionLength) {
272
+ if (inspectionLength < 20 ||
273
+ !matchesAscii(bytes, 0, "RIFF") ||
274
+ !matchesAscii(bytes, 8, "WEBP")) {
275
+ return undefined;
276
+ }
277
+ let offset = 12;
278
+ while (offset + 8 <= inspectionLength) {
279
+ const chunkSize = readUint32LittleEndian(bytes, offset + 4);
280
+ if (chunkSize === undefined)
281
+ return undefined;
282
+ const dataOffset = offset + 8;
283
+ const chunkType = asciiAt(bytes, offset, 4);
284
+ if (chunkType === "VP8X" && chunkSize >= 10) {
285
+ if (dataOffset + 10 > inspectionLength)
286
+ return undefined;
287
+ return normalizeDimensions(1 + readUint24LittleEndian(bytes, dataOffset + 4), 1 + readUint24LittleEndian(bytes, dataOffset + 7));
288
+ }
289
+ if (chunkType === "VP8L" && chunkSize >= 5) {
290
+ if (dataOffset + 5 > inspectionLength || bytes[dataOffset] !== 0x2f) {
291
+ return undefined;
292
+ }
293
+ const byte1 = bytes[dataOffset + 1];
294
+ const byte2 = bytes[dataOffset + 2];
295
+ const byte3 = bytes[dataOffset + 3];
296
+ const byte4 = bytes[dataOffset + 4];
297
+ if (byte1 === undefined ||
298
+ byte2 === undefined ||
299
+ byte3 === undefined ||
300
+ byte4 === undefined) {
301
+ return undefined;
302
+ }
303
+ return normalizeDimensions(1 + byte1 + ((byte2 & 0x3f) << 8), 1 + (byte2 >> 6) + (byte3 << 2) + ((byte4 & 0x0f) << 10));
304
+ }
305
+ if (chunkType === "VP8 " && chunkSize >= 10) {
306
+ if (dataOffset + 10 > inspectionLength ||
307
+ bytes[dataOffset + 3] !== 0x9d ||
308
+ bytes[dataOffset + 4] !== 0x01 ||
309
+ bytes[dataOffset + 5] !== 0x2a) {
310
+ return undefined;
311
+ }
312
+ const width = readUint16LittleEndian(bytes, dataOffset + 6);
313
+ const height = readUint16LittleEndian(bytes, dataOffset + 8);
314
+ return normalizeDimensions(width === undefined ? undefined : width & 0x3fff, height === undefined ? undefined : height & 0x3fff);
315
+ }
316
+ const paddedChunkSize = chunkSize + (chunkSize % 2);
317
+ const nextOffset = dataOffset + paddedChunkSize;
318
+ if (nextOffset <= offset || nextOffset > inspectionLength)
319
+ return undefined;
320
+ offset = nextOffset;
321
+ }
322
+ return undefined;
323
+ }
324
+ function isStartOfFrameMarker(marker) {
325
+ return ((marker >= 0xc0 && marker <= 0xc3) ||
326
+ (marker >= 0xc5 && marker <= 0xc7) ||
327
+ (marker >= 0xc9 && marker <= 0xcb) ||
328
+ (marker >= 0xcd && marker <= 0xcf));
329
+ }
330
+ function matchesAscii(bytes, offset, expected) {
331
+ return matchesBytes(bytes, offset, [...expected].map((character) => character.charCodeAt(0)));
332
+ }
333
+ function matchesBytes(bytes, offset, expected) {
334
+ if (offset < 0 || offset + expected.length > bytes.byteLength)
335
+ return false;
336
+ return expected.every((value, index) => bytes[offset + index] === value);
337
+ }
338
+ function asciiAt(bytes, offset, length) {
339
+ if (offset < 0 || offset + length > bytes.byteLength)
340
+ return undefined;
341
+ return String.fromCharCode(...bytes.subarray(offset, offset + length));
342
+ }
343
+ function readUint16BigEndian(bytes, offset) {
344
+ const high = bytes[offset];
345
+ const low = bytes[offset + 1];
346
+ return high === undefined || low === undefined
347
+ ? undefined
348
+ : high * 0x100 + low;
349
+ }
350
+ function readUint16LittleEndian(bytes, offset) {
351
+ const low = bytes[offset];
352
+ const high = bytes[offset + 1];
353
+ return high === undefined || low === undefined
354
+ ? undefined
355
+ : low + high * 0x100;
356
+ }
357
+ function readUint24LittleEndian(bytes, offset) {
358
+ return ((bytes[offset] ?? 0) +
359
+ (bytes[offset + 1] ?? 0) * 0x100 +
360
+ (bytes[offset + 2] ?? 0) * 0x10_000);
361
+ }
362
+ function readUint32BigEndian(bytes, offset) {
363
+ const byte0 = bytes[offset];
364
+ const byte1 = bytes[offset + 1];
365
+ const byte2 = bytes[offset + 2];
366
+ const byte3 = bytes[offset + 3];
367
+ if (byte0 === undefined ||
368
+ byte1 === undefined ||
369
+ byte2 === undefined ||
370
+ byte3 === undefined) {
371
+ return undefined;
372
+ }
373
+ return byte0 * 0x100_0000 + byte1 * 0x1_0000 + byte2 * 0x100 + byte3;
374
+ }
375
+ function readUint32LittleEndian(bytes, offset) {
376
+ const byte0 = bytes[offset];
377
+ const byte1 = bytes[offset + 1];
378
+ const byte2 = bytes[offset + 2];
379
+ const byte3 = bytes[offset + 3];
380
+ if (byte0 === undefined ||
381
+ byte1 === undefined ||
382
+ byte2 === undefined ||
383
+ byte3 === undefined) {
384
+ return undefined;
385
+ }
386
+ return byte0 + byte1 * 0x100 + byte2 * 0x1_0000 + byte3 * 0x100_0000;
387
+ }
@@ -0,0 +1,15 @@
1
+ export declare const REDACTED_VALUE = "[REDACTED]";
2
+ export declare const REDACTED_URL = "[REDACTED_URL]";
3
+ export type RedactionOptions = {
4
+ readonly sensitiveValues?: readonly (string | null | undefined)[];
5
+ readonly redactUrls?: boolean;
6
+ readonly maxLength?: number;
7
+ };
8
+ export type RedactedDiagnostic = {
9
+ readonly name: string;
10
+ readonly message: string;
11
+ readonly stack?: string;
12
+ };
13
+ export declare function redactText(input: string, options?: RedactionOptions): string;
14
+ export declare function redactUnknown(value: unknown, options?: RedactionOptions): unknown;
15
+ export declare function toRedactedDiagnostic(error: unknown, options?: RedactionOptions): RedactedDiagnostic;
@@ -0,0 +1,185 @@
1
+ export const REDACTED_VALUE = "[REDACTED]";
2
+ export const REDACTED_URL = "[REDACTED_URL]";
3
+ const sensitiveFieldNames = new Set([
4
+ "accesskey",
5
+ "authorization",
6
+ "body",
7
+ "bytes",
8
+ "cookie",
9
+ "cookies",
10
+ "headers",
11
+ "secretkey",
12
+ "signature",
13
+ "screenshoturl",
14
+ "targeturl",
15
+ ]);
16
+ export function redactText(input, options = {}) {
17
+ let output = input
18
+ .replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, `$1${REDACTED_VALUE}`)
19
+ .replace(/([?&](?:access[_-]?key|secret[_-]?key|signature)=)[^&#\s]*/gi, `$1${REDACTED_VALUE}`)
20
+ .replace(/((?:SCREENSHOTSCOUT_(?:ACCESS|SECRET)_KEY|access[_-]?key|secret[_-]?key|signature)\s*(?:=|:)\s*)(?:"[^"]*"|'[^']*'|[^\s,;&}]+)/gi, `$1${REDACTED_VALUE}`);
21
+ const sensitiveValues = [...(options.sensitiveValues ?? [])]
22
+ .filter((value) => value !== null && value !== undefined)
23
+ .filter((value) => value.length > 0)
24
+ .flatMap((value) => {
25
+ const encoded = safelyEncodeURIComponent(value);
26
+ return encoded === undefined || encoded === value
27
+ ? [value]
28
+ : [value, encoded];
29
+ })
30
+ .sort((left, right) => right.length - left.length);
31
+ for (const value of new Set(sensitiveValues)) {
32
+ output = output.split(value).join(REDACTED_VALUE);
33
+ }
34
+ if (options.redactUrls === true) {
35
+ output = output.replace(/https?:\/\/[^\s<>"')\]}]+/gi, REDACTED_URL);
36
+ }
37
+ output = [...output]
38
+ .map((character) => {
39
+ const codePoint = character.codePointAt(0) ?? 0;
40
+ return codePoint <= 8 ||
41
+ codePoint === 11 ||
42
+ codePoint === 12 ||
43
+ (codePoint >= 14 && codePoint <= 31) ||
44
+ codePoint === 127
45
+ ? " "
46
+ : character;
47
+ })
48
+ .join("")
49
+ .trim();
50
+ const maxLength = options.maxLength;
51
+ if (maxLength !== undefined &&
52
+ Number.isSafeInteger(maxLength) &&
53
+ maxLength > 0 &&
54
+ output.length > maxLength) {
55
+ return `${output.slice(0, Math.max(0, maxLength - 1))}…`;
56
+ }
57
+ return output;
58
+ }
59
+ export function redactUnknown(value, options = {}) {
60
+ try {
61
+ return redactUnknownInternal(value, options, new WeakSet(), 0);
62
+ }
63
+ catch {
64
+ return "[REDACTED_OBJECT]";
65
+ }
66
+ }
67
+ export function toRedactedDiagnostic(error, options = {}) {
68
+ try {
69
+ return toRedactedDiagnosticInternal(error, options);
70
+ }
71
+ catch {
72
+ return {
73
+ name: "UnknownError",
74
+ message: "An unexpected value could not be inspected safely.",
75
+ };
76
+ }
77
+ }
78
+ function toRedactedDiagnosticInternal(error, options) {
79
+ if (!(error instanceof Error)) {
80
+ return {
81
+ name: "UnknownError",
82
+ message: redactText("An unexpected non-Error value was thrown.", {
83
+ ...options,
84
+ maxLength: 2_000,
85
+ }),
86
+ };
87
+ }
88
+ const name = redactText(error.name || "Error", {
89
+ ...options,
90
+ maxLength: 200,
91
+ });
92
+ const message = redactText(error.message || "No error message was provided.", {
93
+ ...options,
94
+ redactUrls: true,
95
+ maxLength: 2_000,
96
+ });
97
+ const stack = error.stack === undefined
98
+ ? undefined
99
+ : redactText(error.stack, {
100
+ ...options,
101
+ redactUrls: true,
102
+ maxLength: 8_000,
103
+ });
104
+ return {
105
+ name,
106
+ message,
107
+ ...(stack === undefined ? {} : { stack }),
108
+ };
109
+ }
110
+ function redactUnknownInternal(value, options, seen, depth) {
111
+ if (typeof value === "string")
112
+ return redactText(value, options);
113
+ if (value === null ||
114
+ typeof value === "number" ||
115
+ typeof value === "boolean" ||
116
+ typeof value === "undefined") {
117
+ return value;
118
+ }
119
+ if (typeof value === "bigint" || typeof value === "symbol") {
120
+ return String(value);
121
+ }
122
+ if (typeof value === "function")
123
+ return "[REDACTED_FUNCTION]";
124
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) {
125
+ return "[REDACTED_BINARY]";
126
+ }
127
+ if (value instanceof Error)
128
+ return toRedactedDiagnostic(value, options);
129
+ if (depth >= 5)
130
+ return "[REDACTED_DEPTH_LIMIT]";
131
+ if (typeof value !== "object")
132
+ return "[REDACTED_UNKNOWN]";
133
+ if (seen.has(value))
134
+ return "[REDACTED_CIRCULAR]";
135
+ seen.add(value);
136
+ if (Array.isArray(value)) {
137
+ return value
138
+ .slice(0, 50)
139
+ .map((item) => redactUnknownInternal(item, options, seen, depth + 1));
140
+ }
141
+ let prototype;
142
+ let descriptors;
143
+ try {
144
+ prototype = Object.getPrototypeOf(value);
145
+ descriptors = Object.getOwnPropertyDescriptors(value);
146
+ }
147
+ catch {
148
+ return "[REDACTED_OBJECT]";
149
+ }
150
+ if (prototype !== Object.prototype && prototype !== null) {
151
+ return "[REDACTED_OBJECT]";
152
+ }
153
+ const output = {};
154
+ for (const [key, descriptor] of Object.entries(descriptors).slice(0, 50)) {
155
+ const item = "value" in descriptor
156
+ ? descriptor.value
157
+ : "[REDACTED_ACCESSOR]";
158
+ output[key] = isSensitiveFieldName(key)
159
+ ? REDACTED_VALUE
160
+ : redactUnknownInternal(item, options, seen, depth + 1);
161
+ }
162
+ return output;
163
+ }
164
+ function isSensitiveFieldName(value) {
165
+ const normalized = value.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
166
+ return (sensitiveFieldNames.has(normalized) ||
167
+ normalized.includes("accesskey") ||
168
+ normalized.includes("authorization") ||
169
+ normalized.includes("cookie") ||
170
+ normalized.includes("secretkey") ||
171
+ normalized.includes("signature") ||
172
+ normalized.includes("screenshoturl") ||
173
+ normalized.includes("targeturl") ||
174
+ normalized.endsWith("body") ||
175
+ normalized.endsWith("bytes") ||
176
+ normalized.endsWith("headers"));
177
+ }
178
+ function safelyEncodeURIComponent(value) {
179
+ try {
180
+ return encodeURIComponent(value);
181
+ }
182
+ catch {
183
+ return undefined;
184
+ }
185
+ }
@@ -0,0 +1,43 @@
1
+ import type { CallToolResult, ContentBlock } from "@modelcontextprotocol/sdk/types.js";
2
+ import { z } from "zod";
3
+ export declare const captureResultMetadataSchema: z.ZodObject<{
4
+ screenshotUrl: z.ZodOptional<z.ZodString>;
5
+ screenshotUrlExpiresAt: z.ZodOptional<z.ZodString>;
6
+ cacheStatus: z.ZodOptional<z.ZodString>;
7
+ format: z.ZodOptional<z.ZodEnum<{
8
+ png: "png";
9
+ jpg: "jpg";
10
+ jpeg: "jpeg";
11
+ webp: "webp";
12
+ gif: "gif";
13
+ tiff: "tiff";
14
+ pdf: "pdf";
15
+ }>>;
16
+ mimeType: z.ZodOptional<z.ZodString>;
17
+ imageWidth: z.ZodOptional<z.ZodNumber>;
18
+ imageHeight: z.ZodOptional<z.ZodNumber>;
19
+ inlineImageIncluded: z.ZodOptional<z.ZodBoolean>;
20
+ inlineImageOmissionReason: z.ZodOptional<z.ZodEnum<{
21
+ result_mode_url_only: "result_mode_url_only";
22
+ format_not_inline_eligible: "format_not_inline_eligible";
23
+ unsupported_mime_type: "unsupported_mime_type";
24
+ mime_type_mismatch: "mime_type_mismatch";
25
+ raw_size_exceeds_limit: "raw_size_exceeds_limit";
26
+ dimensions_unavailable: "dimensions_unavailable";
27
+ dimensions_exceed_limit: "dimensions_exceed_limit";
28
+ client_not_compatible: "client_not_compatible";
29
+ serialized_size_unavailable: "serialized_size_unavailable";
30
+ serialized_size_exceeds_limit: "serialized_size_exceeds_limit";
31
+ }>>;
32
+ }, z.core.$strict>;
33
+ export type CaptureResultMetadata = z.infer<typeof captureResultMetadataSchema>;
34
+ export type CaptureSuccessFormattingInput = {
35
+ readonly metadata: CaptureResultMetadata;
36
+ readonly additionalContent?: readonly ContentBlock[];
37
+ readonly sensitiveValues?: readonly (string | null | undefined)[];
38
+ };
39
+ export declare function formatCaptureSuccess(input: CaptureSuccessFormattingInput): CallToolResult;
40
+ export declare function assertSafeScreenshotUrl(value: string, sensitiveValues?: readonly (string | null | undefined)[]): void;
41
+ export declare class UnsafeCaptureResultError extends Error {
42
+ constructor(message: string);
43
+ }