fastbrowser_cli 1.0.35 → 1.0.39

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 (102) hide show
  1. package/README.md +26 -5
  2. package/dist/contribs/_shared/fastbrowser_helper.d.ts +13 -0
  3. package/dist/contribs/_shared/fastbrowser_helper.d.ts.map +1 -0
  4. package/dist/contribs/_shared/fastbrowser_helper.js +39 -0
  5. package/dist/contribs/_shared/fastbrowser_helper.js.map +1 -0
  6. package/dist/contribs/linkedin_cli/src/cli.d.ts +3 -0
  7. package/dist/contribs/linkedin_cli/src/cli.d.ts.map +1 -0
  8. package/dist/contribs/linkedin_cli/src/cli.js +299 -0
  9. package/dist/contribs/linkedin_cli/src/cli.js.map +1 -0
  10. package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.d.ts +73 -0
  11. package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.d.ts.map +1 -0
  12. package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.js +866 -0
  13. package/dist/contribs/linkedin_cli/src/libs/linkedin_profile_helper.js.map +1 -0
  14. package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.d.ts +61 -0
  15. package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.d.ts.map +1 -0
  16. package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.js +885 -0
  17. package/dist/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.js.map +1 -0
  18. package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.d.ts +11 -0
  19. package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.d.ts.map +1 -0
  20. package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.js +145 -0
  21. package/dist/contribs/linkedin_cli/src/libs/linkedin_thread_helper.js.map +1 -0
  22. package/dist/contribs/twitter_cli/src/cli.d.ts +3 -0
  23. package/dist/contribs/twitter_cli/src/cli.d.ts.map +1 -0
  24. package/dist/contribs/twitter_cli/src/cli.js +273 -0
  25. package/dist/contribs/twitter_cli/src/cli.js.map +1 -0
  26. package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.d.ts +28 -0
  27. package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.d.ts.map +1 -0
  28. package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.js +274 -0
  29. package/dist/contribs/twitter_cli/src/libs/twitter_profile_helper.js.map +1 -0
  30. package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.d.ts +43 -0
  31. package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.d.ts.map +1 -0
  32. package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.js +519 -0
  33. package/dist/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.js.map +1 -0
  34. package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.d.ts +11 -0
  35. package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.d.ts.map +1 -0
  36. package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.js +213 -0
  37. package/dist/contribs/twitter_cli/src/libs/twitter_thread_helper.js.map +1 -0
  38. package/dist/fastbrowser_cli/fastbrowser_cli.js +43 -0
  39. package/dist/fastbrowser_cli/fastbrowser_cli.js.map +1 -1
  40. package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts +4 -0
  41. package/dist/fastbrowser_httpd/libs/tool-schemas.d.ts.map +1 -1
  42. package/dist/fastbrowser_httpd/libs/tool-schemas.js +4 -0
  43. package/dist/fastbrowser_httpd/libs/tool-schemas.js.map +1 -1
  44. package/dist/fastbrowser_mcp/fastbrowser_mcp.js +36 -2
  45. package/dist/fastbrowser_mcp/fastbrowser_mcp.js.map +1 -1
  46. package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts +2 -0
  47. package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts.map +1 -1
  48. package/dist/fastbrowser_mcp/libs/mcp_target_helper.js +12 -0
  49. package/dist/fastbrowser_mcp/libs/mcp_target_helper.js.map +1 -1
  50. package/dist/fastbrowser_mcp/libs/response_formatter.d.ts +1 -0
  51. package/dist/fastbrowser_mcp/libs/response_formatter.d.ts.map +1 -1
  52. package/dist/fastbrowser_mcp/libs/response_formatter.js +27 -0
  53. package/dist/fastbrowser_mcp/libs/response_formatter.js.map +1 -1
  54. package/dist/shared/fastbrowser_helper.d.ts +13 -0
  55. package/dist/shared/fastbrowser_helper.d.ts.map +1 -0
  56. package/dist/shared/fastbrowser_helper.js +39 -0
  57. package/dist/shared/fastbrowser_helper.js.map +1 -0
  58. package/examples/linkedin_cli_TOREMOVE/README.md +7 -0
  59. package/examples/linkedin_cli_TOREMOVE/linkedin_dm.sh +40 -0
  60. package/examples/linkedin_cli_TOREMOVE/linkedin_dm.ts +326 -0
  61. package/examples/linkedin_cli_TOREMOVE/linkedin_dm_messages.ts +279 -0
  62. package/examples/linkedin_cli_TOREMOVE/linkedin_full_cycle.sh +5 -0
  63. package/examples/{linkedin_cli/linked_post.sh → linkedin_cli_TOREMOVE/linkedin_post.sh} +3 -0
  64. package/examples/linkedin_cli_TOREMOVE/message_thread.a11y.txt +252 -0
  65. package/examples/whatsapp/whatapp.a11y.txt +1521 -0
  66. package/examples/whatsapp/whatsapp.sh +10 -0
  67. package/listitem +7 -0
  68. package/package.json +7 -3
  69. package/skills/fastbrowser/SKILL.md +116 -29
  70. package/src/contribs/_shared/fastbrowser_helper.ts +49 -0
  71. package/src/contribs/linkedin_cli/README.md +80 -0
  72. package/src/contribs/linkedin_cli/data/linkedin_posts_jeromeetienne.a11y.txt +2364 -0
  73. package/src/contribs/linkedin_cli/data/linkedin_posts_jontwigge.a11y.txt +2740 -0
  74. package/src/contribs/linkedin_cli/data/linkedin_posts_julien_guezennec.a11y.txt +2073 -0
  75. package/src/contribs/linkedin_cli/data/linkedin_profile_jeromeetienne.a11y.txt +1863 -0
  76. package/src/contribs/linkedin_cli/data/linkedin_profile_jontwigge.a11y.txt +1738 -0
  77. package/src/contribs/linkedin_cli/data/linkedin_profile_julien_guezennec.a11y.txt +2182 -0
  78. package/src/contribs/linkedin_cli/src/cli.ts +345 -0
  79. package/src/contribs/linkedin_cli/src/libs/linkedin_profile_helper.ts +964 -0
  80. package/src/contribs/linkedin_cli/src/libs/linkedin_recent_posts_helper.ts +982 -0
  81. package/src/contribs/linkedin_cli/src/libs/linkedin_thread_helper.ts +171 -0
  82. package/src/contribs/twitter_cli/README.md +79 -0
  83. package/src/contribs/twitter_cli/data/twitter_chat.a11y.txt +215 -0
  84. package/src/contribs/twitter_cli/data/twitter_home.a11y.txt +467 -0
  85. package/src/contribs/twitter_cli/data/twitter_profile.a11y.txt +418 -0
  86. package/src/contribs/twitter_cli/data/twitter_profile_jontwigge.a11y.txt +484 -0
  87. package/src/contribs/twitter_cli/data/twitter_profile_molokoloco.a11y.txt +483 -0
  88. package/src/contribs/twitter_cli/src/cli.ts +315 -0
  89. package/src/contribs/twitter_cli/src/libs/twitter_profile_helper.ts +328 -0
  90. package/src/contribs/twitter_cli/src/libs/twitter_recent_posts_helper.ts +607 -0
  91. package/src/contribs/twitter_cli/src/libs/twitter_thread_helper.ts +240 -0
  92. package/src/fastbrowser_cli/fastbrowser_cli.ts +51 -0
  93. package/src/fastbrowser_httpd/libs/tool-schemas.ts +6 -0
  94. package/src/fastbrowser_mcp/fastbrowser_mcp.ts +46 -3
  95. package/src/fastbrowser_mcp/libs/mcp_target_helper.ts +11 -0
  96. package/src/fastbrowser_mcp/libs/response_formatter.ts +29 -0
  97. package/src/shared/fastbrowser_helper.ts +49 -0
  98. package/tsconfig.json +1 -1
  99. package/examples/linkedin_cli/linked_dm.sh +0 -19
  100. package/examples/mcp_client_playwright.ts +0 -34
  101. /package/examples/{linkedin_cli → linkedin_cli_TOREMOVE}/linkedin.snapshot.txt +0 -0
  102. /package/examples/{twitter_cli → twitter_cli_TOREMOVE}/twitter_post.sh +0 -0
