fastbrowser_cli 1.0.30 → 1.0.33

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.
Files changed (56) hide show
  1. package/README.md +1 -2
  2. package/dist/fastbrowser_cli/fastbrowser_cli.js +11 -19
  3. package/dist/fastbrowser_cli/fastbrowser_cli.js.map +1 -1
  4. package/dist/fastbrowser_cli/libs/query-builder.d.ts +2 -0
  5. package/dist/fastbrowser_cli/libs/query-builder.d.ts.map +1 -1
  6. package/dist/fastbrowser_cli/libs/query-builder.js +4 -0
  7. package/dist/fastbrowser_cli/libs/query-builder.js.map +1 -1
  8. package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts +2 -0
  9. package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts.map +1 -1
  10. package/dist/fastbrowser_mcp/fastbrowser_mcp.d.ts.map +1 -1
  11. package/dist/fastbrowser_mcp/fastbrowser_mcp.js +147 -22
  12. package/dist/fastbrowser_mcp/fastbrowser_mcp.js.map +1 -1
  13. package/dist/fastbrowser_mcp/libs/mcp_my_client.d.ts.map +1 -1
  14. package/dist/fastbrowser_mcp/libs/mcp_my_client.js +8 -0
  15. package/dist/fastbrowser_mcp/libs/mcp_my_client.js.map +1 -1
  16. package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts.map +1 -1
  17. package/dist/fastbrowser_mcp/libs/mcp_target_helper.js +15 -2
  18. package/dist/fastbrowser_mcp/libs/mcp_target_helper.js.map +1 -1
  19. package/dist/fastbrowser_mcp/libs/schemas.d.ts +4 -0
  20. package/dist/fastbrowser_mcp/libs/schemas.d.ts.map +1 -1
  21. package/dist/fastbrowser_mcp/libs/schemas.js +6 -0
  22. package/dist/fastbrowser_mcp/libs/schemas.js.map +1 -1
  23. package/dist/shared/logger.d.ts +86 -0
  24. package/dist/shared/logger.d.ts.map +1 -0
  25. package/dist/shared/logger.js +269 -0
  26. package/dist/shared/logger.js.map +1 -0
  27. package/docs/brainstorm_scrap_by_ai.md +1 -1
  28. package/docs/feature_support_cli.md +7 -8
  29. package/docs/target_tools/target_tools_chrome_devtools.md +963 -0
  30. package/docs/target_tools/target_tools_playwright.md +763 -0
  31. package/examples/linkedin_cli/linked_dm.sh +16 -0
  32. package/examples/linkedin_cli/linked_post.sh +13 -0
  33. package/examples/linkedin_cli/linkedin.snapshot.txt +1245 -0
  34. package/examples/todomvc/todomvc.a11y.txt +44 -0
  35. package/examples/todomvc/todomvc.sh +11 -0
  36. package/examples/wttj_cli/fastbrowser_helper.ts +39 -0
  37. package/examples/wttj_cli/wttf_job-original.a11y.txt +652 -0
  38. package/examples/wttj_cli/wttf_job.a11y.txt +317 -0
  39. package/examples/{welcometothejungle/wttj-job.ts → wttj_cli/wttj_job copy.ts } +60 -11
  40. package/examples/wttj_cli/wttj_job.ts +179 -0
  41. package/examples/wttj_cli/wttj_search.ts +162 -0
  42. package/package.json +10 -4
  43. package/skills/fastbrowser/SKILL.md +10 -11
  44. package/skills/fastbrowser-script/SKILL.md +4 -4
  45. package/src/fastbrowser_cli/fastbrowser_cli.ts +14 -25
  46. package/src/fastbrowser_cli/libs/query-builder.ts +6 -0
  47. package/src/fastbrowser_mcp/fastbrowser_mcp.ts +181 -26
  48. package/src/fastbrowser_mcp/libs/mcp_my_client.ts +17 -0
  49. package/src/fastbrowser_mcp/libs/mcp_target_helper.ts +15 -2
  50. package/src/fastbrowser_mcp/libs/schemas.ts +6 -0
  51. package/src/shared/logger.ts +317 -0
  52. package/test.a11y.txt +828 -0
  53. package/tests/query-builder.test.ts +51 -11
  54. package/examples/welcometothejungle/fastbrowser_helper.ts +0 -39
  55. package/examples/welcometothejungle/wttj-search.ts +0 -82
  56. /package/examples/{post-to-x.sh → twitter_cli/twitter_post.sh} +0 -0
