@mehmoodqureshi/chrome-mcp 0.3.0 → 0.4.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/README.md +11 -4
- package/dist/shared/protocol.d.ts +2 -2
- package/dist/shared/protocol.js +1 -0
- package/dist/shared/snapshot.js +64 -2
- package/dist/src/bridge/server.js +17 -1
- package/dist/src/cli.js +24 -3
- package/dist/src/config.js +13 -0
- package/dist/src/executor/cdp-executor.d.ts +10 -0
- package/dist/src/executor/cdp-executor.js +230 -129
- package/dist/src/executor/extension-executor.d.ts +3 -0
- package/dist/src/executor/extension-executor.js +6 -1
- package/dist/src/executor/stub-executor.d.ts +1 -0
- package/dist/src/executor/stub-executor.js +3 -0
- package/dist/src/executor/types.d.ts +22 -1
- package/dist/src/executor/types.js +29 -1
- package/dist/src/mcp/server.d.ts +2 -2
- package/dist/src/mcp/server.js +7 -5
- package/dist/src/mcp/tools.d.ts +2 -0
- package/dist/src/mcp/tools.js +61 -1
- package/dist/src/mcp/validators.d.ts +6 -0
- package/dist/src/mcp/validators.js +20 -2
- package/dist/src/security/policy.d.ts +14 -3
- package/dist/src/security/policy.js +23 -4
- package/docs/BLUEPRINT.md +2 -2
- package/extension-dist/background.js +77 -3
- package/extension-dist/manifest.json +20 -5
- package/package.json +1 -1
|
@@ -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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
458
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
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;
|
|
@@ -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
|
-
|
|
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
|
|
@@ -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) {
|
package/dist/src/mcp/server.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -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
|
|
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${
|
|
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() {
|
package/dist/src/mcp/tools.d.ts
CHANGED
|
@@ -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;
|