@@ -0,0 +1,240 @@
1
+ // npm imports
2
+ import { A11yQuery, A11yTree, AxNode } from 'a11y_parse';
3
+
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ //
7
+ ///////////////////////////////////////////////////////////////////////////////
8
+ ///////////////////////////////////////////////////////////////////////////////
9
+
10
+ const TIME_OF_DAY_REGEXP = /^\d{1,2}:\d{2}\s*(AM|PM)$/i;
11
+ const FULL_DATE_REGEXP = /^([A-Za-z]+)\s+(\d{1,2})(?:,\s*(\d{4}))?$/;
12
+ const WEEKDAYS: Record<string, number> = {
13
+ Sunday: 0, Monday: 1, Tuesday: 2, Wednesday: 3,
14
+ Thursday: 4, Friday: 5, Saturday: 6,
15
+ };
16
+ const MONTHS: Record<string, number> = {
17
+ Jan: 0, January: 0,
18
+ Feb: 1, February: 1,
19
+ Mar: 2, March: 2,
20
+ Apr: 3, April: 3,
21
+ May: 4,
22
+ Jun: 5, June: 5,
23
+ Jul: 6, July: 6,
24
+ Aug: 7, August: 7,
25
+ Sep: 8, September: 8,
26
+ Oct: 9, October: 9,
27
+ Nov: 10, November: 10,
28
+ Dec: 11, December: 11,
29
+ };
30
+
31
+ export class TwitterThreadHelper {
32
+ static async parseMessagesThread(
33
+ rawOutput: string,
34
+ otherHandle: string,
35
+ overrideYear?: number,
36
+ ): Promise<string> {
37
+ const treeText = TwitterThreadHelper.extractAxTreeText(rawOutput);
38
+ if (treeText.length === 0) {
39
+ return '';
40
+ }
41
+ const root = A11yTree.parse(treeText);
42
+ const items = A11yQuery.querySelectorAll(root, 'main listitem');
43
+ const year = overrideYear !== undefined ? overrideYear : new Date().getFullYear();
44
+
45
+ const lines: string[] = [];
46
+ let currentDate: Date | null = null;
47
+ const pending: string[] = [];
48
+
49
+ for (const item of items) {
50
+ const valueGenerics = A11yQuery.querySelectorAll(item, 'generic[value]');
51
+ if (valueGenerics.length === 0) {
52
+ continue;
53
+ }
54
+
55
+ const dateMarker = TwitterThreadHelper.detectDateMarker(item, valueGenerics, year);
56
+ if (dateMarker !== null) {
57
+ currentDate = dateMarker;
58
+ continue;
59
+ }
60
+
61
+ const timestampNodes = TwitterThreadHelper.findTimestampNodes(valueGenerics);
62
+ if (timestampNodes.length === 0) {
63
+ const text = TwitterThreadHelper.collectText(valueGenerics, new Set());
64
+ if (text.length > 0) {
65
+ pending.push(text);
66
+ }
67
+ continue;
68
+ }
69
+
70
+ const time = TwitterThreadHelper.parseTimeOfDay(timestampNodes[0].attributes['value']);
71
+ if (time === null) {
72
+ continue;
73
+ }
74
+ if (currentDate === null) {
75
+ continue;
76
+ }
77
+
78
+ const sender = A11yQuery.querySelector(item, 'img') !== undefined
79
+ ? 'You'
80
+ : otherHandle;
81
+
82
+ const timestampUids = new Set(timestampNodes.map((n) => n.uid));
83
+ const text = TwitterThreadHelper.collectText(valueGenerics, timestampUids);
84
+ const iso = TwitterThreadHelper.combineDateTime(currentDate, time);
85
+
86
+ for (const pendingText of pending) {
87
+ lines.push(`${iso}:${sender}:${pendingText}`);
88
+ }
89
+ pending.length = 0;
90
+
91
+ if (text.length === 0) {
92
+ continue;
93
+ }
94
+ lines.push(`${iso}:${sender}:${text}`);
95
+ }
96
+
97
+ return lines.join('\n');
98
+ }
99
+
100
+ private static collectText(valueGenerics: AxNode[], excludeUids: Set<string>): string {
101
+ const parts: string[] = [];
102
+ for (const node of valueGenerics) {
103
+ if (excludeUids.has(node.uid) === true) {
104
+ continue;
105
+ }
106
+ const value = node.attributes['value'];
107
+ if (value === undefined) {
108
+ continue;
109
+ }
110
+ const trimmed = value.trim();
111
+ if (trimmed.length === 0) {
112
+ continue;
113
+ }
114
+ parts.push(trimmed);
115
+ }
116
+ return parts.join(' ');
117
+ }
118
+
119
+ ///////////////////////////////////////////////////////////////////////////////
120
+ ///////////////////////////////////////////////////////////////////////////////
121
+ //
122
+ ///////////////////////////////////////////////////////////////////////////////
123
+ ///////////////////////////////////////////////////////////////////////////////
124
+
125
+ private static extractAxTreeText(rawOutput: string): string {
126
+ const lines: string[] = [];
127
+ for (const line of rawOutput.split('\n')) {
128
+ if (/^\s*uid=/.test(line) === true) {
129
+ lines.push(line);
130
+ }
131
+ }
132
+ return lines.join('\n');
133
+ }
134
+
135
+ private static findTimestampNodes(valueGenerics: AxNode[]): AxNode[] {
136
+ const result: AxNode[] = [];
137
+ for (const node of valueGenerics) {
138
+ const value = node.attributes['value'];
139
+ if (value === undefined) {
140
+ continue;
141
+ }
142
+ if (TIME_OF_DAY_REGEXP.test(value) === true) {
143
+ result.push(node);
144
+ }
145
+ }
146
+ return result;
147
+ }
148
+
149
+ private static detectDateMarker(
150
+ item: AxNode,
151
+ valueGenerics: AxNode[],
152
+ fallbackYear: number,
153
+ ): Date | null {
154
+ const hasTimestamp = TwitterThreadHelper.findTimestampNodes(valueGenerics).length > 0;
155
+ if (hasTimestamp === true) {
156
+ return null;
157
+ }
158
+ if (A11yQuery.querySelector(item, 'img') !== undefined) {
159
+ return null;
160
+ }
161
+ for (const node of valueGenerics) {
162
+ const raw = node.attributes['value'];
163
+ if (raw === undefined) {
164
+ continue;
165
+ }
166
+ const value = raw.trim();
167
+ const parsed = TwitterThreadHelper.parseDateMarker(value, fallbackYear);
168
+ if (parsed !== null) {
169
+ return parsed;
170
+ }
171
+ }
172
+ return null;
173
+ }
174
+
175
+ private static parseDateMarker(value: string, fallbackYear: number): Date | null {
176
+ if (value === 'Today') {
177
+ const now = new Date();
178
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
179
+ }
180
+ if (value === 'Yesterday') {
181
+ const now = new Date();
182
+ const d = new Date(now.getFullYear(), now.getMonth(), now.getDate());
183
+ d.setDate(d.getDate() - 1);
184
+ return d;
185
+ }
186
+ if (WEEKDAYS[value] !== undefined) {
187
+ const target = WEEKDAYS[value];
188
+ const now = new Date();
189
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
190
+ const diff = (today.getDay() - target + 7) % 7;
191
+ const days = diff === 0 ? 7 : diff;
192
+ today.setDate(today.getDate() - days);
193
+ return today;
194
+ }
195
+ const match = value.match(FULL_DATE_REGEXP);
196
+ if (match === null) {
197
+ return null;
198
+ }
199
+ const monthIndex = MONTHS[match[1]];
200
+ if (monthIndex === undefined) {
201
+ return null;
202
+ }
203
+ const day = parseInt(match[2], 10);
204
+ const explicitYear = match[3] !== undefined ? parseInt(match[3], 10) : null;
205
+ const year = explicitYear !== null ? explicitYear : fallbackYear;
206
+ const candidate = new Date(year, monthIndex, day);
207
+ if (explicitYear === null && candidate.getTime() > Date.now()) {
208
+ candidate.setFullYear(year - 1);
209
+ }
210
+ return candidate;
211
+ }
212
+
213
+ private static parseTimeOfDay(value: string | undefined): { hours: number; minutes: number } | null {
214
+ if (value === undefined) {
215
+ return null;
216
+ }
217
+ const match = value.trim().match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
218
+ if (match === null) {
219
+ return null;
220
+ }
221
+ let hours = parseInt(match[1], 10);
222
+ const minutes = parseInt(match[2], 10);
223
+ const meridiem = match[3].toUpperCase();
224
+ if (meridiem === 'AM' && hours === 12) {
225
+ hours = 0;
226
+ } else if (meridiem === 'PM' && hours !== 12) {
227
+ hours += 12;
228
+ }
229
+ return { hours, minutes };
230
+ }
231
+
232
+ private static combineDateTime(date: Date, time: { hours: number; minutes: number }): string {
233
+ const yyyy = date.getFullYear().toString().padStart(4, '0');
234
+ const mm = (date.getMonth() + 1).toString().padStart(2, '0');
235
+ const dd = date.getDate().toString().padStart(2, '0');
236
+ const hh = time.hours.toString().padStart(2, '0');
237
+ const mi = time.minutes.toString().padStart(2, '0');
238
+ return `${yyyy}-${mm}-${dd}T${hh}:${mi}:00`;
239
+ }
240
+ }
@@ -252,6 +252,48 @@ async function main(): Promise<void> {
252
252
  await MainHelper.runTool(cmd, 'list_pages', {});
253
253
  });
