cdp-mcp-server 5.0.1 → 5.0.3
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 +91 -0
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/connection/browser-discovery.d.ts +41 -4
- package/dist/connection/browser-discovery.js +125 -8
- package/dist/connection/browser-discovery.js.map +1 -1
- package/dist/connection/health-monitor.d.ts +22 -0
- package/dist/connection/health-monitor.js +155 -28
- package/dist/connection/health-monitor.js.map +1 -1
- package/dist/connection/index.d.ts +3 -3
- package/dist/connection/index.js +1 -1
- package/dist/connection/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/tools/form.js +150 -150
- package/dist/tools/interact.js +137 -137
- package/dist/tools/tabs.js +3 -3
- package/dist/types.d.ts +4 -0
- package/dist/utils/connect-diagnostics.d.ts +46 -0
- package/dist/utils/connect-diagnostics.js +124 -0
- package/dist/utils/connect-diagnostics.js.map +1 -0
- package/package.json +17 -13
package/dist/tools/interact.js
CHANGED
|
@@ -199,12 +199,12 @@ async function handleType(ctx, args) {
|
|
|
199
199
|
}, retryTimeout);
|
|
200
200
|
// Clear existing content if requested (default: true)
|
|
201
201
|
const clearCode = args.clear !== false
|
|
202
|
-
? `if ('value' in this) {
|
|
203
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
|
|
204
|
-
|| Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
205
|
-
if (nativeSetter) nativeSetter.call(this, '');
|
|
206
|
-
else this.value = '';
|
|
207
|
-
this.dispatchEvent(new Event('input', {bubbles:true}));
|
|
202
|
+
? `if ('value' in this) {
|
|
203
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
|
|
204
|
+
|| Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
205
|
+
if (nativeSetter) nativeSetter.call(this, '');
|
|
206
|
+
else this.value = '';
|
|
207
|
+
this.dispatchEvent(new Event('input', {bubbles:true}));
|
|
208
208
|
} else if (this.isContentEditable) { this.textContent = ''; }`
|
|
209
209
|
: '';
|
|
210
210
|
await ctx.sendCommand('Runtime.callFunctionOn', {
|
|
@@ -264,20 +264,20 @@ async function handleType(ctx, args) {
|
|
|
264
264
|
else {
|
|
265
265
|
// Instant fill using nativeInputValueSetter for React/Angular compatibility
|
|
266
266
|
await ctx.sendCommand('Runtime.evaluate', {
|
|
267
|
-
expression: `(() => {
|
|
268
|
-
const el = document.activeElement;
|
|
269
|
-
if (!el) return;
|
|
270
|
-
const val = ${JSON.stringify(args.text)};
|
|
271
|
-
if ('value' in el) {
|
|
272
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
|
|
273
|
-
|| Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
274
|
-
if (nativeSetter) nativeSetter.call(el, val);
|
|
275
|
-
else el.value = val;
|
|
276
|
-
} else if (el.isContentEditable) {
|
|
277
|
-
document.execCommand('insertText', false, val);
|
|
278
|
-
}
|
|
279
|
-
el.dispatchEvent(new Event('input', {bubbles:true}));
|
|
280
|
-
el.dispatchEvent(new Event('change', {bubbles:true}));
|
|
267
|
+
expression: `(() => {
|
|
268
|
+
const el = document.activeElement;
|
|
269
|
+
if (!el) return;
|
|
270
|
+
const val = ${JSON.stringify(args.text)};
|
|
271
|
+
if ('value' in el) {
|
|
272
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
|
|
273
|
+
|| Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
274
|
+
if (nativeSetter) nativeSetter.call(el, val);
|
|
275
|
+
else el.value = val;
|
|
276
|
+
} else if (el.isContentEditable) {
|
|
277
|
+
document.execCommand('insertText', false, val);
|
|
278
|
+
}
|
|
279
|
+
el.dispatchEvent(new Event('input', {bubbles:true}));
|
|
280
|
+
el.dispatchEvent(new Event('change', {bubbles:true}));
|
|
281
281
|
})()`,
|
|
282
282
|
}, sess);
|
|
283
283
|
}
|
|
@@ -308,38 +308,38 @@ async function handleFill(ctx, args) {
|
|
|
308
308
|
}, retryTimeout);
|
|
309
309
|
const fieldType = field.type || 'text';
|
|
310
310
|
const r = await ctx.sendCommand('Runtime.callFunctionOn', {
|
|
311
|
-
functionDeclaration: `function() {
|
|
312
|
-
this.scrollIntoView({ block: "center" });
|
|
313
|
-
const type = ${JSON.stringify(fieldType)};
|
|
314
|
-
const val = ${JSON.stringify(field.value)};
|
|
315
|
-
if (type === "checkbox") {
|
|
316
|
-
const wanted = val === "true" || val === "1";
|
|
317
|
-
if (this.checked !== wanted) this.click();
|
|
318
|
-
return { ok: true, value: String(this.checked) };
|
|
319
|
-
}
|
|
320
|
-
if (type === "radio") { this.click(); return { ok: true, value: val }; }
|
|
321
|
-
if (type === "select") {
|
|
322
|
-
let opt = Array.from(this.options).find(o => o.value === val || o.textContent.trim() === val);
|
|
323
|
-
if (!opt) return { error: "Option not found: " + val };
|
|
324
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
|
|
325
|
-
if (nativeSetter) nativeSetter.call(this, opt.value);
|
|
326
|
-
else this.value = opt.value;
|
|
327
|
-
this.dispatchEvent(new Event('input', {bubbles:true}));
|
|
328
|
-
this.dispatchEvent(new Event('change', {bubbles:true}));
|
|
329
|
-
return { ok: true, value: opt.textContent.trim() };
|
|
330
|
-
}
|
|
331
|
-
this.focus();
|
|
332
|
-
if ('value' in this) {
|
|
333
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
|
|
334
|
-
|| Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
335
|
-
if (nativeSetter) nativeSetter.call(this, val);
|
|
336
|
-
else this.value = val;
|
|
337
|
-
} else if (this.isContentEditable) {
|
|
338
|
-
this.textContent = val;
|
|
339
|
-
}
|
|
340
|
-
this.dispatchEvent(new Event('input', {bubbles:true}));
|
|
341
|
-
this.dispatchEvent(new Event('change', {bubbles:true}));
|
|
342
|
-
return { ok: true, value: val.substring(0, 40) };
|
|
311
|
+
functionDeclaration: `function() {
|
|
312
|
+
this.scrollIntoView({ block: "center" });
|
|
313
|
+
const type = ${JSON.stringify(fieldType)};
|
|
314
|
+
const val = ${JSON.stringify(field.value)};
|
|
315
|
+
if (type === "checkbox") {
|
|
316
|
+
const wanted = val === "true" || val === "1";
|
|
317
|
+
if (this.checked !== wanted) this.click();
|
|
318
|
+
return { ok: true, value: String(this.checked) };
|
|
319
|
+
}
|
|
320
|
+
if (type === "radio") { this.click(); return { ok: true, value: val }; }
|
|
321
|
+
if (type === "select") {
|
|
322
|
+
let opt = Array.from(this.options).find(o => o.value === val || o.textContent.trim() === val);
|
|
323
|
+
if (!opt) return { error: "Option not found: " + val };
|
|
324
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
|
|
325
|
+
if (nativeSetter) nativeSetter.call(this, opt.value);
|
|
326
|
+
else this.value = opt.value;
|
|
327
|
+
this.dispatchEvent(new Event('input', {bubbles:true}));
|
|
328
|
+
this.dispatchEvent(new Event('change', {bubbles:true}));
|
|
329
|
+
return { ok: true, value: opt.textContent.trim() };
|
|
330
|
+
}
|
|
331
|
+
this.focus();
|
|
332
|
+
if ('value' in this) {
|
|
333
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
|
|
334
|
+
|| Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
335
|
+
if (nativeSetter) nativeSetter.call(this, val);
|
|
336
|
+
else this.value = val;
|
|
337
|
+
} else if (this.isContentEditable) {
|
|
338
|
+
this.textContent = val;
|
|
339
|
+
}
|
|
340
|
+
this.dispatchEvent(new Event('input', {bubbles:true}));
|
|
341
|
+
this.dispatchEvent(new Event('change', {bubbles:true}));
|
|
342
|
+
return { ok: true, value: val.substring(0, 40) };
|
|
343
343
|
}`,
|
|
344
344
|
objectId,
|
|
345
345
|
returnByValue: true,
|
|
@@ -363,35 +363,35 @@ async function handleSelect(ctx, args) {
|
|
|
363
363
|
return resolveElementObjectId(ctx, sess, args.uid, args.selector, args.tabId);
|
|
364
364
|
}, retryTimeout);
|
|
365
365
|
const result = await ctx.sendCommand('Runtime.callFunctionOn', {
|
|
366
|
-
functionDeclaration: `function() {
|
|
367
|
-
const sel = this;
|
|
368
|
-
if (sel.tagName === "SELECT") {
|
|
369
|
-
const targetVal = ${JSON.stringify(args.value)};
|
|
370
|
-
let opt = Array.from(sel.options).find(o => o.value === targetVal);
|
|
371
|
-
if (!opt) opt = Array.from(sel.options).find(o => o.textContent.trim() === targetVal);
|
|
372
|
-
if (!opt) return { error: "Option not found: " + targetVal + ". Available: " + Array.from(sel.options).map(o => o.textContent.trim()).join(", ") };
|
|
373
|
-
|
|
374
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
|
|
375
|
-
if (nativeSetter) nativeSetter.call(sel, opt.value);
|
|
376
|
-
else sel.value = opt.value;
|
|
377
|
-
|
|
378
|
-
sel.dispatchEvent(new Event("input", { bubbles: true }));
|
|
379
|
-
sel.dispatchEvent(new Event("change", { bubbles: true }));
|
|
380
|
-
return { selected: opt.textContent.trim(), value: opt.value };
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const role = sel.getAttribute("role");
|
|
384
|
-
const isCustomDropdown = role === "combobox" || role === "listbox" ||
|
|
385
|
-
sel.getAttribute("aria-haspopup") ||
|
|
386
|
-
sel.classList.toString().match(/MuiSelect|ant-select|react-select|Select/i);
|
|
387
|
-
|
|
388
|
-
if (isCustomDropdown) {
|
|
389
|
-
sel.scrollIntoView({ block: "center" });
|
|
390
|
-
sel.click();
|
|
391
|
-
return { customDropdown: true, message: "Custom dropdown opened — click the matching option" };
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return { error: "Not a <select> element and not a recognized custom dropdown. Element: <" + sel.tagName.toLowerCase() + ">" };
|
|
366
|
+
functionDeclaration: `function() {
|
|
367
|
+
const sel = this;
|
|
368
|
+
if (sel.tagName === "SELECT") {
|
|
369
|
+
const targetVal = ${JSON.stringify(args.value)};
|
|
370
|
+
let opt = Array.from(sel.options).find(o => o.value === targetVal);
|
|
371
|
+
if (!opt) opt = Array.from(sel.options).find(o => o.textContent.trim() === targetVal);
|
|
372
|
+
if (!opt) return { error: "Option not found: " + targetVal + ". Available: " + Array.from(sel.options).map(o => o.textContent.trim()).join(", ") };
|
|
373
|
+
|
|
374
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
|
|
375
|
+
if (nativeSetter) nativeSetter.call(sel, opt.value);
|
|
376
|
+
else sel.value = opt.value;
|
|
377
|
+
|
|
378
|
+
sel.dispatchEvent(new Event("input", { bubbles: true }));
|
|
379
|
+
sel.dispatchEvent(new Event("change", { bubbles: true }));
|
|
380
|
+
return { selected: opt.textContent.trim(), value: opt.value };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const role = sel.getAttribute("role");
|
|
384
|
+
const isCustomDropdown = role === "combobox" || role === "listbox" ||
|
|
385
|
+
sel.getAttribute("aria-haspopup") ||
|
|
386
|
+
sel.classList.toString().match(/MuiSelect|ant-select|react-select|Select/i);
|
|
387
|
+
|
|
388
|
+
if (isCustomDropdown) {
|
|
389
|
+
sel.scrollIntoView({ block: "center" });
|
|
390
|
+
sel.click();
|
|
391
|
+
return { customDropdown: true, message: "Custom dropdown opened — click the matching option" };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return { error: "Not a <select> element and not a recognized custom dropdown. Element: <" + sel.tagName.toLowerCase() + ">" };
|
|
395
395
|
}`,
|
|
396
396
|
objectId,
|
|
397
397
|
returnByValue: true,
|
|
@@ -403,19 +403,19 @@ async function handleSelect(ctx, args) {
|
|
|
403
403
|
if (v?.customDropdown) {
|
|
404
404
|
await sleep(300);
|
|
405
405
|
const clickResult = await ctx.sendCommand('Runtime.evaluate', {
|
|
406
|
-
expression: `(() => {
|
|
407
|
-
const val = ${JSON.stringify(args.value)};
|
|
408
|
-
const options = document.querySelectorAll('[role="option"], [role="listitem"], li[data-value], .MuiMenuItem-root, .ant-select-item, [class*="option"]');
|
|
409
|
-
for (const opt of options) {
|
|
410
|
-
const text = opt.textContent.trim();
|
|
411
|
-
const value = opt.getAttribute("data-value") || opt.getAttribute("value") || "";
|
|
412
|
-
if (text === val || value === val || text.toLowerCase() === val.toLowerCase()) {
|
|
413
|
-
opt.scrollIntoView({ block: "center" });
|
|
414
|
-
opt.click();
|
|
415
|
-
return { selected: text, value: value || text };
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return { error: "Option not found in custom dropdown: " + val + ". Visible options: " + Array.from(options).slice(0, 10).map(o => o.textContent.trim()).join(", ") };
|
|
406
|
+
expression: `(() => {
|
|
407
|
+
const val = ${JSON.stringify(args.value)};
|
|
408
|
+
const options = document.querySelectorAll('[role="option"], [role="listitem"], li[data-value], .MuiMenuItem-root, .ant-select-item, [class*="option"]');
|
|
409
|
+
for (const opt of options) {
|
|
410
|
+
const text = opt.textContent.trim();
|
|
411
|
+
const value = opt.getAttribute("data-value") || opt.getAttribute("value") || "";
|
|
412
|
+
if (text === val || value === val || text.toLowerCase() === val.toLowerCase()) {
|
|
413
|
+
opt.scrollIntoView({ block: "center" });
|
|
414
|
+
opt.click();
|
|
415
|
+
return { selected: text, value: value || text };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return { error: "Option not found in custom dropdown: " + val + ". Visible options: " + Array.from(options).slice(0, 10).map(o => o.textContent.trim()).join(", ") };
|
|
419
419
|
})()`,
|
|
420
420
|
returnByValue: true,
|
|
421
421
|
}, sess);
|
|
@@ -696,10 +696,10 @@ async function handleFocus(ctx, args) {
|
|
|
696
696
|
const retryTimeout = args.timeout || ctx.config.actionTimeout;
|
|
697
697
|
const { objectId, resolvedSession } = await withRetry(() => resolveElementObjectId(ctx, sess, args.uid, args.selector, args.tabId), retryTimeout);
|
|
698
698
|
const result = await ctx.sendCommand('Runtime.callFunctionOn', {
|
|
699
|
-
functionDeclaration: `function() {
|
|
700
|
-
this.scrollIntoView({ block: "center" });
|
|
701
|
-
this.focus();
|
|
702
|
-
return { tag: this.tagName.toLowerCase(), label: (this.getAttribute("aria-label") || this.textContent || "").trim().substring(0, 60) };
|
|
699
|
+
functionDeclaration: `function() {
|
|
700
|
+
this.scrollIntoView({ block: "center" });
|
|
701
|
+
this.focus();
|
|
702
|
+
return { tag: this.tagName.toLowerCase(), label: (this.getAttribute("aria-label") || this.textContent || "").trim().substring(0, 60) };
|
|
703
703
|
}`,
|
|
704
704
|
objectId,
|
|
705
705
|
returnByValue: true,
|
|
@@ -719,14 +719,14 @@ async function handleCheck(ctx, args) {
|
|
|
719
719
|
return resolveElementObjectId(ctx, sess, args.uid, args.selector, args.tabId);
|
|
720
720
|
}, retryTimeout);
|
|
721
721
|
await ctx.sendCommand('Runtime.callFunctionOn', {
|
|
722
|
-
functionDeclaration: `function() {
|
|
723
|
-
this.scrollIntoView({ block: "center" });
|
|
724
|
-
const desired = ${args.checked === true};
|
|
725
|
-
if (this.checked !== desired) {
|
|
726
|
-
this.click();
|
|
727
|
-
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
728
|
-
}
|
|
729
|
-
return { checked: this.checked };
|
|
722
|
+
functionDeclaration: `function() {
|
|
723
|
+
this.scrollIntoView({ block: "center" });
|
|
724
|
+
const desired = ${args.checked === true};
|
|
725
|
+
if (this.checked !== desired) {
|
|
726
|
+
this.click();
|
|
727
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
728
|
+
}
|
|
729
|
+
return { checked: this.checked };
|
|
730
730
|
}`,
|
|
731
731
|
objectId,
|
|
732
732
|
returnByValue: true,
|
|
@@ -785,14 +785,14 @@ async function resolveElement(ctx, sess, uid, selector, tabId) {
|
|
|
785
785
|
throw Errors.staleRef(uid);
|
|
786
786
|
// Get bounding rect and element info via JS
|
|
787
787
|
const info = await ctx.sendCommand('Runtime.callFunctionOn', {
|
|
788
|
-
functionDeclaration: `function() {
|
|
789
|
-
const r = this.getBoundingClientRect();
|
|
790
|
-
return {
|
|
791
|
-
x: r.x + r.width / 2, y: r.y + r.height / 2,
|
|
792
|
-
w: r.width, h: r.height,
|
|
793
|
-
tag: this.tagName.toLowerCase(),
|
|
794
|
-
label: (this.getAttribute('aria-label') || this.textContent || '').trim().substring(0, 60),
|
|
795
|
-
};
|
|
788
|
+
functionDeclaration: `function() {
|
|
789
|
+
const r = this.getBoundingClientRect();
|
|
790
|
+
return {
|
|
791
|
+
x: r.x + r.width / 2, y: r.y + r.height / 2,
|
|
792
|
+
w: r.width, h: r.height,
|
|
793
|
+
tag: this.tagName.toLowerCase(),
|
|
794
|
+
label: (this.getAttribute('aria-label') || this.textContent || '').trim().substring(0, 60),
|
|
795
|
+
};
|
|
796
796
|
}`,
|
|
797
797
|
objectId: object.objectId,
|
|
798
798
|
returnByValue: true,
|
|
@@ -804,16 +804,16 @@ async function resolveElement(ctx, sess, uid, selector, tabId) {
|
|
|
804
804
|
if (!selector)
|
|
805
805
|
throw new Error('Provide uid or selector to identify the element.');
|
|
806
806
|
const result = await ctx.sendCommand('Runtime.evaluate', {
|
|
807
|
-
expression: `(() => {
|
|
808
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
809
|
-
if (!el) return null;
|
|
810
|
-
const r = el.getBoundingClientRect();
|
|
811
|
-
return {
|
|
812
|
-
x: r.x + r.width / 2, y: r.y + r.height / 2,
|
|
813
|
-
w: r.width, h: r.height,
|
|
814
|
-
tag: el.tagName.toLowerCase(),
|
|
815
|
-
label: (el.getAttribute('aria-label') || el.textContent || '').trim().substring(0, 60),
|
|
816
|
-
};
|
|
807
|
+
expression: `(() => {
|
|
808
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
809
|
+
if (!el) return null;
|
|
810
|
+
const r = el.getBoundingClientRect();
|
|
811
|
+
return {
|
|
812
|
+
x: r.x + r.width / 2, y: r.y + r.height / 2,
|
|
813
|
+
w: r.width, h: r.height,
|
|
814
|
+
tag: el.tagName.toLowerCase(),
|
|
815
|
+
label: (el.getAttribute('aria-label') || el.textContent || '').trim().substring(0, 60),
|
|
816
|
+
};
|
|
817
817
|
})()`,
|
|
818
818
|
returnByValue: true,
|
|
819
819
|
}, sess);
|
|
@@ -969,16 +969,16 @@ function getAdjacentKey(char) {
|
|
|
969
969
|
async function findAndSetFileInput(ctx, sess, files) {
|
|
970
970
|
try {
|
|
971
971
|
const inputObj = await ctx.sendCommand('Runtime.evaluate', {
|
|
972
|
-
expression: `(() => {
|
|
973
|
-
function find(root) {
|
|
974
|
-
const el = root.querySelector('input[type="file"]');
|
|
975
|
-
if (el) return el;
|
|
976
|
-
for (const s of root.querySelectorAll('*')) {
|
|
977
|
-
if (s.shadowRoot) { const f = find(s.shadowRoot); if (f) return f; }
|
|
978
|
-
}
|
|
979
|
-
return null;
|
|
980
|
-
}
|
|
981
|
-
return find(document);
|
|
972
|
+
expression: `(() => {
|
|
973
|
+
function find(root) {
|
|
974
|
+
const el = root.querySelector('input[type="file"]');
|
|
975
|
+
if (el) return el;
|
|
976
|
+
for (const s of root.querySelectorAll('*')) {
|
|
977
|
+
if (s.shadowRoot) { const f = find(s.shadowRoot); if (f) return f; }
|
|
978
|
+
}
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
981
|
+
return find(document);
|
|
982
982
|
})()`,
|
|
983
983
|
returnByValue: false,
|
|
984
984
|
}, sess);
|
|
@@ -992,9 +992,9 @@ async function findAndSetFileInput(ctx, sess, files) {
|
|
|
992
992
|
}, sess);
|
|
993
993
|
// Dispatch change+input events
|
|
994
994
|
await ctx.sendCommand('Runtime.callFunctionOn', {
|
|
995
|
-
functionDeclaration: `function() {
|
|
996
|
-
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
997
|
-
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
995
|
+
functionDeclaration: `function() {
|
|
996
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
997
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
998
998
|
}`,
|
|
999
999
|
objectId: inputObj.result.objectId,
|
|
1000
1000
|
}, sess).catch(() => { });
|
package/dist/tools/tabs.js
CHANGED
|
@@ -253,9 +253,9 @@ export async function handleNew(ctx, args) {
|
|
|
253
253
|
// Phase 5: Wait for load
|
|
254
254
|
try {
|
|
255
255
|
await ctx.sendCommand('Runtime.evaluate', {
|
|
256
|
-
expression: `new Promise(resolve => {
|
|
257
|
-
if (document.readyState === 'complete') return resolve(true);
|
|
258
|
-
window.addEventListener('load', () => resolve(true), { once: true });
|
|
256
|
+
expression: `new Promise(resolve => {
|
|
257
|
+
if (document.readyState === 'complete') return resolve(true);
|
|
258
|
+
window.addEventListener('load', () => resolve(true), { once: true });
|
|
259
259
|
})`,
|
|
260
260
|
awaitPromise: true,
|
|
261
261
|
returnByValue: true,
|
package/dist/types.d.ts
CHANGED
|
@@ -43,6 +43,10 @@ export interface ServerConfig {
|
|
|
43
43
|
maxTempAgeMs: number;
|
|
44
44
|
/** If set, auto-connect to this Chrome instance/profile on startup */
|
|
45
45
|
autoConnectProfile: string;
|
|
46
|
+
/** Extra User Data dir checked first during discovery (CDP_USER_DATA) */
|
|
47
|
+
userDataDir: string;
|
|
48
|
+
/** Process command-line scan on discovery failure (CDP_PROCESS_SCAN, default ON) */
|
|
49
|
+
processScan: boolean;
|
|
46
50
|
/** If true, expose 6 slim tools instead of 15 full tools (for small-context models) */
|
|
47
51
|
slimMode: boolean;
|
|
48
52
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connect-failure diagnostics — classifies why auto-connect failed and
|
|
3
|
+
* produces a kind + human message with the exact fix.
|
|
4
|
+
*
|
|
5
|
+
* Failure signatures (source-verified, see _archive research 2026-10):
|
|
6
|
+
* - TCP refused on every port, no browser processes → no browser / not launched
|
|
7
|
+
* - TCP refused, browser running WITH debug flag → Chrome 136+ default-profile block
|
|
8
|
+
* - Port bound, /json/version → 404 → gated remote-debugging (toggle)
|
|
9
|
+
* - WS rejected with --remote-allow-origins hint → origin allowlist
|
|
10
|
+
* - Port file existed but WS connect failed → approval dialog denied / stale file
|
|
11
|
+
*/
|
|
12
|
+
/** Outcome of probing one HTTP debug port. */
|
|
13
|
+
export type PortProbeOutcome = 'refused' | 'not_found' | 'timeout' | 'ok' | 'error';
|
|
14
|
+
export interface PortProbeEvidence {
|
|
15
|
+
port: number;
|
|
16
|
+
outcome: PortProbeOutcome;
|
|
17
|
+
/** HTTP status code when available (404 for gated mode). */
|
|
18
|
+
status?: number;
|
|
19
|
+
/** Short error detail (error message, never a full stack). */
|
|
20
|
+
detail?: string;
|
|
21
|
+
}
|
|
22
|
+
/** What the (optional) process scan saw on the machine. */
|
|
23
|
+
export interface ProcessInfo {
|
|
24
|
+
/** Browser process names seen running (e.g. 'brave', 'chrome'). */
|
|
25
|
+
browsersRunning: string[];
|
|
26
|
+
/** At least one browser was launched with --remote-debugging-port. */
|
|
27
|
+
flagPresent: boolean;
|
|
28
|
+
}
|
|
29
|
+
export type ConnectFailureKind = 'no_browser' | 'no_debug_flag' | 'm136_default_dir' | 'toggle_mode' | 'approval_denied' | 'origin_blocked' | 'generic';
|
|
30
|
+
export interface ConnectDiagnosis {
|
|
31
|
+
kind: ConnectFailureKind;
|
|
32
|
+
headline: string;
|
|
33
|
+
fix: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ClassifyInput {
|
|
36
|
+
probes: PortProbeEvidence[];
|
|
37
|
+
/** True when file-based discovery found at least one DevToolsActivePort. */
|
|
38
|
+
portFilesFound: number;
|
|
39
|
+
/** Error messages captured from failed WS connect attempts. */
|
|
40
|
+
connectErrors: string[];
|
|
41
|
+
/** Process-scan result, or null when scan disabled/unavailable. */
|
|
42
|
+
processInfo: ProcessInfo | null;
|
|
43
|
+
host: string;
|
|
44
|
+
primaryPort: number;
|
|
45
|
+
}
|
|
46
|
+
export declare function classifyConnectFailure(input: ClassifyInput): ConnectDiagnosis;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connect-failure diagnostics — classifies why auto-connect failed and
|
|
3
|
+
* produces a kind + human message with the exact fix.
|
|
4
|
+
*
|
|
5
|
+
* Failure signatures (source-verified, see _archive research 2026-10):
|
|
6
|
+
* - TCP refused on every port, no browser processes → no browser / not launched
|
|
7
|
+
* - TCP refused, browser running WITH debug flag → Chrome 136+ default-profile block
|
|
8
|
+
* - Port bound, /json/version → 404 → gated remote-debugging (toggle)
|
|
9
|
+
* - WS rejected with --remote-allow-origins hint → origin allowlist
|
|
10
|
+
* - Port file existed but WS connect failed → approval dialog denied / stale file
|
|
11
|
+
*/
|
|
12
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
13
|
+
const ANY_BOUND = (probes) => probes.some((p) => p.outcome === 'not_found' || p.outcome === 'ok');
|
|
14
|
+
const ALL_REFUSED = (probes) => probes.length > 0 && probes.every((p) => p.outcome === 'refused');
|
|
15
|
+
function relaunchCommand(host, port) {
|
|
16
|
+
const target = host === '127.0.0.1' || host === 'localhost' ? '' : ` ${host}`;
|
|
17
|
+
return (`& '<browser.exe>' --user-data-dir='<dir>' --remote-debugging-port=${port}${target}`);
|
|
18
|
+
}
|
|
19
|
+
// ─── Classifier ─────────────────────────────────────────────────────
|
|
20
|
+
export function classifyConnectFailure(input) {
|
|
21
|
+
const { probes, portFilesFound, connectErrors, processInfo, host, primaryPort } = input;
|
|
22
|
+
const originError = connectErrors.find((e) => e.includes('--remote-allow-origins'));
|
|
23
|
+
if (originError) {
|
|
24
|
+
return {
|
|
25
|
+
kind: 'origin_blocked',
|
|
26
|
+
headline: 'The browser rejected this WebSocket origin.',
|
|
27
|
+
fix: `Relaunch the browser with:\n` +
|
|
28
|
+
` --remote-allow-origins=${host === '127.0.0.1' ? '*' : host}\n` +
|
|
29
|
+
`Or connect from the same origin the browser expects.`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// Most specific: a port file was found and a WS connect was attempted but
|
|
33
|
+
// failed (approval dialog denied, stale file, no visible window).
|
|
34
|
+
if (portFilesFound > 0 && connectErrors.length > 0) {
|
|
35
|
+
return {
|
|
36
|
+
kind: 'approval_denied',
|
|
37
|
+
headline: 'A DevToolsActivePort file was found but the WebSocket connection ' +
|
|
38
|
+
'was rejected.',
|
|
39
|
+
fix: `Likely causes:\n` +
|
|
40
|
+
` 1. The permission dialog was dismissed or no browser window was ` +
|
|
41
|
+
`visible (approval mode auto-denies without a window).\n` +
|
|
42
|
+
` 2. The port file is stale (browser restarted on another port). ` +
|
|
43
|
+
`Retry after the browser settles.\n` +
|
|
44
|
+
` 3. Or relaunch with the flag: ${relaunchCommand(host, primaryPort)}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Gated remote-debugging: port bound but the legacy HTTP API is absent.
|
|
48
|
+
const gated = probes.find((p) => p.outcome === 'not_found' && p.status === 404);
|
|
49
|
+
if (gated) {
|
|
50
|
+
return {
|
|
51
|
+
kind: 'toggle_mode',
|
|
52
|
+
headline: 'Your browser is running the new gated remote-debugging ' +
|
|
53
|
+
'(chrome://inspect/#remote-debugging toggle). It does not serve the ' +
|
|
54
|
+
'legacy HTTP discovery API.',
|
|
55
|
+
fix: `Do ONE of these:\n` +
|
|
56
|
+
` 1. Look at your browser — a permission dialog may be waiting. ` +
|
|
57
|
+
`Click "Allow" (needs a visible browser window).\n` +
|
|
58
|
+
` 2. Or relaunch with the flag (same data dir keeps your logins):\n` +
|
|
59
|
+
` ${relaunchCommand(host, primaryPort)}\n` +
|
|
60
|
+
` 3. Or keep the toggle and make sure the browser's User Data dir ` +
|
|
61
|
+
`is discoverable (CDP_USER_DATA env or process scan).`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (ALL_REFUSED(probes)) {
|
|
65
|
+
const browsers = processInfo?.browsersRunning ?? [];
|
|
66
|
+
if (browsers.length === 0) {
|
|
67
|
+
return {
|
|
68
|
+
kind: 'no_browser',
|
|
69
|
+
headline: 'Could not find any browser with remote debugging enabled.',
|
|
70
|
+
fix: `To fix, do ONE of these:\n` +
|
|
71
|
+
` 1. Launch your browser with the debug flag:\n` +
|
|
72
|
+
` ${relaunchCommand(host, primaryPort)}\n` +
|
|
73
|
+
` 2. Or enable the chrome://inspect/#remote-debugging toggle ` +
|
|
74
|
+
`(Chrome/Brave 144+)\n` +
|
|
75
|
+
` 3. Or set CDP_PORT env var if using a non-standard port\n\n` +
|
|
76
|
+
`The server will auto-connect once debugging is enabled.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (processInfo?.flagPresent) {
|
|
80
|
+
// Flag present but nothing listening → Chrome 136+ default-profile block
|
|
81
|
+
return {
|
|
82
|
+
kind: 'm136_default_dir',
|
|
83
|
+
headline: 'The debug port was requested but Chromium 136+ refuses CDP on the ' +
|
|
84
|
+
'DEFAULT User Data directory.',
|
|
85
|
+
fix: `Relaunch with an explicit NON-DEFAULT data dir (your logins stay ` +
|
|
86
|
+
`if you reuse the same dir):\n` +
|
|
87
|
+
` ${relaunchCommand(host, primaryPort)}\n` +
|
|
88
|
+
`Note: Chrome for Testing is exempt from this restriction.`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
kind: 'no_debug_flag',
|
|
93
|
+
headline: 'A browser is running, but without remote debugging enabled.',
|
|
94
|
+
fix: `Do ONE of these:\n` +
|
|
95
|
+
` 1. Fully exit the browser, then relaunch with:\n` +
|
|
96
|
+
` ${relaunchCommand(host, primaryPort)}\n` +
|
|
97
|
+
` 2. Or enable the chrome://inspect/#remote-debugging toggle ` +
|
|
98
|
+
`(Chrome/Brave 144+) — the server will ask for permission on connect.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Port file existed but connect failed without a recognizable signature —
|
|
102
|
+
// handled above; this branch is unreachable but kept for safety.
|
|
103
|
+
if (portFilesFound > 0 && connectErrors.length > 0) {
|
|
104
|
+
return {
|
|
105
|
+
kind: 'approval_denied',
|
|
106
|
+
headline: 'A DevToolsActivePort file was found but the WebSocket connection ' +
|
|
107
|
+
'was rejected.',
|
|
108
|
+
fix: `Likely causes:\n` +
|
|
109
|
+
` 1. The permission dialog was dismissed or no browser window was ` +
|
|
110
|
+
`visible (approval mode auto-denies without a window).\n` +
|
|
111
|
+
` 2. The port file is stale (browser restarted on another port). ` +
|
|
112
|
+
`Retry after the browser settles.\n` +
|
|
113
|
+
` 3. Or relaunch with the flag: ${relaunchCommand(host, primaryPort)}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
kind: 'generic',
|
|
118
|
+
headline: 'Could not connect to any browser.',
|
|
119
|
+
fix: `Launch the browser with:\n` +
|
|
120
|
+
` ${relaunchCommand(host, primaryPort)}\n` +
|
|
121
|
+
`Or enable the chrome://inspect/#remote-debugging toggle.`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=connect-diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect-diagnostics.js","sourceRoot":"","sources":["../../src/utils/connect-diagnostics.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdp-mcp-server",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.3",
|
|
4
4
|
"description": "The power user's browser MCP — connects to your real Chrome/Edge/Brave with full DevTools capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"cdp-mcp-server": "dist/index.js"
|
|
10
|
+
},
|
|
8
11
|
"author": "Tushar Agarwal",
|
|
9
12
|
"license": "MIT",
|
|
10
13
|
"repository": {
|
|
@@ -25,18 +28,9 @@
|
|
|
25
28
|
"browser-mcp"
|
|
26
29
|
],
|
|
27
30
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
29
|
-
},
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
32
|
-
"ws": "^8.0.0"
|
|
33
|
-
},
|
|
34
|
-
"devDependencies": {
|
|
35
|
-
"@types/node": "^20.19.39",
|
|
36
|
-
"@types/ws": "^8.18.1",
|
|
37
|
-
"typescript": "^5.9.3",
|
|
38
|
-
"vitest": "^2.1.9"
|
|
31
|
+
"node": ">=20"
|
|
39
32
|
},
|
|
33
|
+
"packageManager": "pnpm@12.6.0",
|
|
40
34
|
"scripts": {
|
|
41
35
|
"build": "tsc",
|
|
42
36
|
"start": "node dist/index.js",
|
|
@@ -48,5 +42,15 @@
|
|
|
48
42
|
"lint": "tsc --noEmit",
|
|
49
43
|
"typecheck": "tsc --noEmit",
|
|
50
44
|
"clean": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true})\""
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
48
|
+
"ws": "^8.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^20.19.39",
|
|
52
|
+
"@types/ws": "^8.18.1",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"vitest": "^2.1.9"
|
|
51
55
|
}
|
|
52
|
-
}
|
|
56
|
+
}
|