@@ -11,6 +11,7 @@ import { z } from "zod";
11
11
  import * as A11yParse from "a11y_parse";
12
12
 
13
13
  // local imports
14
+ import { Logger } from "../shared/logger.js"
14
15
  import { McpMyClient } from "./libs/mcp_my_client.js";
15
16
  import { McpProxy } from "./libs/mcp_proxy.js";
16
17
  import { ResponseFormatter } from "./libs/response_formatter.js";
@@ -40,6 +41,10 @@ export {
40
41
  type QuerySelectorsFirstInput,
41
42
  };
42
43
 
44
+ const logger = Logger.fromMetaUrl(import.meta.url, {
45
+ allToStderr: true,
46
+ });
47
+
43
48
  ///////////////////////////////////////////////////////////////////////////////
44
49
  ///////////////////////////////////////////////////////////////////////////////
45
50
  //
@@ -106,18 +111,32 @@ class MainHelper {
106
111
  selector: string,
107
112
  selectedNodes: A11yParse.AxNode[],
108
113
  withAncestors: boolean,
114
+ withChildren: boolean,
109
115
  ): string {
110
- let text: string = `## Node found for selector '${selector}' (${selectedNodes.length} node${selectedNodes.length > 1 ? 's' : ''}${withAncestors ? ', with ancestors' : ''}):\n`;
111
- if (withAncestors) {
112
- if (selectedNodes.length === 0) {
113
- text += "No node found";
114
- } else {
115
- const ancestorTree = A11yParse.A11yTree.buildAncestorTree(selectedNodes);
116
- text += A11yParse.A11yTree.stringify(ancestorTree);
116
+ const nodeCount = selectedNodes.length;
117
+ const pluralS = nodeCount > 1 ? 's' : '';
118
+ const ancestorsText = withAncestors ? ', with ancestors' : '';
119
+ const childrenText = withChildren ? ', with children' : '';
120
+ let text: string = `## Node${pluralS} found for selector '${selector}' (${nodeCount} node${pluralS}${ancestorsText}${childrenText}):`;
121
+ if (selectedNodes.length === 0) {
122
+ if (text.length > 0) text += '\n';
123
+ text += "No node found";
124
+ } else if (withAncestors === true) {
125
+ const subsetTree = A11yParse.A11yTree.buildSubsetTree(selectedNodes, {
126
+ withAncestors: true,
127
+ withChildren,
128
+ });
129
+ if (text.length > 0) text += '\n';
130
+ text += A11yParse.A11yDisplay.stringifyTree(subsetTree);
131
+ } else if (withChildren === true) {
132
+ for (const selectedNode of selectedNodes) {
133
+ if (text.length > 0) text += '\n';
134
+ text += A11yParse.A11yDisplay.stringifyTree(selectedNode);
117
135
  }
118
136
  } else {
119
137
  for (const selectedNode of selectedNodes) {
120
- text += A11yParse.A11yTree.stringify(selectedNode) + '\n';
138
+ if (text.length > 0) text += '\n';
139
+ text += A11yParse.A11yDisplay.stringifyNode(selectedNode);
121
140
  }
122
141
  }
123
142
  text += '\n';
@@ -144,7 +163,7 @@ class MainHelper {
144
163
  selectedNodes.splice(querySelector.limit);
145
164
  }
146
165
 
147
- responseTexts.push(this._formatSelectedNodes(querySelector.selector, selectedNodes, querySelector.withAncestors));
166
+ responseTexts.push(this._formatSelectedNodes(querySelector.selector, selectedNodes, querySelector.withAncestors, querySelector.withChildren));
148
167
  }
149
168
 
150
169
  // join the response texts for all selectors and return
@@ -167,7 +186,7 @@ class MainHelper {
167
186
  for (const querySelector of querySelectors.selectors) {
168
187
  const selectedNodes = A11yParse.A11yQuery.querySelectorAll(a11yTree, querySelector.selector);
169
188
  const firstNode = selectedNodes.length > 0 ? [selectedNodes[0]] : [];
170
- responseTexts.push(this._formatSelectedNodes(querySelector.selector, firstNode, querySelector.withAncestors));
189
+ responseTexts.push(this._formatSelectedNodes(querySelector.selector, firstNode, querySelector.withAncestors, querySelector.withChildren));
171
190
  }
172
191
 
173
192
  return responseTexts.join('\n');
@@ -195,10 +214,16 @@ class MainHelper {
195
214
  inputSchema: z.object({}),
196
215
  },
197
216
  async () => {
217
+ // log the events
218
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.listPages}: listing pages`);
219
+
198
220
  const toolConfig = await McpTargetHelper.targetToolListPages(mcpTarget);
199
221
  const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
200
222
  let outputStr = await ResponseFormatter.formatListPages(mcpTarget, callToolResult);
201
223
 
224
+ // log the events
225
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.listPages}: output:`)
226
+ logger.warn(`${outputStr}`);
202
227
  return {
203
228
  content: [{ type: "text", text: outputStr }],
204
229
  };
@@ -228,11 +253,18 @@ class MainHelper {
228
253
  // const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
229
254
  // let outputStr = await ResponseFormatter.formatNewPage(mcpTarget, callToolResult, url);
230
255
 
256
+ // log the events
257
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.newPage}: url=${url}`);
258
+
259
+
231
260
  // so working around this by calling the navigate_page tool instead of new_page when the target is playwright
232
261
  const toolConfig = await McpTargetHelper.targetToolNavigatePage(mcpTarget, url);
233
262
  const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
234
263
  let outputStr = await ResponseFormatter.formatNavigatePage(mcpTarget, callToolResult);
235
264
 
265
+ // log the events
266
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.newPage}: output:`)
267
+ logger.warn(`${outputStr}`);
236
268
 
237
269
  return {
238
270
  content: [{ type: "text", text: outputStr }],
@@ -255,10 +287,17 @@ class MainHelper {
255
287
  }),
256
288
  },
