@debugg-ai/debugg-ai-mcp 6.1.6 → 6.2.2
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/CHANGELOG.md +19 -0
- package/README.md +13 -1
- package/dist/handlers/localExecutorBridge.js +202 -0
- package/dist/handlers/probePageHandler.js +1 -0
- package/dist/handlers/testPageChangesHandler.js +47 -8
- package/dist/services/localBrowser.js +18 -70
- package/dist/services/localExecutor/credentialBox.js +43 -0
- package/dist/services/localExecutor/httpTransport.js +72 -0
- package/dist/services/localExecutor/opRunner.js +437 -0
- package/dist/services/localExecutor/ops.js +12 -0
- package/dist/services/localExecutor/session.js +125 -0
- package/dist/services/navigationGuard.js +71 -0
- package/dist/services/workflows.js +5 -0
- package/dist/tools/probePage.js +1 -1
- package/dist/tools/testPageChanges.js +8 -0
- package/dist/types/index.js +6 -0
- package/dist/utils/projectRequired.js +8 -4
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export function isLoopbackHost(h) {
|
|
2
|
+
return ['localhost', '127.0.0.1', '[::1]', '::1'].includes(h.toLowerCase());
|
|
3
|
+
}
|
|
4
|
+
export function safeHost(u) {
|
|
5
|
+
try {
|
|
6
|
+
return new URL(u).hostname;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return u;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function hostAllowed(host, allowed) {
|
|
13
|
+
const h = host.toLowerCase();
|
|
14
|
+
return allowed.some((d) => {
|
|
15
|
+
const a = d.trim().toLowerCase().replace(/^\*?\./, '');
|
|
16
|
+
return a !== '' && (h === a || h.endsWith(`.${a}`));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** Guard every current and future page of the context. `onBlocked` receives the refused host. */
|
|
20
|
+
export async function installNavigationGuard(context, allowed, onBlocked) {
|
|
21
|
+
const check = (u) => {
|
|
22
|
+
try {
|
|
23
|
+
const h = new URL(u).hostname;
|
|
24
|
+
return hostAllowed(h, allowed) || isLoopbackHost(h);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const guard = async (pg) => {
|
|
31
|
+
const cdp = await context.newCDPSession(pg);
|
|
32
|
+
const mainFrameId = (await cdp.send('Page.getFrameTree')).frameTree.frame.id;
|
|
33
|
+
await cdp.send('Fetch.enable', { patterns: [
|
|
34
|
+
{ urlPattern: '*', resourceType: 'Document', requestStage: 'Request' },
|
|
35
|
+
{ urlPattern: '*', resourceType: 'Document', requestStage: 'Response' },
|
|
36
|
+
] });
|
|
37
|
+
cdp.on('Fetch.requestPaused', (ev) => {
|
|
38
|
+
void (async () => {
|
|
39
|
+
try {
|
|
40
|
+
if (ev.frameId !== mainFrameId) {
|
|
41
|
+
await cdp.send(ev.responseStatusCode ? 'Fetch.continueResponse' : 'Fetch.continueRequest', { requestId: ev.requestId });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (ev.responseStatusCode === undefined) {
|
|
45
|
+
if (!check(ev.request.url)) {
|
|
46
|
+
onBlocked(safeHost(ev.request.url));
|
|
47
|
+
await cdp.send('Fetch.failRequest', { requestId: ev.requestId, errorReason: 'BlockedByClient' });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
await cdp.send('Fetch.continueRequest', { requestId: ev.requestId });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const loc = ev.responseHeaders?.find((h) => h.name.toLowerCase() === 'location')?.value;
|
|
54
|
+
if (ev.responseStatusCode >= 300 && ev.responseStatusCode < 400 && loc) {
|
|
55
|
+
const next = new URL(loc, ev.request.url).toString();
|
|
56
|
+
if (!check(next)) {
|
|
57
|
+
onBlocked(safeHost(next));
|
|
58
|
+
await cdp.send('Fetch.failRequest', { requestId: ev.requestId, errorReason: 'BlockedByClient' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await cdp.send('Fetch.continueResponse', { requestId: ev.requestId });
|
|
63
|
+
}
|
|
64
|
+
catch { /* page closed */ }
|
|
65
|
+
})();
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
for (const pg of context.pages())
|
|
69
|
+
await guard(pg);
|
|
70
|
+
context.on('page', (pg) => { void guard(pg).catch(() => { }); });
|
|
71
|
+
}
|
|
@@ -43,11 +43,16 @@ const noProgressTimeoutMs = () => {
|
|
|
43
43
|
*/
|
|
44
44
|
export const EVAL_TEMPLATE_SLUG = 'flow/e2es/app-eval'; // check_app_in_browser
|
|
45
45
|
export const PAGE_PROBE_TEMPLATE_SLUG = 'flow/tools/probe'; // probe_page
|
|
46
|
+
export const LOCAL_BROWSE_TEMPLATE_SLUG = 'flow/tools/browse'; // check_app_in_browser browser:'local' (sense/think/act brain)
|
|
46
47
|
export const RAW_CRAWL_TEMPLATE_SLUG = 'crawl-execution-workflow-template'; // trigger_crawl
|
|
47
48
|
/** The slug the eval-template dispatch pins to (env-overridable). */
|
|
48
49
|
export function getEvalTemplateSlug() {
|
|
49
50
|
return process.env.DEBUGGAI_EVAL_TEMPLATE || EVAL_TEMPLATE_SLUG;
|
|
50
51
|
}
|
|
52
|
+
/** The slug a local-executor check pins to: the browser_use brain cannot drive a local executor (env-overridable). */
|
|
53
|
+
export function getLocalBrowseTemplateSlug() {
|
|
54
|
+
return process.env.DEBUGGAI_LOCAL_TEMPLATE || LOCAL_BROWSE_TEMPLATE_SLUG;
|
|
55
|
+
}
|
|
51
56
|
/** The slug the page-probe dispatch pins to (env-overridable). */
|
|
52
57
|
export function getPageProbeTemplateSlug() {
|
|
53
58
|
return process.env.DEBUGGAI_PROBE_TEMPLATE || PAGE_PROBE_TEMPLATE_SLUG;
|
package/dist/tools/probePage.js
CHANGED
|
@@ -84,7 +84,7 @@ export function buildProbePageTool() {
|
|
|
84
84
|
browser: {
|
|
85
85
|
type: 'string',
|
|
86
86
|
enum: ['remote', 'local'],
|
|
87
|
-
description: "Default 'remote' (cloud browser; localhost is tunneled). 'local' runs Chrome on THIS machine against the URL directly: no tunnel, nothing uploaded, screenshot also saved under ~/.debugg-ai/captures. Works only when the MCP server runs on your machine
|
|
87
|
+
description: "Default 'remote' (cloud browser; localhost is tunneled). 'local' runs Chrome on THIS machine against the URL directly: no tunnel, nothing uploaded, screenshot also saved under ~/.debugg-ai/captures. Works only when the MCP server runs on your machine (stdio). waitForSelector/waitForLoadState/timeoutMs apply; the local mode returns screenshot, console errors, network summary, title and (optionally) HTML.",
|
|
88
88
|
},
|
|
89
89
|
headless: {
|
|
90
90
|
type: 'boolean',
|
|
@@ -106,6 +106,14 @@ export function buildTestPageChangesTool(ctx) {
|
|
|
106
106
|
type: "boolean",
|
|
107
107
|
description: "Default false. Set true to force a REAL login instead of reusing the warm session the backend keeps per account. Use when the login flow itself is what you're checking, when you suspect the stored session is stale, or when the app's only route between personas is a logout. Costs one login; the run re-captures afterwards, so later runs stay fast."
|
|
108
108
|
},
|
|
109
|
+
browser: {
|
|
110
|
+
type: "string",
|
|
111
|
+
enum: ["remote", "local"],
|
|
112
|
+
description: "Unset: Chrome on THIS machine when it is installed, otherwise the cloud browser (a plan with cloud browsers; localhost is tunneled). 'local' runs the clicking in your own Chrome — free on every plan, no tunnel, no *.localhost limits, much faster — while the cloud agent still decides each step. 'remote' forces the cloud browser (Professional and above). Passwords passed in this call stay on this machine. Page screenshots and text are still sent to the cloud agent. Not available over the remote HTTP MCP."
|
|
113
|
+
},
|
|
114
|
+
headless: { type: "boolean", description: "browser:'local' only. Default true; false shows the Chrome window." },
|
|
115
|
+
useProfile: { type: "boolean", description: "browser:'local' only. Use the MCP-owned logged-in Chrome profile (never your everyday Chrome). Requires allowedDomains." },
|
|
116
|
+
allowedDomains: { type: "array", items: { type: "string" }, description: "browser:'local' only. Extra hostnames (and subdomains) the run may navigate to, beyond the url's own host and localhost. Navigation off this list is refused, redirects included." },
|
|
109
117
|
extraOrigins: {
|
|
110
118
|
type: "array",
|
|
111
119
|
items: { type: "string" },
|
package/dist/types/index.js
CHANGED
|
@@ -64,6 +64,12 @@ export const TestPageChangesInputSchema = z.object({
|
|
|
64
64
|
// skip login; that is right for speed and wrong when the login itself is what
|
|
65
65
|
// you are checking, or when the app's only route between personas is a logout.
|
|
66
66
|
freshSession: z.boolean().optional(),
|
|
67
|
+
// 'local' runs the clicking in Chrome on THIS machine (no tunnel); the cloud agent still decides each step.
|
|
68
|
+
// Unset: your own Chrome when it is installed (stdio server), otherwise the cloud browser.
|
|
69
|
+
browser: z.enum(['remote', 'local']).optional(),
|
|
70
|
+
headless: z.boolean().optional(),
|
|
71
|
+
useProfile: z.boolean().optional(),
|
|
72
|
+
allowedDomains: z.array(z.string().min(1)).max(32).optional(),
|
|
67
73
|
// Extra LOCAL origins the run may be redirected to (bead w1os.2) — e.g. an OAuth
|
|
68
74
|
// vendor on :4717 (also host.docker.internal:4717) or a callback on :4304. Each is
|
|
69
75
|
// tunnelled and redirects to it are followed through the tunnel. Only origins named
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* Shared ProjectRequired text (platform-y2ob.2): says what was observed and
|
|
3
3
|
* what to pass. Used by check_app_in_browser and trigger_crawl.
|
|
4
4
|
*/
|
|
5
|
-
export function projectRequiredMessage(repoName) {
|
|
5
|
+
export function projectRequiredMessage(repoName, acceptsProjectUuid = true) {
|
|
6
6
|
const observed = repoName
|
|
7
7
|
? `Could not determine the DebuggAI project: no project is linked to git repo "${repoName}".`
|
|
8
8
|
: 'Could not determine the DebuggAI project: this is not a git repo and no repoName was passed.';
|
|
9
9
|
return observed
|
|
10
|
-
+
|
|
10
|
+
+ (acceptsProjectUuid
|
|
11
|
+
? ' Pass `projectUuid` (list yours with the `project` tool, action "list") or `repoName` (the "org/repo" of a linked project).'
|
|
12
|
+
: ' Pass `repoName` (matches a project name, slug or linked "org/repo"; list yours with the `project` tool, action "list") or `environmentId`.');
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* A repo->project lookup that FAILED (platform-y2ob.4), as opposed to one that
|
|
@@ -15,7 +17,7 @@ export function projectRequiredMessage(repoName) {
|
|
|
15
17
|
* network error says nothing about whether a project exists. Reports the HTTP
|
|
16
18
|
* status and the backend's message verbatim.
|
|
17
19
|
*/
|
|
18
|
-
export function projectLookupFailedPayload(repoName, err) {
|
|
20
|
+
export function projectLookupFailedPayload(repoName, err, acceptsProjectUuid = true) {
|
|
19
21
|
const e = err;
|
|
20
22
|
const statusCode = typeof e?.statusCode === 'number' ? e.statusCode : undefined;
|
|
21
23
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -25,7 +27,9 @@ export function projectLookupFailedPayload(repoName, err) {
|
|
|
25
27
|
return {
|
|
26
28
|
error: 'ProjectLookupFailed',
|
|
27
29
|
message: `Could not look up the DebuggAI project for git repo "${repoName}" (${how}). `
|
|
28
|
-
+ 'This is a lookup failure, not a missing link:
|
|
30
|
+
+ 'This is a lookup failure, not a missing link: '
|
|
31
|
+
+ (acceptsProjectUuid ? 'pass `projectUuid` to skip the lookup, ' : 'pass `environmentId` to skip the lookup, ')
|
|
32
|
+
+ 'or fix the connection and retry.',
|
|
29
33
|
detail: { repoName, ...(statusCode !== undefined ? { statusCode } : {}), ...(e?.networkCode ? { networkCode: e.networkCode } : {}) },
|
|
30
34
|
};
|
|
31
35
|
}
|