254
254
 
255
+ program
256
+ .command('check')
257
+ .description('Verify the connection with the browser; restart the server once if it looks desynched')
258
+ .action(async (_opts, cmd: Command) => {
259
+ const serverUrl = MainHelper.getServerUrlFromCmd(cmd);
260
+ const mcpTarget = MainHelper.getMcpTargetFromCmd(cmd);
261
+
262
+ const hasPages = (response: { content: Array<{ text: string }> }): boolean => {
263
+ const joined = response.content.map((part) => part.text).join('\n');
264
+ const lines = joined.split('\n');
265
+ for (const line of lines) {
266
+ if (/^\s*\d+:/.test(line) === true) {
267
+ return true;
268
+ }
269
+ }
270
+ return false;
271
+ };
272
+
273
+ if (MainHelper.getAutostartFromCmd(cmd) === true) {
274
+ await ServerManager.ensureRunning(serverUrl, mcpTarget);
275
+ }
276
+
277
+ const firstResponse = await HttpClient.postTool(serverUrl, 'list_pages', {});
278
+ if (hasPages(firstResponse) === true) {
279
+ console.log('It is properly connected to the browser');
280
+ return;
281
+ }
282
+
283
+ console.log('Connection with the browser is desynched, relaunching the server');
284
+ await ServerManager.stop(serverUrl);
285
+ await ServerManager.start(serverUrl, mcpTarget);
286
+
287
+ const secondResponse = await HttpClient.postTool(serverUrl, 'list_pages', {});
288
+ if (hasPages(secondResponse) === true) {
289
+ console.log('It is properly connected to the browser');
290
+ return;
291
+ }
292
+
293
+ console.error('Connection with the browser is still broken after server restart');
294
+ process.exit(1);
295
+ });
296
+
255
297
  program