257
289
  async ({ pageId }: { pageId: number }) => {
290
+ // log the events
291
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.closePage}: pageId=${pageId}`);
292
+
258
293
  const toolConfig = await McpTargetHelper.targetToolClosePage(mcpTarget, pageId);
259
294
  const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
260
295
  let outputStr = await ResponseFormatter.formatClosePage(mcpTarget, callToolResult, pageId);
261
296
 
297
+ // log the events
298
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.closePage}: output:`)
299
+ logger.warn(`${outputStr}`);
300
+
262
301
  return {
263
302
  content: [{ type: "text", text: outputStr }],
264
303
  };
@@ -280,10 +319,17 @@ class MainHelper {
280
319
  }),
281
320
  },
282
321
  async ({ url }: { url: string }) => {
322
+ // log the events
323
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.navigatePage}: url=${url}`);
324
+
283
325
  const toolConfig = await McpTargetHelper.targetToolNavigatePage(mcpTarget, url);
284
326
  const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
285
327
  let outputStr = await ResponseFormatter.formatNavigatePage(mcpTarget, callToolResult);
286
328
 
329
+ // log the events
330
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.navigatePage}: output:`)
331
+ logger.warn(`${outputStr}`);
332
+
287
333
  return {
288
334
  content: [{ type: "text", text: outputStr }],
289
335
  };
@@ -303,9 +349,16 @@ class MainHelper {
303
349
  inputSchema: z.object({}),
304
350
  },
305
351
  async () => {
352
+ // log the events
353
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.takeSnapshot}: taking snapshot`);
354
+
306
355
  const a11yText: string = await MainHelper._getA11yText(mcpClient);
307
356
  let outputStr = a11yText
308
357
 
358
+ // log the events
359
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.takeSnapshot}: output:`);
360
+ logger.warn(`${outputStr}`);
361
+
309
362
  return {
310
363
  content: [{ type: "text", text: outputStr }],
311
364
  };
@@ -342,9 +395,16 @@ class MainHelper {
342
395
  inputSchema: QuerySelectorsInputSchema,
343
396
  },
