fastbrowser_cli 1.0.31 → 1.0.35
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 +1 -2
- package/dist/fastbrowser_cli/fastbrowser_cli.js +11 -19
- package/dist/fastbrowser_cli/fastbrowser_cli.js.map +1 -1
- package/dist/fastbrowser_cli/libs/query-builder.d.ts +2 -0
- package/dist/fastbrowser_cli/libs/query-builder.d.ts.map +1 -1
- package/dist/fastbrowser_cli/libs/query-builder.js +4 -0
- package/dist/fastbrowser_cli/libs/query-builder.js.map +1 -1
- package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts +2 -0
- package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/fastbrowser_mcp.js +193 -32
- package/dist/fastbrowser_mcp/fastbrowser_mcp.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_my_client.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_my_client.js +8 -0
- package/dist/fastbrowser_mcp/libs/mcp_my_client.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.js +15 -2
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.d.ts +1 -0
- package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.js +33 -1
- package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/schemas.d.ts +4 -0
- package/dist/fastbrowser_mcp/libs/schemas.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/schemas.js +6 -0
- package/dist/fastbrowser_mcp/libs/schemas.js.map +1 -1
- package/dist/shared/logger.d.ts +86 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +269 -0
- package/dist/shared/logger.js.map +1 -0
- package/docs/brainstorm_scrap_by_ai.md +1 -1
- package/docs/feature_support_cli.md +7 -8
- package/docs/target_tools/target_tools_chrome_devtools.md +963 -0
- package/docs/target_tools/target_tools_playwright.md +763 -0
- package/examples/linkedin_cli/linked_dm.sh +19 -0
- package/examples/linkedin_cli/linked_post.sh +13 -0
- package/examples/linkedin_cli/linkedin.snapshot.txt +1245 -0
- package/examples/todomvc/todomvc.a11y.txt +44 -0
- package/examples/todomvc/todomvc.sh +11 -0
- package/examples/wttj_cli/fastbrowser_helper.ts +39 -0
- package/examples/wttj_cli/wttf_job-original.a11y.txt +652 -0
- package/examples/wttj_cli/wttf_job.a11y.txt +317 -0
- package/examples/{welcometothejungle/wttj-job.ts → wttj_cli/wttj_job copy.ts } +60 -11
- package/examples/wttj_cli/wttj_job.ts +179 -0
- package/examples/wttj_cli/wttj_search.ts +162 -0
- package/package.json +10 -3
- package/skills/fastbrowser/SKILL.md +10 -11
- package/skills/fastbrowser-script/SKILL.md +4 -4
- package/src/fastbrowser_cli/fastbrowser_cli.ts +14 -25
- package/src/fastbrowser_cli/libs/query-builder.ts +6 -0
- package/src/fastbrowser_mcp/fastbrowser_mcp.ts +231 -36
- package/src/fastbrowser_mcp/libs/mcp_my_client.ts +17 -0
- package/src/fastbrowser_mcp/libs/mcp_target_helper.ts +15 -2
- package/src/fastbrowser_mcp/libs/playwright_a11y_helper.ts +33 -1
- package/src/fastbrowser_mcp/libs/schemas.ts +6 -0
- package/src/shared/logger.ts +317 -0
- package/test.a11y.txt +828 -0
- package/tests/query-builder.test.ts +51 -11
- package/examples/welcometothejungle/fastbrowser_helper.ts +0 -39
- package/examples/welcometothejungle/wttj-search.ts +0 -82
- /package/examples/{post-to-x.sh → twitter_cli/twitter_post.sh} +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// node imports
|
|
4
|
-
import * as Assert from 'node:assert/strict';
|
|
5
4
|
import Fs from 'node:fs';
|
|
6
5
|
import Path from 'node:path';
|
|
7
6
|
|
|
@@ -11,6 +10,7 @@ import { z } from "zod";
|
|
|
11
10
|
import * as A11yParse from "a11y_parse";
|
|
12
11
|
|
|
13
12
|
// local imports
|
|
13
|
+
import { Logger } from "../shared/logger.js"
|
|
14
14
|
import { McpMyClient } from "./libs/mcp_my_client.js";
|
|
15
15
|
import { McpProxy } from "./libs/mcp_proxy.js";
|
|
16
16
|
import { ResponseFormatter } from "./libs/response_formatter.js";
|
|
@@ -40,6 +40,10 @@ export {
|
|
|
40
40
|
type QuerySelectorsFirstInput,
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
const logger = Logger.fromMetaUrl(import.meta.url, {
|
|
44
|
+
allToStderr: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
43
47
|
///////////////////////////////////////////////////////////////////////////////
|
|
44
48
|
///////////////////////////////////////////////////////////////////////////////
|
|
45
49
|
//
|
|
@@ -47,21 +51,62 @@ export {
|
|
|
47
51
|
///////////////////////////////////////////////////////////////////////////////
|
|
48
52
|
|
|
49
53
|
class MainHelper {
|
|
54
|
+
/**
|
|
55
|
+
* Multiple retry... not sure it is actually useful - https://github.com/jeromeetienne/skillmd_collection/issues/47
|
|
56
|
+
*
|
|
57
|
+
* @param mcpClient
|
|
58
|
+
* @returns
|
|
59
|
+
*/
|
|
50
60
|
private static async _getA11yText(mcpClient: McpMyClient): Promise<string> {
|
|
51
61
|
const mcpTarget = await mcpClient.getMcpTarget();
|
|
52
62
|
const toolConfig = await McpTargetHelper.targetToolTakeSnapshot(mcpTarget);
|
|
53
63
|
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
64
|
+
// `take_snapshot` is racy — for reasons we haven't traced (could be playwright's a11y serializer, the
|
|
65
|
+
// `@playwright/mcp` extension transport, or the chrome extension itself), back-to-back calls on an unchanged
|
|
66
|
+
// page sometimes return incomplete or empty trees. Stabilize by taking snapshots in pairs and only returning
|
|
67
|
+
// once two consecutive ones agree on node count within STABLE_TOLERANCE. On exhaustion, return best-effort
|
|
68
|
+
// rather than throw, so legitimately tiny pages don't break workflows.
|
|
69
|
+
const MAX_ATTEMPTS = 6;
|
|
70
|
+
const RETRY_DELAY_MS = 250;
|
|
71
|
+
const STABLE_TOLERANCE = 2;
|
|
72
|
+
|
|
73
|
+
let prev: { text: string; nodeCount: number } | undefined = undefined;
|
|
74
|
+
let last: { text: string; nodeCount: number } = { text: '', nodeCount: 0 };
|
|
75
|
+
|
|
76
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
77
|
+
const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
|
|
78
|
+
const text = await ResponseFormatter.formatTakeSnapshot(mcpTarget, callToolResult);
|
|
79
|
+
const nodeCount = MainHelper._countSnapshotNodes(text);
|
|
80
|
+
last = { text, nodeCount };
|
|
81
|
+
|
|
82
|
+
if (prev !== undefined && Math.abs(nodeCount - prev.nodeCount) <= STABLE_TOLERANCE) {
|
|
83
|
+
if (nodeCount === 0) {
|
|
84
|
+
logger.warn(`${mcpTarget}:take_snapshot: settled at empty after ${attempt} attempt(s) — returning empty snapshot`);
|
|
85
|
+
}
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
57
88
|
|
|
58
|
-
|
|
59
|
-
const snapshotText = await ResponseFormatter.formatTakeSnapshot(mcpTarget, callToolResult);
|
|
60
|
-
// sanity check
|
|
61
|
-
Assert.ok(snapshotText !== undefined, "Snapshot text is empty");
|
|
89
|
+
logger.warn(`${mcpTarget}:take_snapshot: attempt ${attempt}/${MAX_ATTEMPTS}: nodeCount=${nodeCount}, prev=${prev === undefined ? 'n/a' : prev.nodeCount} — not stable, retrying`);
|
|
62
90
|
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
prev = last;
|
|
92
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
logger.warn(`${mcpTarget}:take_snapshot: exhausted ${MAX_ATTEMPTS} attempts without stabilizing — returning best-effort (nodeCount=${last.nodeCount})`);
|
|
98
|
+
return last.text;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private static _countSnapshotNodes(snapshotText: string): number {
|
|
102
|
+
if (snapshotText.trim().length === 0) return 0;
|
|
103
|
+
let count = 0;
|
|
104
|
+
for (const line of snapshotText.split('\n')) {
|
|
105
|
+
if (line.trim().startsWith('uid=')) {
|
|
106
|
+
count += 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return count;
|
|
65
110
|
}
|
|
66
111
|
|
|
67
112
|
/**
|
|
@@ -106,18 +151,32 @@ class MainHelper {
|
|
|
106
151
|
selector: string,
|
|
107
152
|
selectedNodes: A11yParse.AxNode[],
|
|
108
153
|
withAncestors: boolean,
|
|
154
|
+
withChildren: boolean,
|
|
109
155
|
): string {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
156
|
+
const nodeCount = selectedNodes.length;
|
|
157
|
+
const pluralS = nodeCount > 1 ? 's' : '';
|
|
158
|
+
const ancestorsText = withAncestors ? ', with ancestors' : '';
|
|
159
|
+
const childrenText = withChildren ? ', with children' : '';
|
|
160
|
+
let text: string = `## Node${pluralS} found for selector '${selector}' (${nodeCount} node${pluralS}${ancestorsText}${childrenText}):`;
|
|
161
|
+
if (selectedNodes.length === 0) {
|
|
162
|
+
if (text.length > 0) text += '\n';
|
|
163
|
+
text += "No node found";
|
|
164
|
+
} else if (withAncestors === true) {
|
|
165
|
+
const subsetTree = A11yParse.A11yTree.buildSubsetTree(selectedNodes, {
|
|
166
|
+
withAncestors: true,
|
|
167
|
+
withChildren,
|
|
168
|
+
});
|
|
169
|
+
if (text.length > 0) text += '\n';
|
|
170
|
+
text += A11yParse.A11yDisplay.stringifyTree(subsetTree);
|
|
171
|
+
} else if (withChildren === true) {
|
|
172
|
+
for (const selectedNode of selectedNodes) {
|
|
173
|
+
if (text.length > 0) text += '\n';
|
|
174
|
+
text += A11yParse.A11yDisplay.stringifyTree(selectedNode);
|
|
117
175
|
}
|
|
118
176
|
} else {
|
|
119
177
|
for (const selectedNode of selectedNodes) {
|
|
120
|
-
text
|
|
178
|
+
if (text.length > 0) text += '\n';
|
|
179
|
+
text += A11yParse.A11yDisplay.stringifyNode(selectedNode);
|
|
121
180
|
}
|
|
122
181
|
}
|
|
123
182
|
text += '\n';
|
|
@@ -144,7 +203,7 @@ class MainHelper {
|
|
|
144
203
|
selectedNodes.splice(querySelector.limit);
|
|
145
204
|
}
|
|
146
205
|
|
|
147
|
-
responseTexts.push(this._formatSelectedNodes(querySelector.selector, selectedNodes, querySelector.withAncestors));
|
|
206
|
+
responseTexts.push(this._formatSelectedNodes(querySelector.selector, selectedNodes, querySelector.withAncestors, querySelector.withChildren));
|
|
148
207
|
}
|
|
149
208
|
|
|
150
209
|
// join the response texts for all selectors and return
|
|
@@ -167,7 +226,7 @@ class MainHelper {
|
|
|
167
226
|
for (const querySelector of querySelectors.selectors) {
|
|
168
227
|
const selectedNodes = A11yParse.A11yQuery.querySelectorAll(a11yTree, querySelector.selector);
|
|
169
228
|
const firstNode = selectedNodes.length > 0 ? [selectedNodes[0]] : [];
|
|
170
|
-
responseTexts.push(this._formatSelectedNodes(querySelector.selector, firstNode, querySelector.withAncestors));
|
|
229
|
+
responseTexts.push(this._formatSelectedNodes(querySelector.selector, firstNode, querySelector.withAncestors, querySelector.withChildren));
|
|
171
230
|
}
|
|
172
231
|
|
|
173
232
|
return responseTexts.join('\n');
|
|
@@ -195,10 +254,16 @@ class MainHelper {
|
|
|
195
254
|
inputSchema: z.object({}),
|
|
196
255
|
},
|
|
197
256
|
async () => {
|
|
257
|
+
// log the events
|
|
258
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.listPages}: listing pages`);
|
|
259
|
+
|
|
198
260
|
const toolConfig = await McpTargetHelper.targetToolListPages(mcpTarget);
|
|
199
261
|
const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
|
|
200
262
|
let outputStr = await ResponseFormatter.formatListPages(mcpTarget, callToolResult);
|
|
201
263
|
|
|
264
|
+
// log the events
|
|
265
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.listPages}: output:`)
|
|
266
|
+
logger.warn(`${outputStr}`);
|
|
202
267
|
return {
|
|
203
268
|
content: [{ type: "text", text: outputStr }],
|
|
204
269
|
};
|
|
@@ -228,11 +293,18 @@ class MainHelper {
|
|
|
228
293
|
// const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
|
|
229
294
|
// let outputStr = await ResponseFormatter.formatNewPage(mcpTarget, callToolResult, url);
|
|
230
295
|
|
|
296
|
+
// log the events
|
|
297
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.newPage}: url=${url}`);
|
|
298
|
+
|
|
299
|
+
|
|
231
300
|
// so working around this by calling the navigate_page tool instead of new_page when the target is playwright
|
|
232
301
|
const toolConfig = await McpTargetHelper.targetToolNavigatePage(mcpTarget, url);
|
|
233
302
|
const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
|
|
234
303
|
let outputStr = await ResponseFormatter.formatNavigatePage(mcpTarget, callToolResult);
|
|
235
304
|
|
|
305
|
+
// log the events
|
|
306
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.newPage}: output:`)
|
|
307
|
+
logger.warn(`${outputStr}`);
|
|
236
308
|
|
|
237
309
|
return {
|
|
238
310
|
content: [{ type: "text", text: outputStr }],
|
|
@@ -255,10 +327,17 @@ class MainHelper {
|
|
|
255
327
|
}),
|
|
256
328
|
},
|
|
257
329
|
async ({ pageId }: { pageId: number }) => {
|
|
330
|
+
// log the events
|
|
331
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.closePage}: pageId=${pageId}`);
|
|
332
|
+
|
|
258
333
|
const toolConfig = await McpTargetHelper.targetToolClosePage(mcpTarget, pageId);
|
|
259
334
|
const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
|
|
260
335
|
let outputStr = await ResponseFormatter.formatClosePage(mcpTarget, callToolResult, pageId);
|
|
261
336
|
|
|
337
|
+
// log the events
|
|
338
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.closePage}: output:`)
|
|
339
|
+
logger.warn(`${outputStr}`);
|
|
340
|
+
|
|
262
341
|
return {
|
|
263
342
|
content: [{ type: "text", text: outputStr }],
|
|
264
343
|
};
|
|
@@ -280,10 +359,17 @@ class MainHelper {
|
|
|
280
359
|
}),
|
|
281
360
|
},
|
|
282
361
|
async ({ url }: { url: string }) => {
|
|
362
|
+
// log the events
|
|
363
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.navigatePage}: url=${url}`);
|
|
364
|
+
|
|
283
365
|
const toolConfig = await McpTargetHelper.targetToolNavigatePage(mcpTarget, url);
|
|
284
366
|
const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
|
|
285
367
|
let outputStr = await ResponseFormatter.formatNavigatePage(mcpTarget, callToolResult);
|
|
286
368
|
|
|
369
|
+
// log the events
|
|
370
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.navigatePage}: output:`)
|
|
371
|
+
logger.warn(`${outputStr}`);
|
|
372
|
+
|
|
287
373
|
return {
|
|
288
374
|
content: [{ type: "text", text: outputStr }],
|
|
289
375
|
};
|
|
@@ -303,9 +389,16 @@ class MainHelper {
|
|
|
303
389
|
inputSchema: z.object({}),
|
|
304
390
|
},
|
|
305
391
|
async () => {
|
|
392
|
+
// log the events
|
|
393
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.takeSnapshot}: taking snapshot`);
|
|
394
|
+
|
|
306
395
|
const a11yText: string = await MainHelper._getA11yText(mcpClient);
|
|
307
396
|
let outputStr = a11yText
|
|
308
397
|
|
|
398
|
+
// log the events
|
|
399
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.takeSnapshot}: output:`);
|
|
400
|
+
logger.warn(`${outputStr}`);
|
|
401
|
+
|
|
309
402
|
return {
|
|
310
403
|
content: [{ type: "text", text: outputStr }],
|
|
311
404
|
};
|
|
@@ -342,9 +435,16 @@ class MainHelper {
|
|
|
342
435
|
inputSchema: QuerySelectorsInputSchema,
|
|
343
436
|
},
|
|
344
437
|
async (querySelectorsInput: QuerySelectorsInput) => {
|
|
438
|
+
// log the events
|
|
439
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.querySelectorsAll}: querying selectors: ${JSON.stringify(querySelectorsInput)}`);
|
|
440
|
+
|
|
345
441
|
// query the accessibility tree with the provided selector
|
|
346
442
|
const outputText: string = await MainHelper.querySelectorsAll(mcpClient, querySelectorsInput);
|
|
347
443
|
|
|
444
|
+
// log the events
|
|
445
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.querySelectorsAll}: output:`);
|
|
446
|
+
logger.warn(`${outputText}`);
|
|
447
|
+
|
|
348
448
|
return {
|
|
349
449
|
content: [{ type: "text", text: outputText }],
|
|
350
450
|
};
|
|
@@ -367,7 +467,15 @@ class MainHelper {
|
|
|
367
467
|
inputSchema: QuerySelectorsFirstInputSchema,
|
|
368
468
|
},
|
|
369
469
|
async (querySelectorsInput: QuerySelectorsFirstInput) => {
|
|
470
|
+
// log the events
|
|
471
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.querySelectors}: querying selectors: ${JSON.stringify(querySelectorsInput)}`);
|
|
472
|
+
|
|
370
473
|
const outputText: string = await MainHelper.querySelectors(mcpClient, querySelectorsInput);
|
|
474
|
+
|
|
475
|
+
// log the events
|
|
476
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.querySelectors}: output:`);
|
|
477
|
+
logger.warn(`${outputText}`);
|
|
478
|
+
|
|
371
479
|
return {
|
|
372
480
|
content: [{ type: "text", text: outputText }],
|
|
373
481
|
};
|
|
@@ -389,6 +497,9 @@ class MainHelper {
|
|
|
389
497
|
}),
|
|
390
498
|
},
|
|
391
499
|
async ({ keys }: { keys: string }) => {
|
|
500
|
+
// log the events
|
|
501
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.pressKeys}: pressing keys: ${keys}`);
|
|
502
|
+
|
|
392
503
|
// Build the list of keys to send, splitting regular characters into individual key presses, but keeping special keys as-is
|
|
393
504
|
const keysToSend: string[] = [];
|
|
394
505
|
const keysSplit = keys.split(',').map((key) => key.trim());
|
|
@@ -419,6 +530,10 @@ class MainHelper {
|
|
|
419
530
|
|
|
420
531
|
let outputText = await ResponseFormatter.formatPressKeys(mcpTarget, keysToSend);
|
|
421
532
|
|
|
533
|
+
// log the events
|
|
534
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.pressKeys}: output:`);
|
|
535
|
+
logger.warn(`${outputText}`);
|
|
536
|
+
|
|
422
537
|
// return a response indicating which keys were pressed
|
|
423
538
|
return {
|
|
424
539
|
content: [{ type: "text", text: outputText }],
|
|
@@ -444,12 +559,17 @@ class MainHelper {
|
|
|
444
559
|
},
|
|
445
560
|
},
|
|
446
561
|
async ({ selector }: { selector: string }) => {
|
|
447
|
-
|
|
562
|
+
// log the events
|
|
563
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.click}: clicking selector: ${selector}`);
|
|
448
564
|
|
|
565
|
+
const uid = await MainHelper._resolveSelectorToUid(mcpClient, selector);
|
|
449
566
|
const toolConfig = await McpTargetHelper.targetToolClick(mcpTarget, uid);
|
|
450
567
|
const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
|
|
451
568
|
let outputText = await ResponseFormatter.formatClick(mcpTarget, callToolResult);
|
|
452
569
|
|
|
570
|
+
// log the events
|
|
571
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.click}: output:`);
|
|
572
|
+
|
|
453
573
|
return {
|
|
454
574
|
content: [{ type: "text", text: outputText }],
|
|
455
575
|
};
|
|
@@ -477,6 +597,9 @@ class MainHelper {
|
|
|
477
597
|
},
|
|
478
598
|
},
|
|
479
599
|
async ({ elements }: { elements: { selector: string; value: string }[] }) => {
|
|
600
|
+
// log the events
|
|
601
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.fillForm}: filling form with elements: ${JSON.stringify(elements)}`);
|
|
602
|
+
|
|
480
603
|
const resolved = [];
|
|
481
604
|
for (const element of elements) {
|
|
482
605
|
const uid = await MainHelper._resolveSelectorToUid(mcpClient, element.selector);
|
|
@@ -485,32 +608,36 @@ class MainHelper {
|
|
|
485
608
|
value: element.value,
|
|
486
609
|
});
|
|
487
610
|
}
|
|
611
|
+
let callToolResult: CallToolResult;
|
|
488
612
|
if (mcpTarget === 'chrome_devtools') {
|
|
489
613
|
const toolConfig = await McpTargetHelper.targetToolFillForm(mcpTarget, resolved);
|
|
490
|
-
|
|
491
|
-
// const callToolResult = await mcpClient.callTool('fill_form', { elements: resolved });
|
|
492
|
-
return callToolResult
|
|
614
|
+
callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
|
|
493
615
|
} else if (mcpTarget === 'playwright') {
|
|
494
616
|
type Field = {
|
|
495
617
|
name: string; // Human readable name for the field, e.g. "Email address"
|
|
496
618
|
// type can be textbox, checkbox, radio, combobox, or slider
|
|
497
619
|
type: 'textbox' | 'checkbox' | 'radio' | 'combobox' | 'slider';
|
|
498
620
|
// the uid of the field's corresponding node in the accessibility tree
|
|
499
|
-
|
|
621
|
+
target: string;
|
|
500
622
|
// the value to fill into the field - for checkboxes this can be "checked" or "unchecked", for radio buttons this can be "selected", for comboboxes this can be the option to select, and for sliders this can be the value to set the slider to
|
|
501
623
|
value: string
|
|
502
624
|
};
|
|
503
625
|
const fields: Field[] = resolved.map((element, index) => ({
|
|
504
626
|
name: `Field ${index + 1}`,
|
|
505
627
|
type: 'textbox', // for simplicity, we assume all fields are textboxes in this example
|
|
506
|
-
|
|
628
|
+
target: element.uid,
|
|
507
629
|
value: element.value,
|
|
508
630
|
}));
|
|
509
|
-
|
|
510
|
-
return callToolResult
|
|
631
|
+
callToolResult = await mcpClient.callTool('browser_fill_form', { fields: fields });
|
|
511
632
|
} else {
|
|
512
633
|
throw new Error(`Unsupported MCP target: ${mcpTarget}`);
|
|
513
634
|
}
|
|
635
|
+
|
|
636
|
+
// log the events
|
|
637
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.fillForm}: output:`);
|
|
638
|
+
logger.warn(`${JSON.stringify(callToolResult)}`);
|
|
639
|
+
|
|
640
|
+
return callToolResult
|
|
514
641
|
}
|
|
515
642
|
);
|
|
516
643
|
|
|
@@ -530,6 +657,10 @@ class MainHelper {
|
|
|
530
657
|
inputSchema: timezoneSchema,
|
|
531
658
|
},
|
|
532
659
|
async ({ timezone }) => {
|
|
660
|
+
// log the events
|
|
661
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.getCurrentDateTime}: getting current date and time with timezone: ${timezone ?? 'local system timezone'}`);
|
|
662
|
+
|
|
663
|
+
|
|
533
664
|
const date = new Date();
|
|
534
665
|
const options: Intl.DateTimeFormatOptions = {
|
|
535
666
|
timeZone: timezone,
|
|
@@ -542,6 +673,10 @@ class MainHelper {
|
|
|
542
673
|
timeZoneName: "short",
|
|
543
674
|
};
|
|
544
675
|
const formatted = new Intl.DateTimeFormat("en-US", options).format(date);
|
|
676
|
+
|
|
677
|
+
// log the events
|
|
678
|
+
logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.getCurrentDateTime}: output: ${formatted}`);
|
|
679
|
+
|
|
545
680
|
return {
|
|
546
681
|
content: [{ type: "text", text: formatted }],
|
|
547
682
|
};
|
|
@@ -564,6 +699,11 @@ class MainHelper {
|
|
|
564
699
|
mcpTarget: FastBrowserMcpTarget,
|
|
565
700
|
verbose?: boolean
|
|
566
701
|
}): Promise<void> {
|
|
702
|
+
// Redirect process.stderr to a log file, so that the MCP communication is not polluted by logs.
|
|
703
|
+
const logFile = Path.resolve(import.meta.dirname, `../../outputs/fastbrowser_mcp_${new Date().toISOString()}.log`);
|
|
704
|
+
const logStream = Fs.createWriteStream(logFile, { flags: 'a' });
|
|
705
|
+
process.stderr.write = logStream.write.bind(logStream);
|
|
706
|
+
|
|
567
707
|
///////////////////////////////////////////////////////////////////////////////
|
|
568
708
|
///////////////////////////////////////////////////////////////////////////////
|
|
569
709
|
// mcp client
|
|
@@ -616,11 +756,13 @@ class MainHelper {
|
|
|
616
756
|
{
|
|
617
757
|
selector: "link, button",
|
|
618
758
|
withAncestors: true,
|
|
759
|
+
withChildren: false,
|
|
619
760
|
limit: 0,
|
|
620
761
|
},
|
|
621
762
|
{
|
|
622
763
|
selector: 'heading[level="1"]',
|
|
623
764
|
withAncestors: false,
|
|
765
|
+
withChildren: false,
|
|
624
766
|
limit: 0,
|
|
625
767
|
},
|
|
626
768
|
],
|
|
@@ -646,19 +788,64 @@ class MainHelper {
|
|
|
646
788
|
for (const toolName of toolsToProxys) {
|
|
647
789
|
await mcpProxy.proxyToolCall(mcpClient, toolName)
|
|
648
790
|
}
|
|
791
|
+
|
|
649
792
|
///////////////////////////////////////////////////////////////////////////////
|
|
650
793
|
///////////////////////////////////////////////////////////////////////////////
|
|
651
794
|
// .querySelectorsAll tool implementation
|
|
652
795
|
///////////////////////////////////////////////////////////////////////////////
|
|
653
796
|
///////////////////////////////////////////////////////////////////////////////
|
|
654
|
-
const mcpServer = await mcpProxy.getMcpServer();
|
|
655
797
|
|
|
798
|
+
const mcpServer = await mcpProxy.getMcpServer();
|
|
656
799
|
await MainHelper.initExternalTools(mcpServer, mcpClient);
|
|
657
800
|
|
|
658
|
-
|
|
659
801
|
// Connect the MCP proxy server to start accepting connections from MCP clients (e.g. LLM agents).
|
|
660
802
|
await mcpProxy.connect();
|
|
661
803
|
}
|
|
804
|
+
|
|
805
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
806
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
807
|
+
// .commandTargetTools: connect to the target MCP and print each tool's name, description, and input schema.
|
|
808
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
809
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
810
|
+
|
|
811
|
+
static async commandTargetTools({
|
|
812
|
+
mcpTarget,
|
|
813
|
+
}: {
|
|
814
|
+
mcpTarget: FastBrowserMcpTarget,
|
|
815
|
+
}): Promise<void> {
|
|
816
|
+
const { command: mcpCommand, args: mcpArgs } = McpTargetHelper.mcpArgs(mcpTarget);
|
|
817
|
+
|
|
818
|
+
const mcpClient = new McpMyClient({
|
|
819
|
+
name: 'fastbrowser_target_tools_client',
|
|
820
|
+
version: '1.0.0',
|
|
821
|
+
mcpTarget,
|
|
822
|
+
transport: {
|
|
823
|
+
type: 'stdio',
|
|
824
|
+
command: mcpCommand,
|
|
825
|
+
args: mcpArgs,
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
await mcpClient.connect();
|
|
830
|
+
try {
|
|
831
|
+
const tools = await mcpClient.listTools();
|
|
832
|
+
console.log(`# Tools available on MCP target '${mcpTarget}' (${tools.length})\n`);
|
|
833
|
+
for (const tool of tools) {
|
|
834
|
+
console.log(`## ${tool.name}`);
|
|
835
|
+
console.log('');
|
|
836
|
+
console.log(`### Description`)
|
|
837
|
+
console.log(`${tool.description ?? '(no description)'}`);
|
|
838
|
+
console.log('');
|
|
839
|
+
console.log(`### Input schema`);
|
|
840
|
+
console.log("```");
|
|
841
|
+
console.log(JSON.stringify(tool.inputSchema, null, 2));
|
|
842
|
+
console.log("```");
|
|
843
|
+
console.log('');
|
|
844
|
+
}
|
|
845
|
+
} finally {
|
|
846
|
+
await mcpClient.close();
|
|
847
|
+
}
|
|
848
|
+
}
|
|
662
849
|
}
|
|
663
850
|
|
|
664
851
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -668,12 +855,6 @@ class MainHelper {
|
|
|
668
855
|
///////////////////////////////////////////////////////////////////////////////
|
|
669
856
|
|
|
670
857
|
async function main() {
|
|
671
|
-
// Redirect process.stderr to a log file, so that the MCP communication is not polluted by logs.
|
|
672
|
-
const logFile = Path.resolve(import.meta.dirname, `../../outputs/fastbrowser_mcp_${new Date().toISOString()}.log`);
|
|
673
|
-
const logStream = Fs.createWriteStream(logFile, { flags: 'a' });
|
|
674
|
-
process.stderr.write = logStream.write.bind(logStream);
|
|
675
|
-
|
|
676
|
-
|
|
677
858
|
// throw Error("This entry point is not meant to be run directly. Please run one of the npm scripts defined in package.json, e.g. 'npm run start:fastbrowser_mcp' or 'npm run inspect:fastbrowser_mcp:chrome_devtools'");
|
|
678
859
|
|
|
679
860
|
const program = new Command();
|
|
@@ -693,6 +874,20 @@ async function main() {
|
|
|
693
874
|
});
|
|
694
875
|
});
|
|
695
876
|
|
|
877
|
+
program
|
|
878
|
+
.command('target_tools')
|
|
879
|
+
.description('List the tools exposed by the target MCP (name, description, input schema)')
|
|
880
|
+
.addOption(
|
|
881
|
+
new Option('-b, --mcp_target <mcpTarget>', 'the MCP target to introspect')
|
|
882
|
+
.choices(['chrome_devtools', 'playwright'])
|
|
883
|
+
.default('playwright')
|
|
884
|
+
)
|
|
885
|
+
.action(async (options: { mcp_target: FastBrowserMcpTarget }) => {
|
|
886
|
+
await MainHelper.commandTargetTools({
|
|
887
|
+
mcpTarget: options.mcp_target,
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
|
|
696
891
|
// display help if no command is provided
|
|
697
892
|
if (process.argv.length < 3) {
|
|
698
893
|
program.help();
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
1
3
|
// node import
|
|
4
|
+
import { Logger } from "../../shared/logger.js"
|
|
2
5
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
6
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
7
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
@@ -6,6 +9,12 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
|
6
9
|
import type { CallToolResult, Prompt, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
7
10
|
import { FastBrowserMcpTarget } from "../fastbrowser_types.js";
|
|
8
11
|
|
|
12
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
13
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
14
|
+
//
|
|
15
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
16
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
17
|
+
|
|
9
18
|
export type StdioConfig = {
|
|
10
19
|
type: "stdio";
|
|
11
20
|
command: string;
|
|
@@ -34,6 +43,10 @@ export interface McpClientOptions {
|
|
|
34
43
|
///////////////////////////////////////////////////////////////////////////////
|
|
35
44
|
///////////////////////////////////////////////////////////////////////////////
|
|
36
45
|
|
|
46
|
+
const logger = Logger.fromMetaUrl(import.meta.url, {
|
|
47
|
+
allToStderr: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
37
50
|
// TODO remove this function - it seems to do vastly nothing - more confusing that helpful
|
|
38
51
|
// - i use the McpServer directly, and i got a wrapper for the client... discrepancy is confusing
|
|
39
52
|
export class McpMyClient {
|
|
@@ -57,9 +70,11 @@ export class McpMyClient {
|
|
|
57
70
|
|
|
58
71
|
async connect(): Promise<void> {
|
|
59
72
|
if (this.connected) return;
|
|
73
|
+
logger.warn(`McpMyClient:connect: Connecting to MCP with target ${this.mcpTarget}`);
|
|
60
74
|
|
|
61
75
|
this.transport = this.createTransport(this.options.transport);
|
|
62
76
|
await this.client.connect(this.transport);
|
|
77
|
+
logger.warn(`McpMyClient:connect: Connected to MCP with target ${this.mcpTarget}`);
|
|
63
78
|
this.connected = true;
|
|
64
79
|
}
|
|
65
80
|
|
|
@@ -72,6 +87,7 @@ export class McpMyClient {
|
|
|
72
87
|
async listTools(): Promise<Tool[]> {
|
|
73
88
|
this.assertConnected();
|
|
74
89
|
const { tools } = await this.client.listTools();
|
|
90
|
+
logger.warn(`McpMyClient:listTools: ${JSON.stringify(tools, null, 2)}`);
|
|
75
91
|
return tools;
|
|
76
92
|
}
|
|
77
93
|
|
|
@@ -80,6 +96,7 @@ export class McpMyClient {
|
|
|
80
96
|
args: Record<string, unknown> = {},
|
|
81
97
|
): Promise<CallToolResult> {
|
|
82
98
|
this.assertConnected();
|
|
99
|
+
logger.warn(`McpMyClient:callTool: name: ${name} args: ${JSON.stringify(args, null, 2)}`);
|
|
83
100
|
return this.client.callTool({ name, arguments: args }) as Promise<CallToolResult>;
|
|
84
101
|
}
|
|
85
102
|
|
|
@@ -182,9 +182,21 @@ export class McpTargetHelper {
|
|
|
182
182
|
|
|
183
183
|
static async targetToolClick(mcpTarget: FastBrowserMcpTarget, uid: string): Promise<TargetToolConfig> {
|
|
184
184
|
if (mcpTarget === 'chrome_devtools') {
|
|
185
|
-
|
|
185
|
+
const toolConfig: TargetToolConfig = {
|
|
186
|
+
toolName: 'click',
|
|
187
|
+
toolArgs: {
|
|
188
|
+
uid
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
return toolConfig;
|
|
186
192
|
} else if (mcpTarget === 'playwright') {
|
|
187
|
-
|
|
193
|
+
const toolConfig: TargetToolConfig = {
|
|
194
|
+
toolName: 'browser_click',
|
|
195
|
+
toolArgs: {
|
|
196
|
+
target: uid
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
return toolConfig;
|
|
188
200
|
} else {
|
|
189
201
|
throw new Error(`Unsupported MCP target: ${mcpTarget}`);
|
|
190
202
|
}
|
|
@@ -194,6 +206,7 @@ export class McpTargetHelper {
|
|
|
194
206
|
if (mcpTarget === 'chrome_devtools') {
|
|
195
207
|
return { toolName: 'fill_form', toolArgs: { elements } };
|
|
196
208
|
} else if (mcpTarget === 'playwright') {
|
|
209
|
+
// FIXME browser_fill_form tool exist in playwright MCP... implement it later
|
|
197
210
|
throw new Error(`Fill form tool is not supported for Playwright MCP target yet`);
|
|
198
211
|
} else {
|
|
199
212
|
throw new Error(`Unsupported MCP target: ${mcpTarget}`);
|
|
@@ -115,7 +115,39 @@ export class PlaywrightA11yConverter {
|
|
|
115
115
|
stack.push(node);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
// Playwright's `browser_snapshot` response often interleaves the a11y tree with preamble lines like
|
|
119
|
+
// `- Page URL: ...`, `- Page Title: ...`, `- Console: 193 errors, 0 warnings`, plus trailing entries
|
|
120
|
+
// like `- New`. Each of those parses as a spurious top-level (indent=0) node. Downstream,
|
|
121
|
+
// `A11yParse.A11yTree.parse` silently overwrites `root` for every indent-0 line and returns only the
|
|
122
|
+
// LAST top-level subtree — so the real a11y page tree (usually a large `generic [active]` subtree
|
|
123
|
+
// in the middle) gets discarded and queries return zero hits even though the data is in the text.
|
|
124
|
+
// Pick the largest top-level subtree, which is reliably the actual page tree since the metadata
|
|
125
|
+
// subtrees are tiny (0–1 nodes).
|
|
126
|
+
const primary = PlaywrightA11yConverter._pickPrimarySubtree(allNodes);
|
|
127
|
+
return primary.map((node) => PlaywrightA11yConverter._stringifyNode(node)).join('\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private static _pickPrimarySubtree(allNodes: EmittedNode[]): EmittedNode[] {
|
|
131
|
+
if (allNodes.length === 0) return allNodes;
|
|
132
|
+
|
|
133
|
+
const topLevelIndices: number[] = [];
|
|
134
|
+
for (let i = 0; i < allNodes.length; i++) {
|
|
135
|
+
if (allNodes[i].indent === 0) topLevelIndices.push(i);
|
|
136
|
+
}
|
|
137
|
+
if (topLevelIndices.length <= 1) return allNodes;
|
|
138
|
+
|
|
139
|
+
let bestStart = topLevelIndices[0];
|
|
140
|
+
let bestSize = 0;
|
|
141
|
+
for (let k = 0; k < topLevelIndices.length; k++) {
|
|
142
|
+
const start = topLevelIndices[k];
|
|
143
|
+
const end = k + 1 < topLevelIndices.length ? topLevelIndices[k + 1] : allNodes.length;
|
|
144
|
+
const size = end - start;
|
|
145
|
+
if (size > bestSize) {
|
|
146
|
+
bestSize = size;
|
|
147
|
+
bestStart = start;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return allNodes.slice(bestStart, bestStart + bestSize);
|
|
119
151
|
}
|
|
120
152
|
|
|
121
153
|
///////////////////////////////////////////////////////////////////////////////
|