256
298
  .command('new_page')
257
299
  .description('Open a new browser page')
@@ -343,6 +385,15 @@ async function main(): Promise<void> {
343
385
  await MainHelper.runTool(cmd, 'press_keys', { keys: opts.keys });
344
386
  });
345
387
 
388
+ program
389
+ .command('evaluate_script [file]')
390
+ .description('Evaluate a JavaScript function in the page context. Provide the function text inline via --script, as a [file] path, or piped on stdin. The function should return JSON-able data.')
391
+ .option('--script <script>', 'Inline JS function text (overrides [file] and stdin)')
392
+ .action(async (file: string | undefined, opts: { script?: string }, cmd: Command) => {
393
+ const functionTxt = await MainHelper.readBatchSource(file, opts.script);
394
+ await MainHelper.runTool(cmd, 'evaluate_script', { functionTxt });
395
+ });
396
+
346
397
  program
347
398
  .command('install [skill-folder]')
348
399
  .description('Install all bundled skills into <skill-folder>/skills/ (default: .)')
@@ -61,6 +61,11 @@ export type PressKeysRequest = z.infer<typeof PressKeysRequestSchema>;
61
61
  export const TakeSnapshotRequestSchema = z.object({}).strict();
62
62
  export type TakeSnapshotRequest = z.infer<typeof TakeSnapshotRequestSchema>;