344
397
  async (querySelectorsInput: QuerySelectorsInput) => {
398
+ // log the events
399
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.querySelectorsAll}: querying selectors: ${JSON.stringify(querySelectorsInput)}`);
400
+
345
401
  // query the accessibility tree with the provided selector
346
402
  const outputText: string = await MainHelper.querySelectorsAll(mcpClient, querySelectorsInput);
347
403
 
404
+ // log the events
405
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.querySelectorsAll}: output:`);
406
+ logger.warn(`${outputText}`);
407
+
348
408
  return {
349
409
  content: [{ type: "text", text: outputText }],
350
410
  };
@@ -367,7 +427,15 @@ class MainHelper {
367
427
  inputSchema: QuerySelectorsFirstInputSchema,
368
428
  },
369
429
  async (querySelectorsInput: QuerySelectorsFirstInput) => {
430
+ // log the events
431
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.querySelectors}: querying selectors: ${JSON.stringify(querySelectorsInput)}`);
432
+
370
433
  const outputText: string = await MainHelper.querySelectors(mcpClient, querySelectorsInput);
434
+
435
+ // log the events
436
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.querySelectors}: output:`);
437
+ logger.warn(`${outputText}`);
438
+
371
439
  return {
372
440
  content: [{ type: "text", text: outputText }],
373
441
  };
@@ -389,6 +457,9 @@ class MainHelper {
389
457
  }),
390
458
  },
391
459
  async ({ keys }: { keys: string }) => {
460
+ // log the events
461
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.pressKeys}: pressing keys: ${keys}`);
462
+
392
463
  // Build the list of keys to send, splitting regular characters into individual key presses, but keeping special keys as-is
393
464
  const keysToSend: string[] = [];
394
465
  const keysSplit = keys.split(',').map((key) => key.trim());
@@ -419,6 +490,10 @@ class MainHelper {
419
490
 
420
491
  let outputText = await ResponseFormatter.formatPressKeys(mcpTarget, keysToSend);
421
492
 
493
+ // log the events
494
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.pressKeys}: output:`);
495
+ logger.warn(`${outputText}`);
496
+
422
497
  // return a response indicating which keys were pressed
423
498
  return {
424
499
  content: [{ type: "text", text: outputText }],
@@ -444,12 +519,17 @@ class MainHelper {
444
519
  },
445
520
  },
446
521
  async ({ selector }: { selector: string }) => {
447
- const uid = await MainHelper._resolveSelectorToUid(mcpClient, selector);
522
+ // log the events
523
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.click}: clicking selector: ${selector}`);
448
524
 
525
+ const uid = await MainHelper._resolveSelectorToUid(mcpClient, selector);
449
526
  const toolConfig = await McpTargetHelper.targetToolClick(mcpTarget, uid);
450
527
  const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
451
528
  let outputText = await ResponseFormatter.formatClick(mcpTarget, callToolResult);
452
529
 
530
+ // log the events
531
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.click}: output:`);
532
+
453
533
  return {
454
534
  content: [{ type: "text", text: outputText }],
455
535
  };
@@ -477,6 +557,9 @@ class MainHelper {
477
557
  },
478
558
  },
479
559
  async ({ elements }: { elements: { selector: string; value: string }[] }) => {
560
+ // log the events
561
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.fillForm}: filling form with elements: ${JSON.stringify(elements)}`);
562
+
480
563
  const resolved = [];
481
564
  for (const element of elements) {
482
565
  const uid = await MainHelper._resolveSelectorToUid(mcpClient, element.selector);
@@ -485,32 +568,36 @@ class MainHelper {
485
568
  value: element.value,
486
569
  });
487
570
  }
571
+ let callToolResult: CallToolResult;
488
572
  if (mcpTarget === 'chrome_devtools') {
489
573
  const toolConfig = await McpTargetHelper.targetToolFillForm(mcpTarget, resolved);
490
- const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
491
- // const callToolResult = await mcpClient.callTool('fill_form', { elements: resolved });
492
- return callToolResult
574
+ callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
493
575
  } else if (mcpTarget === 'playwright') {
494
576
  type Field = {
495
577
  name: string; // Human readable name for the field, e.g. "Email address"
496
578
  // type can be textbox, checkbox, radio, combobox, or slider
497
579
  type: 'textbox' | 'checkbox' | 'radio' | 'combobox' | 'slider';
498
580
  // the uid of the field's corresponding node in the accessibility tree
499
- ref: string;
581
+ target: string;
500
582
  // 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
583
  value: string
502
584
  };
503
585
  const fields: Field[] = resolved.map((element, index) => ({
504
586
  name: `Field ${index + 1}`,
505
587
  type: 'textbox', // for simplicity, we assume all fields are textboxes in this example
506
- ref: element.uid,
588
+ target: element.uid,
507
589
  value: element.value,
508
590
  }));
509
- const callToolResult = await mcpClient.callTool('browser_fill_form', { fields: fields });
510
- return callToolResult
591
+ callToolResult = await mcpClient.callTool('browser_fill_form', { fields: fields });
511
592
  } else {
512
593
  throw new Error(`Unsupported MCP target: ${mcpTarget}`);
513
594
  }
595
+
596
+ // log the events
597
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.fillForm}: output:`);
598
+ logger.warn(`${JSON.stringify(callToolResult)}`);
599
+
600
+ return callToolResult
514
601
  }
