@mehmoodqureshi/chrome-mcp 0.3.0 → 0.4.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.
@@ -26,6 +26,8 @@ const types_1 = require("./types");
26
26
  const SINGLETON_FILES = ['SingletonLock', 'SingletonSocket', 'SingletonCookie'];
27
27
  const STEALTH = `Object.defineProperty(navigator,'webdriver',{get:()=>undefined});`;
28
28
  const NON_CONTENT = /^(file|data|devtools|chrome|about):/i;
29
+ /** Playwright errors that mean the page/context/browser died under us → DETACHED. */
30
+ const CLOSED_TARGET = /Target closed|Browser has been closed|Target page, context or browser has been closed|Execution context was destroyed/i;
29
31
  /** Minimal CSS attribute-value escape for ref selectors (refs are `e\d+`, but be safe). */
30
32
  function cssEscape(s) {
31
33
  return s.replace(/["\\]/g, '\\$&');
@@ -250,6 +252,26 @@ class CdpExecutor {
250
252
  }
251
253
  throw new types_1.ExecutorError('SELECTOR_NOT_FOUND', 'provide a selector or a ref from snapshot()');
252
254
  }
255
+ /**
256
+ * Run a live-page operation, converting an opaque "target closed"/crashed
257
+ * error from Playwright into a clean DETACHED. On such a failure we also drop
258
+ * the cached context/tabs so the next call relaunches or reconnects cleanly.
259
+ * Any other error is rethrown unchanged.
260
+ */
261
+ async guard(fn) {
262
+ try {
263
+ return await fn();
264
+ }
265
+ catch (err) {
266
+ const msg = err.message ?? '';
267
+ if (CLOSED_TARGET.test(msg)) {
268
+ this.context = null;
269
+ this.tabs.clear();
270
+ throw new types_1.ExecutorError('DETACHED', 'browser target closed or crashed; reconnect and retry');
271
+ }
272
+ throw err;
273
+ }
274
+ }
253
275
  // -- tabs ---------------------------------------------------------------
254
276
  async tabsList() {
255
277
  const ctx = await this.getContext();
@@ -284,107 +306,135 @@ class CdpExecutor {
284
306
  return w ?? 'load';
285
307
  }
286
308
  async navigate(args) {
287
- const p = await this.resolveTab(args.tabId);
288
- const resp = await p.goto(args.url, { waitUntil: this.toState(args.waitUntil) });
289
- return { url: p.url(), title: await p.title().catch(() => ''), httpStatus: resp?.status() };
309
+ return this.guard(async () => {
310
+ const p = await this.resolveTab(args.tabId);
311
+ const resp = await p.goto(args.url, { waitUntil: this.toState(args.waitUntil) });
312
+ return { url: p.url(), title: await p.title().catch(() => ''), httpStatus: resp?.status() };
313
+ });
290
314
  }
291
315
  async back(tabId) {
292
- const p = await this.resolveTab(tabId);
293
- await p.goBack().catch(() => undefined);
294
- return { url: p.url(), title: await p.title().catch(() => '') };
316
+ return this.guard(async () => {
317
+ const p = await this.resolveTab(tabId);
318
+ await p.goBack().catch(() => undefined);
319
+ return { url: p.url(), title: await p.title().catch(() => '') };
320
+ });
295
321
  }
296
322
  async forward(tabId) {
297
- const p = await this.resolveTab(tabId);
298
- await p.goForward().catch(() => undefined);
299
- return { url: p.url(), title: await p.title().catch(() => '') };
323
+ return this.guard(async () => {
324
+ const p = await this.resolveTab(tabId);
325
+ await p.goForward().catch(() => undefined);
326
+ return { url: p.url(), title: await p.title().catch(() => '') };
327
+ });
300
328
  }
301
329
  async reload(args) {
302
- const p = await this.resolveTab(args?.tabId);
303
- await p.reload({ waitUntil: this.toState(args?.waitUntil) });
304
- return { url: p.url(), title: await p.title().catch(() => '') };
330
+ return this.guard(async () => {
331
+ const p = await this.resolveTab(args?.tabId);
332
+ await p.reload({ waitUntil: this.toState(args?.waitUntil) });
333
+ return { url: p.url(), title: await p.title().catch(() => '') };
334
+ });
305
335
  }
306
336
  // -- interaction --------------------------------------------------------
307
337
  ok = { ok: true };
308
338
  async click(t, opts) {
309
- const p = await this.resolveTab(opts?.tabId);
310
- // Playwright already drives real (trusted) input, so `trusted` is a no-op here.
311
- await this.locator(p, t).click({ button: opts?.button, clickCount: opts?.clickCount });
312
- return this.ok;
339
+ return this.guard(async () => {
340
+ const p = await this.resolveTab(opts?.tabId);
341
+ // Playwright already drives real (trusted) input, so `trusted` is a no-op here.
342
+ await this.locator(p, t).click({ button: opts?.button, clickCount: opts?.clickCount });
343
+ return this.ok;
344
+ });
313
345
  }
314
346
  async type(t, text, opts) {
315
- const p = await this.resolveTab(opts?.tabId);
316
- const loc = this.locator(p, t);
317
- if (opts?.clear)
318
- await loc.fill('');
319
- if (opts?.keyEvents)
320
- await loc.pressSequentially(text);
321
- else
322
- await loc.fill((opts?.clear ? '' : '') + text);
323
- if (opts?.pressEnter)
324
- await loc.press('Enter');
325
- return this.ok;
347
+ return this.guard(async () => {
348
+ const p = await this.resolveTab(opts?.tabId);
349
+ const loc = this.locator(p, t);
350
+ if (opts?.clear)
351
+ await loc.fill('');
352
+ if (opts?.keyEvents)
353
+ await loc.pressSequentially(text);
354
+ else
355
+ await loc.fill(text);
356
+ if (opts?.pressEnter)
357
+ await loc.press('Enter');
358
+ return this.ok;
359
+ });
326
360
  }
327
361
  async selectOption(t, values, opts) {
328
- const p = await this.resolveTab(opts?.tabId);
329
- // Try by value, then fall back to visible label.
330
- const loc = this.locator(p, t);
331
- try {
332
- await loc.selectOption(values);
333
- }
334
- catch {
335
- await loc.selectOption(values.map((label) => ({ label })));
336
- }
337
- return this.ok;
362
+ return this.guard(async () => {
363
+ const p = await this.resolveTab(opts?.tabId);
364
+ // Try by value, then fall back to visible label.
365
+ const loc = this.locator(p, t);
366
+ try {
367
+ await loc.selectOption(values);
368
+ }
369
+ catch {
370
+ await loc.selectOption(values.map((label) => ({ label })));
371
+ }
372
+ return this.ok;
373
+ });
338
374
  }
339
375
  async fill(t, value, opts) {
340
- const p = await this.resolveTab(opts?.tabId);
341
- await this.locator(p, t).fill(value);
342
- return this.ok;
376
+ return this.guard(async () => {
377
+ const p = await this.resolveTab(opts?.tabId);
378
+ await this.locator(p, t).fill(value);
379
+ return this.ok;
380
+ });
343
381
  }
344
382
  async press(key, opts) {
345
- const p = await this.resolveTab(opts?.tabId);
346
- const combo = [...(opts?.modifiers ?? []), key].join('+');
347
- await p.keyboard.press(combo);
348
- return this.ok;
383
+ return this.guard(async () => {
384
+ const p = await this.resolveTab(opts?.tabId);
385
+ const combo = [...(opts?.modifiers ?? []), key].join('+');
386
+ await p.keyboard.press(combo);
387
+ return this.ok;
388
+ });
349
389
  }
350
390
  async hover(t, opts) {
351
- const p = await this.resolveTab(opts?.tabId);
352
- await this.locator(p, t).hover();
353
- return this.ok;
391
+ return this.guard(async () => {
392
+ const p = await this.resolveTab(opts?.tabId);
393
+ await this.locator(p, t).hover();
394
+ return this.ok;
395
+ });
354
396
  }
355
397
  async scroll(opts) {
356
- const p = await this.resolveTab(opts.tabId);
357
- if (opts.target)
358
- await this.locator(p, opts.target).scrollIntoViewIfNeeded();
359
- else if (opts.x !== undefined || opts.y !== undefined)
360
- await p.evaluate(([x, y]) => window.scrollTo(x ?? 0, y ?? 0), [opts.x, opts.y]);
361
- else
362
- await p.mouse.wheel(opts.deltaX ?? 0, opts.deltaY ?? 0);
363
- return this.ok;
398
+ return this.guard(async () => {
399
+ const p = await this.resolveTab(opts.tabId);
400
+ if (opts.target)
401
+ await this.locator(p, opts.target).scrollIntoViewIfNeeded();
402
+ else if (opts.x !== undefined || opts.y !== undefined)
403
+ await p.evaluate(([x, y]) => window.scrollTo(x ?? 0, y ?? 0), [opts.x, opts.y]);
404
+ else
405
+ await p.mouse.wheel(opts.deltaX ?? 0, opts.deltaY ?? 0);
406
+ return this.ok;
407
+ });
364
408
  }
365
409
  // -- read ---------------------------------------------------------------
366
410
  async getText(t, opts) {
367
- const p = await this.resolveTab(opts?.tabId);
368
- const text = t ? await this.locator(p, t).innerText() : await p.locator('body').innerText();
369
- return { text };
411
+ return this.guard(async () => {
412
+ const p = await this.resolveTab(opts?.tabId);
413
+ const text = t ? await this.locator(p, t).innerText() : await p.locator('body').innerText();
414
+ return { text };
415
+ });
370
416
  }
371
417
  async getHtml(t, opts) {
372
- const p = await this.resolveTab(opts?.tabId);
373
- if (!t)
374
- return { html: await p.content() };
375
- const loc = this.locator(p, t);
376
- const html = opts?.outer ? await loc.evaluate((el) => el.outerHTML) : await loc.innerHTML();
377
- return { html };
418
+ return this.guard(async () => {
419
+ const p = await this.resolveTab(opts?.tabId);
420
+ if (!t)
421
+ return { html: await p.content() };
422
+ const loc = this.locator(p, t);
423
+ const html = opts?.outer ? await loc.evaluate((el) => el.outerHTML) : await loc.innerHTML();
424
+ return { html };
425
+ });
378
426
  }
379
427
  async snapshot(opts) {
380
- const p = await this.resolveTab(opts?.tabId);
381
- // Inject collectSnapshot's source and run it in the page (it can't close over module scope).
382
- const raw = await p.evaluate(([fnSrc, interactiveOnly, max]) => {
383
- // eslint-disable-next-line no-eval
384
- const fn = (0, eval)(`(${fnSrc})`);
385
- return fn(interactiveOnly, max);
386
- }, [snapshot_1.collectSnapshot.toString(), opts?.interactiveOnly ?? true, opts?.max ?? 200]);
387
- return raw;
428
+ return this.guard(async () => {
429
+ const p = await this.resolveTab(opts?.tabId);
430
+ // Inject collectSnapshot's source and run it in the page (it can't close over module scope).
431
+ const raw = await p.evaluate(([fnSrc, interactiveOnly, max]) => {
432
+ // eslint-disable-next-line no-eval
433
+ const fn = (0, eval)(`(${fnSrc})`);
434
+ return fn(interactiveOnly, max);
435
+ }, [snapshot_1.collectSnapshot.toString(), opts?.interactiveOnly ?? true, opts?.max ?? 200]);
436
+ return raw;
437
+ });
388
438
  }
389
439
  async getCookies(opts) {
390
440
  const p = await this.resolveTab(opts?.tabId);
@@ -399,69 +449,85 @@ class CdpExecutor {
399
449
  };
400
450
  }
401
451
  async storage(args) {
402
- const p = await this.resolveTab(args.tabId);
403
- return p.evaluate((a) => {
404
- const store = a.session ? window.sessionStorage : window.localStorage;
405
- if (a.op === 'set') {
406
- store.setItem(String(a.key), String(a.value ?? ''));
407
- return { ok: true };
408
- }
409
- if (a.op === 'remove') {
410
- store.removeItem(String(a.key));
411
- return { ok: true };
412
- }
413
- if (a.op === 'clear') {
414
- store.clear();
415
- return { ok: true };
416
- }
417
- if (a.key)
418
- return { ok: true, value: store.getItem(a.key) };
419
- const entries = {};
420
- for (let i = 0; i < store.length; i++) {
421
- const k = store.key(i);
422
- if (k)
423
- entries[k] = store.getItem(k) ?? '';
424
- }
425
- return { ok: true, entries };
426
- }, args);
452
+ return this.guard(async () => {
453
+ const p = await this.resolveTab(args.tabId);
454
+ return p.evaluate((a) => {
455
+ const store = a.session ? window.sessionStorage : window.localStorage;
456
+ if (a.op === 'set') {
457
+ store.setItem(String(a.key), String(a.value ?? ''));
458
+ return { ok: true };
459
+ }
460
+ if (a.op === 'remove') {
461
+ store.removeItem(String(a.key));
462
+ return { ok: true };
463
+ }
464
+ if (a.op === 'clear') {
465
+ store.clear();
466
+ return { ok: true };
467
+ }
468
+ if (a.key)
469
+ return { ok: true, value: store.getItem(a.key) };
470
+ const entries = {};
471
+ for (let i = 0; i < store.length; i++) {
472
+ const k = store.key(i);
473
+ if (k)
474
+ entries[k] = store.getItem(k) ?? '';
475
+ }
476
+ return { ok: true, entries };
477
+ }, args);
478
+ });
427
479
  }
428
480
  async screenshot(opts) {
429
- const p = await this.resolveTab(opts?.tabId);
430
- const buf = opts?.target
431
- ? await this.locator(p, opts.target).screenshot()
432
- : await p.screenshot({ fullPage: opts?.fullPage });
433
- const size = p.viewportSize() ?? { width: 0, height: 0 };
434
- return { dataBase64: buf.toString('base64'), mimeType: 'image/png', width: size.width, height: size.height, truncated: false };
481
+ return this.guard(async () => {
482
+ const p = await this.resolveTab(opts?.tabId);
483
+ const buf = opts?.target
484
+ ? await this.locator(p, opts.target).screenshot()
485
+ : await p.screenshot({ fullPage: opts?.fullPage });
486
+ const size = p.viewportSize() ?? { width: 0, height: 0 };
487
+ return { dataBase64: buf.toString('base64'), mimeType: 'image/png', width: size.width, height: size.height, truncated: false };
488
+ });
435
489
  }
436
490
  async eval(expression, opts) {
437
- const p = await this.resolveTab(opts?.tabId);
438
- try {
439
- const value = await p.evaluate((expr) => {
440
- // eslint-disable-next-line no-eval
441
- return (0, eval)(expr);
442
- }, expression);
443
- return { ok: true, value, type: typeof value };
444
- }
445
- catch (err) {
446
- return { ok: false, error: err.message };
447
- }
491
+ return this.guard(async () => {
492
+ const p = await this.resolveTab(opts?.tabId);
493
+ try {
494
+ const value = await p.evaluate((expr) => {
495
+ // eslint-disable-next-line no-eval
496
+ return (0, eval)(expr);
497
+ }, expression);
498
+ return (0, types_1.truncateEvalResult)({ ok: true, value, type: typeof value });
499
+ }
500
+ catch (err) {
501
+ const msg = err.message ?? '';
502
+ // A dead/crashed target must surface as DETACHED, not a swallowed EvalResult error.
503
+ if (CLOSED_TARGET.test(msg))
504
+ throw err;
505
+ return { ok: false, error: msg };
506
+ }
507
+ });
448
508
  }
449
509
  async waitFor(opts) {
450
- const p = await this.resolveTab(opts.tabId);
451
- const timeout = opts.timeoutMs ?? 30_000;
452
- const start = Date.now();
453
- try {
454
- if (opts.selector) {
455
- await p.locator(opts.selector).first().waitFor({ state: opts.gone ? 'detached' : 'visible', timeout });
510
+ return this.guard(async () => {
511
+ const p = await this.resolveTab(opts.tabId);
512
+ const timeout = opts.timeoutMs ?? 30_000;
513
+ const start = Date.now();
514
+ try {
515
+ if (opts.selector) {
516
+ await p.locator(opts.selector).first().waitFor({ state: opts.gone ? 'detached' : 'visible', timeout });
517
+ }
518
+ else if (opts.textContains) {
519
+ await p.waitForFunction((needle) => document.body?.innerText.includes(needle) ?? false, opts.textContains, { timeout });
520
+ }
521
+ return { matched: true, waitedMs: Date.now() - start };
456
522
  }
457
- else if (opts.textContains) {
458
- await p.waitForFunction((needle) => document.body?.innerText.includes(needle) ?? false, opts.textContains, { timeout });
523
+ catch (err) {
524
+ const msg = err.message ?? '';
525
+ // Crashed target must escalate to DETACHED; a plain timeout stays a non-match.
526
+ if (CLOSED_TARGET.test(msg))
527
+ throw err;
528
+ return { matched: false, waitedMs: Date.now() - start };
459
529
  }
460
- return { matched: true, waitedMs: Date.now() - start };
461
- }
462
- catch {
463
- return { matched: false, waitedMs: Date.now() - start };
464
- }
530
+ });
465
531
  }
466
532
  // -- privileged ---------------------------------------------------------
467
533
  async download(args) {
@@ -485,8 +551,43 @@ class CdpExecutor {
485
551
  if (!target)
486
552
  throw new types_1.ExecutorError('DOWNLOAD_FAILED', 'provide a url or a target');
487
553
  const [dl] = await Promise.all([p.waitForEvent('download'), this.locator(p, target).click()]);
488
- await dl.saveAs(dest);
489
- return { path: dest, backend: this.backend, bytes: 0, suggestedName: dl.suggestedFilename() };
554
+ let bytes = 0;
555
+ try {
556
+ // Surface a cancelled/interrupted download instead of reporting a phantom success.
557
+ const failure = await dl.failure();
558
+ if (failure)
559
+ throw new types_1.ExecutorError('DOWNLOAD_FAILED', `download failed: ${failure}`);
560
+ await dl.saveAs(dest);
561
+ bytes = (0, node_fs_1.statSync)(dest).size;
562
+ }
563
+ catch (err) {
564
+ if (err instanceof types_1.ExecutorError)
565
+ throw err;
566
+ throw new types_1.ExecutorError('DOWNLOAD_FAILED', `failed to save download: ${err.message}`);
567
+ }
568
+ return { path: dest, backend: this.backend, bytes, suggestedName: dl.suggestedFilename() };
569
+ }
570
+ async uploadFile(t, files, opts) {
571
+ return this.guard(async () => {
572
+ for (const f of files) {
573
+ try {
574
+ (0, node_fs_1.statSync)(f);
575
+ }
576
+ catch {
577
+ throw new types_1.ExecutorError('UPLOAD_FAILED', `file not found: ${f}`);
578
+ }
579
+ }
580
+ const p = await this.resolveTab(opts?.tabId);
581
+ try {
582
+ await this.locator(p, t).setInputFiles(files);
583
+ }
584
+ catch (err) {
585
+ if (err instanceof types_1.ExecutorError)
586
+ throw err;
587
+ throw new types_1.ExecutorError('UPLOAD_FAILED', `could not set files on the input: ${err.message}`);
588
+ }
589
+ return this.ok;
590
+ });
490
591
  }
491
592
  }
492
593
  exports.CdpExecutor = CdpExecutor;
@@ -122,4 +122,7 @@ export declare class ExtensionExecutor implements Executor {
122
122
  tabId?: TabId;
123
123
  suggestedName?: string;
124
124
  }): Promise<DownloadResult>;
125
+ uploadFile(t: Target, files: string[], opts?: {
126
+ tabId?: TabId;
127
+ }): Promise<ActionOk>;
125
128
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.ExtensionExecutor = void 0;
13
+ const types_1 = require("./types");
13
14
  /** Flatten a Target into the params a wire command carries. */
14
15
  function targetParams(t) {
15
16
  if (!t)
@@ -122,7 +123,8 @@ class ExtensionExecutor {
122
123
  return (await this.send('screenshot', { fullPage: opts?.fullPage, ...targetParams(opts?.target) }, { tabId: opts?.tabId }));
123
124
  }
124
125
  async eval(expression, opts) {
125
- return (await this.send('eval', { expression, awaitPromise: opts?.awaitPromise }, { tabId: opts?.tabId }));
126
+ const result = (await this.send('eval', { expression, awaitPromise: opts?.awaitPromise }, { tabId: opts?.tabId }));
127
+ return (0, types_1.truncateEvalResult)(result);
126
128
  }
127
129
  async waitFor(opts) {
128
130
  return (await this.send('wait_for', { selector: opts.selector, textContains: opts.textContains, gone: opts.gone, timeoutMs: opts.timeoutMs }, { tabId: opts.tabId, timeoutMs: opts.timeoutMs ? opts.timeoutMs + 5_000 : undefined }));
@@ -131,6 +133,9 @@ class ExtensionExecutor {
131
133
  async download(args) {
132
134
  return (await this.send('download_file', { url: args.url, ...targetParams(args.target), suggestedName: args.suggestedName }, { tabId: args.tabId }));
133
135
  }
136
+ async uploadFile(t, files, opts) {
137
+ return (await this.send('upload_file', { ...targetParams(t), files }, { tabId: opts?.tabId }));
138
+ }
134
139
  }
135
140
  exports.ExtensionExecutor = ExtensionExecutor;
136
141
  //# sourceMappingURL=extension-executor.js.map
@@ -66,4 +66,5 @@ export declare class StubExecutor implements Executor {
66
66
  download(args: {
67
67
  suggestedName?: string;
68
68
  }): Promise<DownloadResult>;
69
+ uploadFile(): Promise<ActionOk>;
69
70
  }
@@ -132,6 +132,9 @@ class StubExecutor {
132
132
  bytes: 0,
133
133
  };
134
134
  }
135
+ async uploadFile() {
136
+ return ok;
137
+ }
135
138
  }
136
139
  exports.StubExecutor = StubExecutor;
137
140
  //# sourceMappingURL=stub-executor.js.map
@@ -50,7 +50,19 @@ export interface EvalResult {
50
50
  value?: unknown;
51
51
  type?: string;
52
52
  error?: string;
53
+ /** Set when `value` exceeded MAX_EVAL_BYTES and was replaced by a truncated JSON string. */
54
+ truncated?: boolean;
53
55
  }
56
+ /** Hard cap on a serialized eval result before it is truncated (see `truncateEvalResult`). */
57
+ export declare const MAX_EVAL_BYTES: number;
58
+ /**
59
+ * Enforce the EvalResult size cap uniformly across backends. Attempts to
60
+ * JSON-serialize `value`; if the UTF-8 byte length exceeds MAX_EVAL_BYTES the
61
+ * value is replaced by the JSON sliced to the cap with a `...[truncated]`
62
+ * marker and `truncated: true` is set. Non-serializable values (stringify
63
+ * throws or yields undefined) are left untouched — this never throws.
64
+ */
65
+ export declare function truncateEvalResult(result: EvalResult): EvalResult;
54
66
  export interface WaitResult {
55
67
  matched: boolean;
56
68
  ref?: string;
@@ -239,13 +251,22 @@ export interface Executor {
239
251
  tabId?: TabId;
240
252
  suggestedName?: string;
241
253
  }): Promise<DownloadResult>;
254
+ /**
255
+ * Set the files on a file `<input>` (target by selector or ref) from local
256
+ * absolute paths — the upload equivalent of a file-picker, without the OS
257
+ * dialog. Privileged: sends local files to the page, so it is gated by
258
+ * `allowUploads` and the destination domain allowlist.
259
+ */
260
+ uploadFile(t: Target, files: string[], opts?: {
261
+ tabId?: TabId;
262
+ }): Promise<ActionOk>;
242
263
  }
243
264
  /**
244
265
  * Server-side executor error codes. A superset of the wire `ExecutorErrorCode`
245
266
  * (which only carries codes that originate inside the extension); these extra
246
267
  * codes describe failures on the server half (no backend, launch failed, etc.).
247
268
  */
248
- export type ExecutorErrorCodeLocal = 'NO_BACKEND' | 'EXTENSION_DISCONNECTED' | 'TIMEOUT' | 'TAB_NOT_FOUND' | 'STALE_TAB' | 'SELECTOR_NOT_FOUND' | 'REF_EXPIRED' | 'EVAL_FAILED' | 'LAUNCH_FAILED' | 'DETACHED' | 'TARGET_GONE' | 'POLICY_DENIED' | 'DEVTOOLS_OPEN' | 'DOWNLOAD_FAILED' | 'BACKPRESSURE';
269
+ export type ExecutorErrorCodeLocal = 'NO_BACKEND' | 'EXTENSION_DISCONNECTED' | 'TIMEOUT' | 'TAB_NOT_FOUND' | 'STALE_TAB' | 'SELECTOR_NOT_FOUND' | 'REF_EXPIRED' | 'EVAL_FAILED' | 'LAUNCH_FAILED' | 'DETACHED' | 'TARGET_GONE' | 'POLICY_DENIED' | 'DEVTOOLS_OPEN' | 'DOWNLOAD_FAILED' | 'UPLOAD_FAILED' | 'BACKPRESSURE';
249
270
  export declare class ExecutorError extends Error {
250
271
  readonly code: ExecutorErrorCodeLocal;
251
272
  constructor(code: ExecutorErrorCodeLocal, message: string);
@@ -11,7 +11,35 @@
11
11
  * `mcp/helpers.ts` from these primitives; only `download` is privileged.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.ExecutorError = void 0;
14
+ exports.ExecutorError = exports.MAX_EVAL_BYTES = void 0;
15
+ exports.truncateEvalResult = truncateEvalResult;
16
+ /** Hard cap on a serialized eval result before it is truncated (see `truncateEvalResult`). */
17
+ exports.MAX_EVAL_BYTES = 256 * 1024;
18
+ /**
19
+ * Enforce the EvalResult size cap uniformly across backends. Attempts to
20
+ * JSON-serialize `value`; if the UTF-8 byte length exceeds MAX_EVAL_BYTES the
21
+ * value is replaced by the JSON sliced to the cap with a `...[truncated]`
22
+ * marker and `truncated: true` is set. Non-serializable values (stringify
23
+ * throws or yields undefined) are left untouched — this never throws.
24
+ */
25
+ function truncateEvalResult(result) {
26
+ if (!result.ok || result.value === undefined)
27
+ return result;
28
+ let json;
29
+ try {
30
+ json = JSON.stringify(result.value);
31
+ }
32
+ catch {
33
+ return result; // not serializable — leave value as-is
34
+ }
35
+ if (json === undefined)
36
+ return result;
37
+ if (Buffer.byteLength(json, 'utf8') <= exports.MAX_EVAL_BYTES)
38
+ return result;
39
+ // Slice by bytes, then trim any partial trailing UTF-8 char before appending the marker.
40
+ const sliced = Buffer.from(json, 'utf8').subarray(0, exports.MAX_EVAL_BYTES).toString('utf8');
41
+ return { ...result, value: `${sliced}...[truncated]`, truncated: true };
42
+ }
15
43
  class ExecutorError extends Error {
16
44
  code;
17
45
  constructor(code, message) {
@@ -10,9 +10,9 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
10
  /** stderr only — never stdout in stdio mode. */
11
11
  export declare function logErr(message: string): void;
12
12
  /** Build a fresh `Server` with the full tool surface registered (no transport). */
13
- export declare function createServer(): Server;
13
+ export declare function createServer(version?: string): Server;
14
14
  /** Start over stdio. Idempotent. */
15
- export declare function startMcpServer(): Promise<void>;
15
+ export declare function startMcpServer(version?: string): Promise<void>;
16
16
  /** Stop and release the transport. Idempotent, best-effort. */
17
17
  export declare function stopMcpServer(): Promise<void>;
18
18
  export declare function isMcpServerRunning(): boolean;
@@ -18,6 +18,8 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
18
18
  const tools_1 = require("./tools");
19
19
  const SERVER_NAME = 'chrome-mcp';
20
20
  const SERVER_VERSION = '0.1.0';
21
+ /** Default version reported when no explicit version is passed in (legacy callers/tests). */
22
+ const DEFAULT_VERSION = SERVER_VERSION;
21
23
  let server = null;
22
24
  let transport = null;
23
25
  /** stderr only — never stdout in stdio mode. */
@@ -25,8 +27,8 @@ function logErr(message) {
25
27
  process.stderr.write(`[chrome-mcp] ${message}\n`);
26
28
  }
27
29
  /** Build a fresh `Server` with the full tool surface registered (no transport). */
28
- function createServer() {
29
- const srv = new index_js_1.Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
30
+ function createServer(version = DEFAULT_VERSION) {
31
+ const srv = new index_js_1.Server({ name: SERVER_NAME, version }, { capabilities: { tools: {} } });
30
32
  (0, tools_1.registerTools)(srv);
31
33
  srv.onerror = (err) => {
32
34
  logErr(`server error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
@@ -34,12 +36,12 @@ function createServer() {
34
36
  return srv;
35
37
  }
36
38
  /** Start over stdio. Idempotent. */
37
- async function startMcpServer() {
39
+ async function startMcpServer(version = DEFAULT_VERSION) {
38
40
  if (server) {
39
41
  logErr('startMcpServer called but already running; ignoring.');
40
42
  return;
41
43
  }
42
- const srv = createServer();
44
+ const srv = createServer(version);
43
45
  const tx = new stdio_js_1.StdioServerTransport();
44
46
  try {
45
47
  await srv.connect(tx);
@@ -52,7 +54,7 @@ async function startMcpServer() {
52
54
  }
53
55
  server = srv;
54
56
  transport = tx;
55
- logErr(`${SERVER_NAME} v${SERVER_VERSION} connected over stdio.`);
57
+ logErr(`${SERVER_NAME} v${version} connected over stdio.`);
56
58
  }
57
59
  /** Stop and release the transport. Idempotent, best-effort. */
58
60
  async function stopMcpServer() {
@@ -25,6 +25,8 @@ interface ToolCtx {
25
25
  }
26
26
  type ToolHandler = (args: Record<string, unknown>, ctx: ToolCtx) => Promise<CallToolResult>;
27
27
  export declare const TOOL_HANDLERS: Record<string, ToolHandler>;
28
+ /** Reset limiter state — for tests that exercise the ceiling. */
29
+ export declare function resetRateLimiter(): void;
28
30
  export declare function dispatchToolCall(name: string, rawArgs: unknown): Promise<CallToolResult>;
29
31
  /** Assert the catalog and the dispatch table describe the same tool set. */
30
32
  export declare function assertNoDrift(): void;