63
63
 
64
+ export const EvaluateScriptRequestSchema = z.object({
65
+ functionTxt: z.string().describe('JS function text to evaluate in the page context. Should return JSON-able data.'),
66
+ });
67
+ export type EvaluateScriptRequest = z.infer<typeof EvaluateScriptRequestSchema>;
68
+
64
69
  ///////////////////////////////////////////////////////////////////////////////
65
70
  ///////////////////////////////////////////////////////////////////////////////
66
71
  // Uniform response — narrowed mirror of MCP's CallToolResult
@@ -104,4 +109,5 @@ export const TOOL_SCHEMAS: ToolSchemaEntry[] = [
104
109
  { routeName: 'query_selectors', mcpToolName: 'querySelectors', requestSchema: QuerySelectorRequestSchema },
105
110
  { routeName: 'press_keys', mcpToolName: 'pressKeys', requestSchema: PressKeysRequestSchema },
106
111
  { routeName: 'take_snapshot', mcpToolName: 'take_snapshot', requestSchema: TakeSnapshotRequestSchema },
112
+ { routeName: 'evaluate_script', mcpToolName: 'evaluate_script', requestSchema: EvaluateScriptRequestSchema },
107
113
  ];
@@ -504,7 +504,20 @@ class MainHelper {
504
504
  const keysToSend: string[] = [];
505
505
  const keysSplit = keys.split(',').map((key) => key.trim());
506
506
  for (const key of keysSplit) {
507
- const specialKeys = ['Tab', 'Enter', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Escape', 'Backspace', 'Delete'];
507
+ // from chatgpt
508
+ const specialKeys = [
509
+ // Control / navigation
510
+ 'Enter', 'Tab', 'Escape', 'Backspace', 'Delete', 'Insert',
511
+
512
+ // Arrows
513
+ 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
514
+
515
+ // Navigation keys
516
+ 'Home', 'End', 'PageUp', 'PageDown',
517
+
518
+ // Function keys
519
+ 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
520
+ ];
508
521
  if (specialKeys.includes(key)) {
509
522
  keysToSend.push(key);
510
523
  } else {
@@ -514,7 +527,7 @@ class MainHelper {
514
527
  }
515
528
  }
516
529
  }
517
- console.error("Keys to send:", keysToSend);
530
+ // console.error("Keys to send:", keysToSend);
518
531
  // chrome-devtools-mcp's 'press_key' tool accepts a single 'key' per call — loop through the sequence
519
532
  for (const key of keysToSend) {
520
533
  const toolConfig = await McpTargetHelper.targetToolPressKey(mcpTarget, key);
@@ -569,7 +582,7 @@ class MainHelper {
569
582
 
570
583
  // log the events
571
584
  logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.click}: output:`);
572
-
585
+ logger.warn(`${outputText}`);
573
586
  return {
574
587
  content: [{ type: "text", text: outputText }],
575
588
  };
@@ -641,6 +654,36 @@ class MainHelper {
641
654
  }
642
655
  );
643
656
 
657
+ ///////////////////////////////////////////////////////////////////////////////
658
+ ///////////////////////////////////////////////////////////////////////////////
659
+ //
660
+ ///////////////////////////////////////////////////////////////////////////////
661
+ ///////////////////////////////////////////////////////////////////////////////
662
+
663
+ mcpServer.registerTool(
664
+ McpTargetHelper.EXTERNAL_TOOL_NAME.evaluateScript,
665
+ {
666
+ description: "Evaluate a JavaScript function in the context of the current page. The function should return JSON-able data.",
667
+ inputSchema: z.object({
668
+ functionTxt: z.string().describe("The JavaScript function to evaluate in the page context. Should return JSON-able data."),
669
+ }),
670
+ },
671
+ async ({ functionTxt }: { functionTxt: string }) => {
672
+ // log the events
673
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.evaluateScript}: evaluating function: ${functionTxt}`);
674
+
675
+ const toolConfig = await McpTargetHelper.targetToolEvaluateScript(mcpTarget, functionTxt);
676
+ const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
677
+ let outputText = await ResponseFormatter.formatEvaluateScript(mcpTarget, callToolResult);
678
+
679
+ // log the events
680
+ logger.warn(`${mcpTarget}:${McpTargetHelper.EXTERNAL_TOOL_NAME.evaluateScript}: output:`);
681
+ logger.warn(`${outputText}`);
682
+ return {
683
+ content: [{ type: "text", text: outputText }],
684
+ };
685
+ }
686
+ )
644
687
  ///////////////////////////////////////////////////////////////////////////////