515
602
  );
516
603
 
@@ -530,6 +617,10 @@ class MainHelper {
530
617
  inputSchema: timezoneSchema,
531
618
  },
532
619
  async ({ timezone }) => {
620
+ // log the events
621
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.getCurrentDateTime}: getting current date and time with timezone: ${timezone ?? 'local system timezone'}`);
622
+
623
+
533
624
  const date = new Date();
534
625
  const options: Intl.DateTimeFormatOptions = {
535
626
  timeZone: timezone,
@@ -542,6 +633,10 @@ class MainHelper {
542
633
  timeZoneName: "short",
543
634
  };
544
635
  const formatted = new Intl.DateTimeFormat("en-US", options).format(date);
636
+
637
+ // log the events
638
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.getCurrentDateTime}: output: ${formatted}`);
639
+
545
640
  return {
546
641
  content: [{ type: "text", text: formatted }],
547
642
  };
@@ -564,6 +659,11 @@ class MainHelper {
564
659
  mcpTarget: FastBrowserMcpTarget,
565
660
  verbose?: boolean
566
661
  }): Promise<void> {
662
+ // Redirect process.stderr to a log file, so that the MCP communication is not polluted by logs.
663
+ const logFile = Path.resolve(import.meta.dirname, `../../outputs/fastbrowser_mcp_${new Date().toISOString()}.log`);
664
+ const logStream = Fs.createWriteStream(logFile, { flags: 'a' });
665
+ process.stderr.write = logStream.write.bind(logStream);
666
+
567
667
  ///////////////////////////////////////////////////////////////////////////////
568
668
  ///////////////////////////////////////////////////////////////////////////////
569
669
  // mcp client
@@ -616,11 +716,13 @@ class MainHelper {
616
716
  {
617
717
  selector: "link, button",
618
718
  withAncestors: true,
719
+ withChildren: false,
619
720
  limit: 0,
620
721
  },
621
722
  {
622
723
  selector: 'heading[level="1"]',
623
724
  withAncestors: false,
725
+ withChildren: false,
624
726
  limit: 0,
625
727
  },
626
728
  ],
@@ -646,19 +748,64 @@ class MainHelper {
646
748
  for (const toolName of toolsToProxys) {
647
749
  await mcpProxy.proxyToolCall(mcpClient, toolName)
648
750
  }
751
+
649
752
  ///////////////////////////////////////////////////////////////////////////////
650
753
  ///////////////////////////////////////////////////////////////////////////////
651
754
  // .querySelectorsAll tool implementation
652
755
  ///////////////////////////////////////////////////////////////////////////////
653
756
  ///////////////////////////////////////////////////////////////////////////////
654
- const mcpServer = await mcpProxy.getMcpServer();
655
757
 
758
+ const mcpServer = await mcpProxy.getMcpServer();
656
759
  await MainHelper.initExternalTools(mcpServer, mcpClient);
657
760
 
658
-
659
761
  // Connect the MCP proxy server to start accepting connections from MCP clients (e.g. LLM agents).
660
762
  await mcpProxy.connect();
661
763
  }
764
+
765
+ ///////////////////////////////////////////////////////////////////////////////
766
+ ///////////////////////////////////////////////////////////////////////////////
767
+ // .commandTargetTools: connect to the target MCP and print each tool's name, description, and input schema.
768
+ ///////////////////////////////////////////////////////////////////////////////
769
+ ///////////////////////////////////////////////////////////////////////////////
770
+
771
+ static async commandTargetTools({
772
+ mcpTarget,
773
+ }: {
774
+ mcpTarget: FastBrowserMcpTarget,
775
+ }): Promise<void> {
776
+ const { command: mcpCommand, args: mcpArgs } = McpTargetHelper.mcpArgs(mcpTarget);
777
+
778
+ const mcpClient = new McpMyClient({
779
+ name: 'fastbrowser_target_tools_client',
780
+ version: '1.0.0',
781
+ mcpTarget,
782
+ transport: {
783
+ type: 'stdio',
784
+ command: mcpCommand,
785
+ args: mcpArgs,
786
+ },
787
+ });
788
+
789
+ await mcpClient.connect();
790
+ try {
791
+ const tools = await mcpClient.listTools();
792
+ console.log(`# Tools available on MCP target '${mcpTarget}' (${tools.length})\n`);
793
+ for (const tool of tools) {
794
+ console.log(`## ${tool.name}`);
795
+ console.log('');
796
+ console.log(`### Description`)
797
+ console.log(`${tool.description ?? '(no description)'}`);
798
+ console.log('');
799
+ console.log(`### Input schema`);
800
+ console.log("```");
801
+ console.log(JSON.stringify(tool.inputSchema, null, 2));
802
+ console.log("```");
803
+ console.log('');
804
+ }
805
+ } finally {
806
+ await mcpClient.close();
807
+ }
808
+ }
662
809
  }