645
688
  ///////////////////////////////////////////////////////////////////////////////
646
689
  // .get_current_datetime tool implementation
@@ -18,6 +18,7 @@ export class McpTargetHelper {
18
18
  pressKeys: "pressKeys",
19
19
  click: "click",
20
20
  fillForm: "fill_form",
21
+ evaluateScript: "evaluate_script",
21
22
  getCurrentDateTime: "get_current_datetime",
22
23
  }
23
24
 
@@ -212,4 +213,14 @@ export class McpTargetHelper {
212
213
  throw new Error(`Unsupported MCP target: ${mcpTarget}`);
213
214
  }
214
215
  }
216
+
217
+ static async targetToolEvaluateScript(mcpTarget: FastBrowserMcpTarget, functionText: string): Promise<TargetToolConfig> {
218
+ if (mcpTarget === 'chrome_devtools') {
219
+ return { toolName: 'evaluate_script', toolArgs: { function: functionText } };
220
+ } else if (mcpTarget === 'playwright') {
221
+ return { toolName: 'browser_evaluate', toolArgs: { function: functionText } };
222
+ } else {
223
+ throw new Error(`Unsupported MCP target: ${mcpTarget}`);
224
+ }
225
+ }
215
226
  }
@@ -211,4 +211,33 @@ export class ResponseFormatter {
211
211
  throw new Error(`Unsupported MCP target: ${mcpTarget}`);
212
212
  }