663
810
 
664
811
  ///////////////////////////////////////////////////////////////////////////////
@@ -668,12 +815,6 @@ class MainHelper {
668
815
  ///////////////////////////////////////////////////////////////////////////////
669
816
 
670
817
  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
818
  // 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
819
 
679
820
  const program = new Command();
@@ -693,6 +834,20 @@ async function main() {
693
834
  });
694
835
  });
695
836
 
837
+ program
838
+ .command('target_tools')
839
+ .description('List the tools exposed by the target MCP (name, description, input schema)')
840
+ .addOption(
841
+ new Option('-b, --mcp_target <mcpTarget>', 'the MCP target to introspect')
842
+ .choices(['chrome_devtools', 'playwright'])
843
+ .default('playwright')
844
+ )
845
+ .action(async (options: { mcp_target: FastBrowserMcpTarget }) => {
846
+ await MainHelper.commandTargetTools({
847
+ mcpTarget: options.mcp_target,
848
+ });
849
+ });
850
+
696
851
  // display help if no command is provided
697
852
  if (process.argv.length < 3) {
698
853
  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
- return { toolName: 'click', toolArgs: { uid } };
185
+ const toolConfig: TargetToolConfig = {
186
+ toolName: 'click',
187
+ toolArgs: {
188
+ uid
189
+ }
190
+ };
191
+ return toolConfig;
186
192
  } else if (mcpTarget === 'playwright') {
187
- return { toolName: 'browser_click', toolArgs: { ref: uid } };
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}`);
@@ -18,6 +18,9 @@ export const QuerySelectorInputSchema = z.object({
18
18
  withAncestors: z.boolean()
19
19
  .describe("Whether to include ancestor nodes in the result")
20
20
  .default(false),
21
+ withChildren: z.boolean()
22
+ .describe("Whether to include descendant nodes (subtree) of each matched node in the result")
23
+ .default(false),
21
24
  });
22
25
 
23
26
  export const QuerySelectorsInputSchema = z.object({
@@ -31,6 +34,9 @@ export const QuerySelectorFirstInputSchema = z.object({
31
34
  withAncestors: z.boolean()
32
35
  .describe("Whether to include ancestor nodes in the result")
33
36
  .default(false),
37
+ withChildren: z.boolean()
38
+ .describe("Whether to include descendant nodes (subtree) of each matched node in the result")
39
+ .default(false),
34
40
  });
35
41
 
36
42
  export const QuerySelectorsFirstInputSchema = z.object({