213
213
  }
214
+
215
+ static async formatEvaluateScript(mcpTarget: FastBrowserMcpTarget, callToolResult: CallToolResult): Promise<string> {
216
+ const resultContent = callToolResult.content[0]
217
+ if (resultContent.type !== "text") throw new Error("Unexpected content type");
218
+ const resultText: string = resultContent.text
219
+
220
+ // ### Result
221
+ // undefined
222
+ // ### Ran Playwright code
223
+ // ```js
224
+ // await page.evaluate('() => {\n const workspace = document.querySelector(\'main#workspace\');\n if (workspace === null) {\n throw new Error(\'Workspace element not found\');\n }\n const tryCount = 6;\n const delayMs = 500;\n (async () => {\n for (let i = 0; i < tryCount; i++) {\n workspace.scrollBy({\n top: 600000,\n behavior: \'smooth\'\n });\n await new Promise(resolve => setTimeout(resolve, delayMs));\n }\n })();\n}');
225
+ // ```
226
+ // ### Events
227
+ // - New console entries: .playwright-mcp/console-2026-05-04T05-43-55-775Z.log#L3959-L4058
228
+
229
+ // Target format example:
230
+ // Successfully clicked on the element
231
+
232
+ if (mcpTarget === 'chrome_devtools') {
233
+ // EXAMPLE:
234
+ // Successfully clicked on the element
235
+
236
+ return resultText
237
+ } else if (mcpTarget === 'playwright') {
238
+ return resultText
239
+ } else {
240
+ throw new Error(`Unsupported MCP target: ${mcpTarget}`);
241
+ }
242
+ }
214
243
  }
@@ -0,0 +1,49 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ const __dirname = new URL('.', import.meta.url).pathname;
4
+
5
+ export class FastBrowserHelper {
6
+ static async run(command: string): Promise<string> {
7
+ // const fullCommand = `npx fastbrowser_cli ${command}`;
8
+ const fullCommand = `npx tsx ${__dirname}../../src/fastbrowser_cli/fastbrowser_cli.ts ${command}`;
9
+ console.error(`Running command: ${fullCommand}`);
10
+ return execSync(fullCommand, { encoding: 'utf8' });
11
+ }
12
+
13
+ static async navigatePage(url: string): Promise<void> {
14
+ await FastBrowserHelper.run(`navigate_page --url '${url}'`);
15
+ }
16
+
17
+ static async fillForm(selector: string, value: string): Promise<void> {
18
+ await FastBrowserHelper.run(`fill_form --selector '${selector}' --value '${value}'`);
19
+ }
20
+
21
+ static async pressKeys(keys: string): Promise<void> {
22
+ await FastBrowserHelper.run(`press_keys --keys '${keys}'`);
23
+ }
24
+
25
+ static async click(selector: string): Promise<void> {
26
+ await FastBrowserHelper.run(`click -s '${selector}'`);
27
+ }
28
+
29
+ static async querySelectorsAll(selector: string, limit: number): Promise<string> {
30
+ return await FastBrowserHelper.run(`query_selectors --all --selector '${selector}' --limit ${limit}`);
31
+ }
32
+
33
+ static async querySelectorsAllWithChildren(selector: string, limit: number): Promise<string> {
34
+ return await FastBrowserHelper.run(`query_selectors --all --selector '${selector}' --limit ${limit} --with-ancestors --with-children`);
35
+ }
36
+
37
+ static async takeSnapshot(): Promise<string> {
38
+ return await FastBrowserHelper.run('take_snapshot');
39
+ }
40
+
41
+ static async querySelectors(selector: string, withAncestors = true): Promise<string> {
42
+ const flag = withAncestors === false ? ' --no-with-ancestors' : '';
43
+ return await FastBrowserHelper.run(`query_selectors --selector '${selector}'${flag}`);
44
+ }
45
+
46
+ static async evaluateScript(functionText: string): Promise<string> {
47
+ return await FastBrowserHelper.run(`evaluate_script --script "${functionText}"`);
48
+ }
49
+ }
package/tsconfig.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "include": [
20
20
  "src/**/*",
21
21
  "tests/**/*",
22
- "examples/**/*",
22
+ "examples/**/*"
23
23
  ],
24
24
  "exclude": [
25
25
  "node_modules",
@@ -1,19 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Restart the server to clear any previous state
4
- NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- server restart
5
-
6
- # Goto linkedin messaging page using the CLI commands below:
7
- NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- navigate_page --url https://www.linkedin.com/messaging/
8
-
9
- # list all the threads conversations in the left sidebar
10
- NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- query_selectors -s 'list[name="Conversation List"] > listitem heading' -a
11
-
12
- # Select the conversation with Eric Defiez
13
- NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- click -s 'list[name="Conversation List"] > listitem heading[name^="Eric Defiez"]'
14
-
15
- # Fill the message content
16
- NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- fill_form -s 'textbox[name^="Write"]' -v "Hello"
17
-
18
- # Click the "Send" button to send the message
19
- NODE_OPTIONS='' NPM_CONFIG_LOGLEVEL=silent npm run dev:cli -- click -s 'button[name^="Send"]'
@@ -1,34 +0,0 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
-
4
- async function main() {
5
- // Note: the PLAYWRIGHT_MCP_EXTENSION_TOKEN environment variable is required to authenticate with the Playwright MCP extension.
6
- const client = new Client({
7
- name: 'foobar',
8
- version: '0.1.0',
9
- });
10
-
11
- const transport = new StdioClientTransport({
12
- command: 'npx',
13
- args: ["@playwright/mcp", "--extension"],
14
- env: {
15
- PLAYWRIGHT_MCP_EXTENSION_TOKEN: 'd-dwfALmOesZLoS7i-ia8Wf7TWrHtlRMHuVCqAUuiKU'
16
- },
17
- });
18
- console.log("Connecting to MCP server...");
19
- await client.connect(transport);
20
- console.log("Connected!");
21
-
22
-
23
- const listToolsResult = await client.listTools()
24
- console.log("Available tools:", listToolsResult.tools.map(tool => tool.name));
25
-
26
- // const toolResult = await client.callTool({
27
- // name: '',
28
- // arguments: args
29
- // })
30
-
31
- await client.close();
32
- }
33
-
34
- void main();