fastbrowser_cli 1.0.14 → 1.0.16

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 (154) hide show
  1. package/.playwright-mcp/.gitignore +3 -0
  2. package/README.md +29 -3
  3. package/dist/fastbrowser_cli/fastbrowser_cli.js +52 -35
  4. package/dist/fastbrowser_cli/fastbrowser_cli.js.map +1 -1
  5. package/dist/fastbrowser_cli/libs/http-client.js +3 -7
  6. package/dist/fastbrowser_cli/libs/http-client.js.map +1 -1
  7. package/dist/fastbrowser_cli/libs/server-manager.d.ts.map +1 -1
  8. package/dist/fastbrowser_cli/libs/server-manager.js +30 -28
  9. package/dist/fastbrowser_cli/libs/server-manager.js.map +1 -1
  10. package/dist/fastbrowser_httpd/fastbrowser_httpd.js +22 -18
  11. package/dist/fastbrowser_httpd/fastbrowser_httpd.js.map +1 -1
  12. package/dist/fastbrowser_httpd/libs/routes.d.ts +2 -2
  13. package/dist/fastbrowser_httpd/libs/routes.d.ts.map +1 -1
  14. package/dist/fastbrowser_httpd/libs/routes.js +4 -8
  15. package/dist/fastbrowser_httpd/libs/routes.js.map +1 -1
  16. package/dist/fastbrowser_httpd/libs/tool-schemas.js +39 -42
  17. package/dist/fastbrowser_httpd/libs/tool-schemas.js.map +1 -1
  18. package/dist/fastbrowser_mcp/fastbrowser_mcp.d.ts.map +1 -1
  19. package/dist/fastbrowser_mcp/fastbrowser_mcp.js +270 -187
  20. package/dist/fastbrowser_mcp/fastbrowser_mcp.js.map +1 -1
  21. package/dist/fastbrowser_mcp/fastbrowser_types.d.ts +5 -0
  22. package/dist/fastbrowser_mcp/fastbrowser_types.d.ts.map +1 -0
  23. package/dist/fastbrowser_mcp/fastbrowser_types.js +2 -0
  24. package/dist/fastbrowser_mcp/fastbrowser_types.js.map +1 -0
  25. package/dist/{fastweb_mcp/libs/mcp_client.d.ts → fastbrowser_mcp/libs/mcp_client_TOREMOVE.d.ts} +6 -2
  26. package/dist/fastbrowser_mcp/libs/mcp_client_TOREMOVE.d.ts.map +1 -0
  27. package/dist/{src/libs/mcp_client.js → fastbrowser_mcp/libs/mcp_client_TOREMOVE.js} +15 -12
  28. package/dist/fastbrowser_mcp/libs/mcp_client_TOREMOVE.js.map +1 -0
  29. package/dist/{src/libs/mcp_client.d.ts → fastbrowser_mcp/libs/mcp_my_client.d.ts} +6 -2
  30. package/dist/fastbrowser_mcp/libs/mcp_my_client.d.ts.map +1 -0
  31. package/dist/{fastweb_mcp/libs/mcp_client.js → fastbrowser_mcp/libs/mcp_my_client.js} +15 -12
  32. package/dist/fastbrowser_mcp/libs/mcp_my_client.js.map +1 -0
  33. package/dist/fastbrowser_mcp/libs/mcp_proxy.d.ts +2 -2
  34. package/dist/fastbrowser_mcp/libs/mcp_proxy.d.ts.map +1 -1
  35. package/dist/fastbrowser_mcp/libs/mcp_proxy.js +9 -16
  36. package/dist/fastbrowser_mcp/libs/mcp_proxy.js.map +1 -1
  37. package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts +35 -0
  38. package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts.map +1 -0
  39. package/dist/fastbrowser_mcp/libs/mcp_target_helper.js +161 -0
  40. package/dist/fastbrowser_mcp/libs/mcp_target_helper.js.map +1 -0
  41. package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.d.ts +28 -0
  42. package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.d.ts.map +1 -0
  43. package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.js +210 -0
  44. package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.js.map +1 -0
  45. package/dist/fastbrowser_mcp/libs/response_formatter.d.ts +10 -0
  46. package/dist/fastbrowser_mcp/libs/response_formatter.d.ts.map +1 -0
  47. package/dist/fastbrowser_mcp/libs/response_formatter.js +155 -0
  48. package/dist/fastbrowser_mcp/libs/response_formatter.js.map +1 -0
  49. package/dist/fastbrowser_mcp/libs/schemas.js +14 -17
  50. package/dist/fastbrowser_mcp/libs/schemas.js.map +1 -1
  51. package/examples/mcp_client_playwright.ts +34 -0
  52. package/examples/welcometothejungle/wttj-job.ts +180 -0
  53. package/examples/welcometothejungle/wttj-search.ts +105 -0
  54. package/outputs/.gitignore +3 -0
  55. package/package.json +12 -8
  56. package/src/fastbrowser_cli/fastbrowser_cli.ts +34 -11
  57. package/src/fastbrowser_cli/libs/server-manager.ts +12 -3
  58. package/src/fastbrowser_httpd/fastbrowser_httpd.ts +16 -5
  59. package/src/fastbrowser_httpd/libs/routes.ts +2 -2
  60. package/src/fastbrowser_mcp/fastbrowser_mcp.ts +324 -150
  61. package/src/fastbrowser_mcp/fastbrowser_types.ts +4 -0
  62. package/src/fastbrowser_mcp/libs/{mcp_client.ts → mcp_client_TOREMOVE.ts} +13 -1
  63. package/src/fastbrowser_mcp/libs/mcp_my_client.ts +128 -0
  64. package/src/fastbrowser_mcp/libs/mcp_proxy.ts +2 -2
  65. package/src/fastbrowser_mcp/libs/mcp_target_helper.ts +164 -0
  66. package/src/fastbrowser_mcp/libs/playwright_a11y_helper.ts +249 -0
  67. package/src/fastbrowser_mcp/libs/response_formatter.ts +162 -0
  68. package/src/fastbrowser_mcp/libs/schemas.ts +2 -2
  69. package/tsconfig.build.json +13 -0
  70. package/tsconfig.json +10 -22
  71. package/dist/contrib/fastweb-cli/fastweb-cli.d.ts +0 -3
  72. package/dist/contrib/fastweb-cli/fastweb-cli.d.ts.map +0 -1
  73. package/dist/contrib/fastweb-cli/fastweb-cli.js +0 -151
  74. package/dist/contrib/fastweb-cli/fastweb-cli.js.map +0 -1
  75. package/dist/contrib/fastweb-cli/http-client.d.ts +0 -7
  76. package/dist/contrib/fastweb-cli/http-client.d.ts.map +0 -1
  77. package/dist/contrib/fastweb-cli/http-client.js +0 -51
  78. package/dist/contrib/fastweb-cli/http-client.js.map +0 -1
  79. package/dist/contrib/fastweb-http-server/fastweb-http-server.d.ts +0 -3
  80. package/dist/contrib/fastweb-http-server/fastweb-http-server.d.ts.map +0 -1
  81. package/dist/contrib/fastweb-http-server/fastweb-http-server.js +0 -82
  82. package/dist/contrib/fastweb-http-server/fastweb-http-server.js.map +0 -1
  83. package/dist/contrib/fastweb-http-server/routes.d.ts +0 -6
  84. package/dist/contrib/fastweb-http-server/routes.d.ts.map +0 -1
  85. package/dist/contrib/fastweb-http-server/routes.js +0 -41
  86. package/dist/contrib/fastweb-http-server/routes.js.map +0 -1
  87. package/dist/contrib/fastweb-http-server/tool-schemas.d.ts +0 -63
  88. package/dist/contrib/fastweb-http-server/tool-schemas.d.ts.map +0 -1
  89. package/dist/contrib/fastweb-http-server/tool-schemas.js +0 -61
  90. package/dist/contrib/fastweb-http-server/tool-schemas.js.map +0 -1
  91. package/dist/fastbrowser_mcp/libs/mcp_client.d.ts +0 -120
  92. package/dist/fastbrowser_mcp/libs/mcp_client.d.ts.map +0 -1
  93. package/dist/fastbrowser_mcp/libs/mcp_client.js +0 -83
  94. package/dist/fastbrowser_mcp/libs/mcp_client.js.map +0 -1
  95. package/dist/fastweb_cli/fastweb_cli.d.ts +0 -3
  96. package/dist/fastweb_cli/fastweb_cli.d.ts.map +0 -1
  97. package/dist/fastweb_cli/fastweb_cli.js +0 -254
  98. package/dist/fastweb_cli/fastweb_cli.js.map +0 -1
  99. package/dist/fastweb_cli/http-client.d.ts +0 -7
  100. package/dist/fastweb_cli/http-client.d.ts.map +0 -1
  101. package/dist/fastweb_cli/http-client.js +0 -51
  102. package/dist/fastweb_cli/http-client.js.map +0 -1
  103. package/dist/fastweb_cli/libs/http-client.d.ts +0 -7
  104. package/dist/fastweb_cli/libs/http-client.d.ts.map +0 -1
  105. package/dist/fastweb_cli/libs/http-client.js +0 -51
  106. package/dist/fastweb_cli/libs/http-client.js.map +0 -1
  107. package/dist/fastweb_cli/libs/server-manager.d.ts +0 -12
  108. package/dist/fastweb_cli/libs/server-manager.d.ts.map +0 -1
  109. package/dist/fastweb_cli/libs/server-manager.js +0 -194
  110. package/dist/fastweb_cli/libs/server-manager.js.map +0 -1
  111. package/dist/fastweb_http_server/fastweb_http_server.d.ts +0 -3
  112. package/dist/fastweb_http_server/fastweb_http_server.d.ts.map +0 -1
  113. package/dist/fastweb_http_server/fastweb_http_server.js +0 -82
  114. package/dist/fastweb_http_server/fastweb_http_server.js.map +0 -1
  115. package/dist/fastweb_http_server/libs/routes.d.ts +0 -6
  116. package/dist/fastweb_http_server/libs/routes.d.ts.map +0 -1
  117. package/dist/fastweb_http_server/libs/routes.js +0 -41
  118. package/dist/fastweb_http_server/libs/routes.js.map +0 -1
  119. package/dist/fastweb_http_server/libs/tool-schemas.d.ts +0 -72
  120. package/dist/fastweb_http_server/libs/tool-schemas.d.ts.map +0 -1
  121. package/dist/fastweb_http_server/libs/tool-schemas.js +0 -65
  122. package/dist/fastweb_http_server/libs/tool-schemas.js.map +0 -1
  123. package/dist/fastweb_http_server/routes.d.ts +0 -6
  124. package/dist/fastweb_http_server/routes.d.ts.map +0 -1
  125. package/dist/fastweb_http_server/routes.js +0 -41
  126. package/dist/fastweb_http_server/routes.js.map +0 -1
  127. package/dist/fastweb_http_server/tool-schemas.d.ts +0 -63
  128. package/dist/fastweb_http_server/tool-schemas.d.ts.map +0 -1
  129. package/dist/fastweb_http_server/tool-schemas.js +0 -61
  130. package/dist/fastweb_http_server/tool-schemas.js.map +0 -1
  131. package/dist/fastweb_mcp/fastweb_mcp.d.ts +0 -4
  132. package/dist/fastweb_mcp/fastweb_mcp.d.ts.map +0 -1
  133. package/dist/fastweb_mcp/fastweb_mcp.js +0 -417
  134. package/dist/fastweb_mcp/fastweb_mcp.js.map +0 -1
  135. package/dist/fastweb_mcp/libs/mcp_client.d.ts.map +0 -1
  136. package/dist/fastweb_mcp/libs/mcp_client.js.map +0 -1
  137. package/dist/fastweb_mcp/libs/mcp_proxy.d.ts +0 -10
  138. package/dist/fastweb_mcp/libs/mcp_proxy.d.ts.map +0 -1
  139. package/dist/fastweb_mcp/libs/mcp_proxy.js +0 -45
  140. package/dist/fastweb_mcp/libs/mcp_proxy.js.map +0 -1
  141. package/dist/fastweb_mcp/libs/schemas.d.ts +0 -28
  142. package/dist/fastweb_mcp/libs/schemas.d.ts.map +0 -1
  143. package/dist/fastweb_mcp/libs/schemas.js +0 -38
  144. package/dist/fastweb_mcp/libs/schemas.js.map +0 -1
  145. package/dist/src/fastweb_mcp.d.ts +0 -17
  146. package/dist/src/fastweb_mcp.d.ts.map +0 -1
  147. package/dist/src/fastweb_mcp.js +0 -342
  148. package/dist/src/fastweb_mcp.js.map +0 -1
  149. package/dist/src/libs/mcp_client.d.ts.map +0 -1
  150. package/dist/src/libs/mcp_client.js.map +0 -1
  151. package/dist/src/libs/mcp_proxy.d.ts +0 -10
  152. package/dist/src/libs/mcp_proxy.d.ts.map +0 -1
  153. package/dist/src/libs/mcp_proxy.js +0 -45
  154. package/dist/src/libs/mcp_proxy.js.map +0 -1
@@ -1,13 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // node imports
4
+ import * as Assert from 'node:assert/strict';
5
+ import Fs from 'node:fs';
6
+ import Path from 'node:path';
7
+
3
8
  // npm imports
4
- import { Command } from 'commander';
9
+ import { Command, Option } from 'commander';
5
10
  import { z } from "zod";
6
- import * as A11yParse from "../../../a11y_parse/dist/src/index.js";
11
+ import * as A11yParse from "a11y_parse";
7
12
 
8
13
  // local imports
9
- import { McpClient } from "./libs/mcp_client.js";
14
+ import { McpMyClient } from "./libs/mcp_my_client.js";
10
15
  import { McpProxy } from "./libs/mcp_proxy.js";
16
+ import { ResponseFormatter } from "./libs/response_formatter.js";
17
+ import { FastBrowserMcpTarget } from './fastbrowser_types.js';
11
18
  import {
12
19
  QuerySelectorInputSchema,
13
20
  QuerySelectorsInputSchema,
@@ -18,6 +25,9 @@ import {
18
25
  type QuerySelectorFirstInput,
19
26
  type QuerySelectorsFirstInput,
20
27
  } from "./libs/schemas.js";
28
+ import { McpTargetHelper } from './libs/mcp_target_helper.js';
29
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
30
+ import { CallToolResult } from '@modelcontextprotocol/sdk/types';
21
31
 
22
32
  export {
23
33
  QuerySelectorInputSchema,
@@ -37,38 +47,50 @@ export {
37
47
  ///////////////////////////////////////////////////////////////////////////////
38
48
 
39
49
  class MainHelper {
50
+ private static async _getA11yText(mcpClient: McpMyClient): Promise<string> {
51
+ const mcpTarget = await mcpClient.getMcpTarget();
52
+ const toolConfig = await McpTargetHelper.targetToolTakeSnapshot(mcpTarget);
53
+ const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
54
+ const snapshotText = await ResponseFormatter.formatTakeSnapshot(mcpTarget, callToolResult);
55
+ // sanity check
56
+ Assert.ok(snapshotText !== undefined, "Snapshot text is empty");
57
+
58
+ // return the snapshot text
59
+ return snapshotText;
60
+ }
61
+
62
+ /**
63
+ * TODO to remove this function - just call the .parse
64
+ * @param mcpClient
65
+ * @returns
66
+ */
67
+ private static async _getSnapshotTree(mcpClient: McpMyClient): Promise<{ snapshotText: string, a11yTree: A11yParse.AxNode }> {
68
+ const a11yText: string = await this._getA11yText(mcpClient);
69
+
70
+ // parse the accessibility tree
71
+ const a11yTree = A11yParse.A11yTree.parse(a11yText);
72
+
73
+ // return the parsed accessibility tree
74
+ return { snapshotText: a11yText, a11yTree }
75
+ }
76
+
40
77
  /**
41
78
  * Takes a snapshot of the current page to get the latest accessibility tree, and parses it into an AxNode tree structure.
42
79
  * @param mcpClient The MCP client to use for taking a snapshot.
43
80
  * @returns The parsed accessibility tree as an AxNode.
44
81
  */
45
- private static async _resolveSelectorToUid(mcpClient: McpClient, selector: string): Promise<string> {
82
+ private static async _resolveSelectorToUid(mcpClient: McpMyClient, selector: string): Promise<string> {
83
+ // Fast path: if the selector is already a uid selector (e.g. "#1_3"), extract and return the uid directly without
84
+ // taking a snapshot or querying the tree.
46
85
  const fastPathMatch = selector.match(/^#([\w-]+)$/);
47
86
  if (fastPathMatch !== null) return fastPathMatch[1];
48
- const a11yTree = await this._getSnapshotTree(mcpClient);
87
+
88
+ // Slow path: take a snapshot, parse the tree, and query it with the provided selector to find the uid of the first matching node.
89
+ const { a11yTree } = await this._getSnapshotTree(mcpClient);
49
90
  const selectedNodes = A11yParse.A11yQuery.querySelectorAll(a11yTree, selector);
50
91
  if (selectedNodes.length === 0) throw new Error(`No node matched selector '${selector}'`);
51
- return selectedNodes[0].uid;
52
- }
53
-
54
- private static async _getSnapshotTree(mcpClient: McpClient): Promise<A11yParse.AxNode> {
55
- // take a snapshot to get the latest accessibility tree
56
- const response = await mcpClient.callTool('take_snapshot', {});
57
- const responseText = response.content[0]
58
- if (responseText.type !== "text") throw new Error("Unexpected content type");
59
92
 
60
- // get the snapshot text and remove the first line (snapshot metadata)
61
- let snapshotText = responseText.text;
62
- snapshotText = snapshotText.split('\n').slice(1).join('\n');
63
-
64
- // log to console for debugging
65
- // console.log("Snapshot text content:");
66
- // console.log(snapshotText);
67
-
68
- // parse the accessibility tree
69
- const a11yTree = A11yParse.A11yTree.parse(snapshotText);
70
-
71
- return a11yTree
93
+ return selectedNodes[0].uid;
72
94
  }
73
95
 
74
96
  /**
@@ -104,8 +126,8 @@ class MainHelper {
104
126
  * @param querySelectors The selectors to query the accessibility tree with.
105
127
  * @returns A text representation of the query results.
106
128
  */
107
- static async querySelectorsAll(mcpClient: McpClient, querySelectors: QuerySelectorsInput): Promise<string> {
108
- const a11yTree = await this._getSnapshotTree(mcpClient);
129
+ static async querySelectorsAll(mcpClient: McpMyClient, querySelectors: QuerySelectorsInput): Promise<string> {
130
+ const { a11yTree } = await this._getSnapshotTree(mcpClient);
109
131
 
110
132
  const responseTexts: string[] = [];
111
133
  for (const querySelector of querySelectors.selectors) {
@@ -133,8 +155,8 @@ class MainHelper {
133
155
  * @param querySelectors The selectors to query the accessibility tree with.
134
156
  * @returns A text representation of the first matching node for each selector.
135
157
  */
136
- static async querySelectors(mcpClient: McpClient, querySelectors: QuerySelectorsFirstInput): Promise<string> {
137
- const a11yTree = await this._getSnapshotTree(mcpClient);
158
+ static async querySelectors(mcpClient: McpMyClient, querySelectors: QuerySelectorsFirstInput): Promise<string> {
159
+ const { a11yTree } = await this._getSnapshotTree(mcpClient);
138
160
 
139
161
  const responseTexts: string[] = [];
140
162
  for (const querySelector of querySelectors.selectors) {
@@ -148,106 +170,82 @@ class MainHelper {
148
170
 
149
171
  ///////////////////////////////////////////////////////////////////////////////
150
172
  ///////////////////////////////////////////////////////////////////////////////
151
- //
173
+ // Init external tools (i.e. tools that are implemented in terms of calls to the mcpClient, rather than directly registered on the mcpClient)
152
174
  ///////////////////////////////////////////////////////////////////////////////
153
175
  ///////////////////////////////////////////////////////////////////////////////
154
176
 
155
- static async commandMcpServer({
156
- verbose = false,
157
- }: {
158
- verbose?: boolean
159
- }): Promise<void> {
177
+ static async initExternalTools(mcpServer: McpServer, mcpClient: McpMyClient): Promise<void> {
178
+ const mcpTarget = await mcpClient.getMcpTarget();
160
179
 
161
180
  ///////////////////////////////////////////////////////////////////////////////
162
181
  ///////////////////////////////////////////////////////////////////////////////
163
- // mcp client
182
+ // list_pages
164
183
  ///////////////////////////////////////////////////////////////////////////////
165
184
  ///////////////////////////////////////////////////////////////////////////////
166
185
 
167
- // Create the mcp client toward the official chrome-devtools-mcp tool, which provides access to a Chrome tab's accessibility
168
- // tree and allows us to take snapshots and query the tree with selectors.
169
- const mcpClient = new McpClient({
170
- name: "my-app",
171
- version: "0.1.0",
172
- transport: {
173
- type: "stdio",
174
- command: "npx",
175
- args: [
176
- "chrome-devtools-mcp@latest", "--autoconnect", "--no-performance-crux", "--no-usage-statistics",
177
- ],
186
+ mcpServer.registerTool(
187
+ McpTargetHelper.EXTERNAL_TOOL_NAME.listPages,
188
+ {
189
+ description: "List the open pages/tabs in the browser",
190
+ inputSchema: z.object({}),
178
191
  },
179
- });
180
-
181
- // Connect the mcp client to the chrome-devtools-mcp tool, which will allow us to take snapshots and query the accessibility tree.
182
- await mcpClient.connect();
183
-
192
+ async () => {
193
+ const toolConfig = await McpTargetHelper.targetToolListPages(mcpTarget);
194
+ const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
195
+ let outputStr = await ResponseFormatter.formatListPages(mcpTarget, callToolResult);
184
196
 
185
- if (verbose) {
186
- // list all the tools available in mcpClient for debugging
187
- const mcpClientTools = await mcpClient.listTools();
188
- console.error("Tools available in mcpClient:");
189
- for (const tool of mcpClientTools) {
190
- console.error(`- ${tool.name}: ${tool.description}`);
197
+ return {
198
+ content: [{ type: "text", text: outputStr }],
199
+ };
191
200
  }
192
- }
201
+ );
193
202
 
194
203
  ///////////////////////////////////////////////////////////////////////////////
195
204
  ///////////////////////////////////////////////////////////////////////////////
196
- //
205
+ // navigate_page
197
206
  ///////////////////////////////////////////////////////////////////////////////
198
207
  ///////////////////////////////////////////////////////////////////////////////
199
208
 
200
- if (false) {
201
- await mcpClient.callTool('navigate_page', {
202
- // url: "https://www.example.com/",
203
- url: "https://welcometothejungle.com/"
204
- });
205
- const responseText = await MainHelper.querySelectorsAll(mcpClient, {
206
- selectors: [
207
- {
208
- selector: "link, button",
209
- withAncestors: true,
210
- limit: 0,
211
- },
212
- {
213
- selector: 'heading[level="1"]',
214
- withAncestors: false,
215
- limit: 0,
216
- },
217
- ],
218
- });
219
- console.log(responseText);
220
- await mcpClient.close();
221
- process.exit(0);
222
- }
209
+ mcpServer.registerTool(
210
+ McpTargetHelper.EXTERNAL_TOOL_NAME.navigatePage,
211
+ {
212
+ description: "Navigate the current page to a new URL",
213
+ inputSchema: z.object({
214
+ url: z.string().describe("The URL to navigate to"),
215
+ }),
216
+ },
217
+ async ({ url }: { url: string }) => {
218
+ const toolConfig = await McpTargetHelper.targetToolNavigatePage(mcpTarget, url);
219
+ const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
220
+ let outputStr = await ResponseFormatter.formatNavigatePage(mcpTarget, callToolResult);
221
+
222
+ return {
223
+ content: [{ type: "text", text: outputStr }],
224
+ };
225
+ }
226
+ );
223
227
 
224
228
  ///////////////////////////////////////////////////////////////////////////////
225
229
  ///////////////////////////////////////////////////////////////////////////////
226
- //
230
+ // list_pages
227
231
  ///////////////////////////////////////////////////////////////////////////////
228
232
  ///////////////////////////////////////////////////////////////////////////////
229
233
 
230
- // Create the MCP proxy server, which will expose tools to the MCP client and proxy calls to those tools to
231
- // the mcpClient connected to the chrome-devtools-mcp.
232
- const mcpProxy = new McpProxy();
233
-
234
- // Proxy the 'most interesting' tools from the mcpClient to the mcpProxy, so that they can be called from the
235
- // MCP server (and thus from an LLM agent connected to the MCP server).
236
- // TODO new_page/navigate_page are very similar - maybe we can unify them in the chrome-devtools-mcp tool and
237
- // - have a single 'open_page' tool instead?
238
- const toolsToProxys: string[] = [
239
- 'list_pages',
240
- 'new_page',
241
- 'close_page',
242
- 'navigate_page',
243
- 'take_snapshot',
244
- ]
245
- for (const toolName of toolsToProxys) {
246
- await mcpProxy.proxyToolCall(mcpClient, toolName)
247
- }
234
+ mcpServer.registerTool(
235
+ McpTargetHelper.EXTERNAL_TOOL_NAME.takeSnapshot,
236
+ {
237
+ description: "Take a snapshot of the current page to get the latest accessibility tree",
238
+ inputSchema: z.object({}),
239
+ },
240
+ async () => {
241
+ const a11yText: string = await MainHelper._getA11yText(mcpClient);
242
+ let outputStr = a11yText
248
243
 
249
- // Connect the MCP proxy server to start accepting connections from MCP clients (e.g. LLM agents).
250
- await mcpProxy.connect();
244
+ return {
245
+ content: [{ type: "text", text: outputStr }],
246
+ };
247
+ }
248
+ );
251
249
 
252
250
  ///////////////////////////////////////////////////////////////////////////////
253
251
  ///////////////////////////////////////////////////////////////////////////////
@@ -269,9 +267,8 @@ class MainHelper {
269
267
  `- You can combine selectors: e.g. 'RootWebArea > link[url*="iana"]'`,
270
268
  ].join('\n');
271
269
 
272
- const mcpServer = await mcpProxy.getMcpServer();
273
270
  mcpServer.registerTool(
274
- "querySelectorsAll",
271
+ McpTargetHelper.EXTERNAL_TOOL_NAME.querySelectorsAll,
275
272
  {
276
273
  description: [
277
274
  `Query the accessibility tree with CSS-like selectors`,
@@ -296,7 +293,7 @@ class MainHelper {
296
293
  ///////////////////////////////////////////////////////////////////////////////
297
294
 
298
295
  mcpServer.registerTool(
299
- "querySelectors",
296
+ McpTargetHelper.EXTERNAL_TOOL_NAME.querySelectors,
300
297
  {
301
298
  description: [
302
299
  `Query the accessibility tree with one or more CSS-like selectors. For each selector, returns the first matching node (or "No node found").`,
@@ -306,7 +303,6 @@ class MainHelper {
306
303
  },
307
304
  async (querySelectorsInput: QuerySelectorsFirstInput) => {
308
305
  const outputText: string = await MainHelper.querySelectors(mcpClient, querySelectorsInput);
309
-
310
306
  return {
311
307
  content: [{ type: "text", text: outputText }],
312
308
  };
@@ -320,7 +316,7 @@ class MainHelper {
320
316
  ///////////////////////////////////////////////////////////////////////////////
321
317
 
322
318
  mcpServer.registerTool(
323
- "pressKeys",
319
+ McpTargetHelper.EXTERNAL_TOOL_NAME.pressKeys,
324
320
  {
325
321
  description: "Press a sequence of keys on the page. E.g. 'Tab', 'Enter', 'ArrowDown'",
326
322
  inputSchema: z.object({
@@ -345,12 +341,22 @@ class MainHelper {
345
341
  console.error("Keys to send:", keysToSend);
346
342
  // chrome-devtools-mcp's 'press_key' tool accepts a single 'key' per call — loop through the sequence
347
343
  for (const key of keysToSend) {
348
- await mcpClient.callTool('press_key', { key });
344
+ const toolConfig = await McpTargetHelper.targetToolPressKey(mcpTarget, key);
345
+ const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
346
+ // trying to handle the error case
347
+ if (callToolResult.isError) {
348
+ console.error(`Error pressing key '${key}':`, callToolResult.error);
349
+ return {
350
+ content: [{ type: "text", text: `Error pressing key '${key}'` }],
351
+ };
352
+ }
349
353
  }
350
354
 
355
+ let outputText = await ResponseFormatter.formatPressKeys(mcpTarget, keysToSend);
356
+
351
357
  // return a response indicating which keys were pressed
352
358
  return {
353
- content: [{ type: "text", text: `Pressed keys: ${keysToSend.join(', ')}` }],
359
+ content: [{ type: "text", text: outputText }],
354
360
  };
355
361
  }
356
362
  );
@@ -362,7 +368,7 @@ class MainHelper {
362
368
  ///////////////////////////////////////////////////////////////////////////////
363
369
 
364
370
  mcpServer.registerTool(
365
- "click",
371
+ McpTargetHelper.EXTERNAL_TOOL_NAME.click,
366
372
  {
367
373
  description: [
368
374
  `Click an element selected by an accessibility selector.`,
@@ -374,7 +380,14 @@ class MainHelper {
374
380
  },
375
381
  async ({ selector }: { selector: string }) => {
376
382
  const uid = await MainHelper._resolveSelectorToUid(mcpClient, selector);
377
- return await mcpClient.callTool('click', { uid });
383
+
384
+ const toolConfig = await McpTargetHelper.targetToolClick(mcpTarget, uid);
385
+ const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
386
+ let outputText = await ResponseFormatter.formatClick(mcpTarget, callToolResult);
387
+
388
+ return {
389
+ content: [{ type: "text", text: outputText }],
390
+ };
378
391
  }
379
392
  );
380
393
 
@@ -385,7 +398,7 @@ class MainHelper {
385
398
  ///////////////////////////////////////////////////////////////////////////////
386
399
 
387
400
  mcpServer.registerTool(
388
- "fill_form",
401
+ McpTargetHelper.EXTERNAL_TOOL_NAME.fillForm,
389
402
  {
390
403
  description: [
391
404
  `Fill one or more form fields, each selected by an accessibility selector.`,
@@ -399,11 +412,40 @@ class MainHelper {
399
412
  },
400
413
  },
401
414
  async ({ elements }: { elements: { selector: string; value: string }[] }) => {
402
- const resolved = await Promise.all(elements.map(async (element) => ({
403
- uid: await MainHelper._resolveSelectorToUid(mcpClient, element.selector),
404
- value: element.value,
405
- })));
406
- return await mcpClient.callTool('fill_form', { elements: resolved });
415
+ const resolved = [];
416
+ for (const element of elements) {
417
+ const uid = await MainHelper._resolveSelectorToUid(mcpClient, element.selector);
418
+ resolved.push({
419
+ uid,
420
+ value: element.value,
421
+ });
422
+ }
423
+ if (mcpTarget === 'chrome_devtools') {
424
+ const toolConfig = await McpTargetHelper.targetToolFillForm(mcpTarget, resolved);
425
+ const callToolResult = await mcpClient.callTool(toolConfig.toolName, toolConfig.toolArgs);
426
+ // const callToolResult = await mcpClient.callTool('fill_form', { elements: resolved });
427
+ return callToolResult
428
+ } else if (mcpTarget === 'playwright') {
429
+ type Field = {
430
+ name: string; // Human readable name for the field, e.g. "Email address"
431
+ // type can be textbox, checkbox, radio, combobox, or slider
432
+ type: 'textbox' | 'checkbox' | 'radio' | 'combobox' | 'slider';
433
+ // the uid of the field's corresponding node in the accessibility tree
434
+ ref: string;
435
+ // 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
436
+ value: string
437
+ };
438
+ const fields: Field[] = resolved.map((element, index) => ({
439
+ name: `Field ${index + 1}`,
440
+ type: 'textbox', // for simplicity, we assume all fields are textboxes in this example
441
+ ref: element.uid,
442
+ value: element.value,
443
+ }));
444
+ const callToolResult = await mcpClient.callTool('browser_fill_form', { fields: fields });
445
+ return callToolResult
446
+ } else {
447
+ throw new Error(`Unsupported MCP target: ${mcpTarget}`);
448
+ }
407
449
  }
408
450
  );
409
451
 
@@ -413,34 +455,144 @@ class MainHelper {
413
455
  ///////////////////////////////////////////////////////////////////////////////
414
456
  ///////////////////////////////////////////////////////////////////////////////
415
457
 
416
- // const mcpServer = await mcpProxy.getMcpServer();
417
- // const timezoneSchema = {
418
- // timezone: z.string().optional().describe("IANA timezone name (e.g. 'America/New_York'). Defaults to local system timezone."),
419
- // };
420
- // mcpServer.registerTool(
421
- // "get_current_datetime",
422
- // {
423
- // description: "Get the current date and time",
424
- // inputSchema: timezoneSchema,
425
- // },
426
- // async ({ timezone }) => {
427
- // const date = new Date();
428
- // const options: Intl.DateTimeFormatOptions = {
429
- // timeZone: timezone,
430
- // year: "numeric",
431
- // month: "2-digit",
432
- // day: "2-digit",
433
- // hour: "2-digit",
434
- // minute: "2-digit",
435
- // second: "2-digit",
436
- // timeZoneName: "short",
437
- // };
438
- // const formatted = new Intl.DateTimeFormat("en-US", options).format(date);
439
- // return {
440
- // content: [{ type: "text", text: formatted }],
441
- // };
442
- // }
443
- // );
458
+ const timezoneSchema = {
459
+ timezone: z.string().optional().describe("IANA timezone name (e.g. 'America/New_York'). Defaults to local system timezone."),
460
+ };
461
+ mcpServer.registerTool(
462
+ McpTargetHelper.EXTERNAL_TOOL_NAME.getCurrentDateTime,
463
+ {
464
+ description: "Get the current date and time",
465
+ inputSchema: timezoneSchema,
466
+ },
467
+ async ({ timezone }) => {
468
+ const date = new Date();
469
+ const options: Intl.DateTimeFormatOptions = {
470
+ timeZone: timezone,
471
+ year: "numeric",
472
+ month: "2-digit",
473
+ day: "2-digit",
474
+ hour: "2-digit",
475
+ minute: "2-digit",
476
+ second: "2-digit",
477
+ timeZoneName: "short",
478
+ };
479
+ const formatted = new Intl.DateTimeFormat("en-US", options).format(date);
480
+ return {
481
+ content: [{ type: "text", text: formatted }],
482
+ };
483
+ }
484
+ );
485
+ }
486
+
487
+ ///////////////////////////////////////////////////////////////////////////////
488
+ ///////////////////////////////////////////////////////////////////////////////
489
+ // .commandMcpServer: the main entry point for starting the MCP proxy server, which connects to the MCP
490
+ // target (e.g. chrome-devtools-mcp), proxies tools from the MCP target, and registers external tools that
491
+ // are implemented in terms of calls to the MCP target.
492
+ ///////////////////////////////////////////////////////////////////////////////
493
+ ///////////////////////////////////////////////////////////////////////////////
494
+
495
+ static async commandMcpServer({
496
+ mcpTarget,
497
+ verbose = false,
498
+ }: {
499
+ mcpTarget: FastBrowserMcpTarget,
500
+ verbose?: boolean
501
+ }): Promise<void> {
502
+ ///////////////////////////////////////////////////////////////////////////////
503
+ ///////////////////////////////////////////////////////////////////////////////
504
+ // mcp client
505
+ ///////////////////////////////////////////////////////////////////////////////
506
+ ///////////////////////////////////////////////////////////////////////////////
507
+
508
+ const { command: mcpCommand, args: mcpArgs } = McpTargetHelper.mcpArgs(mcpTarget);
509
+
510
+ // TODO remove this MCPMyClient - confusing
511
+
512
+ // Create the mcp client toward the official chrome-devtools-mcp tool, which provides access to a Chrome tab's accessibility
513
+ // tree and allows us to take snapshots and query the tree with selectors.
514
+ const mcpClient = new McpMyClient({
515
+ name: "fastbrowser_mcp_proxy_client",
516
+ version: "1.0.0",
517
+ mcpTarget,
518
+ transport: {
519
+ type: "stdio",
520
+ command: mcpCommand,
521
+ args: mcpArgs,
522
+ },
523
+ });
524
+
525
+ // Connect the mcp client to the chrome-devtools-mcp tool, which will allow us to take snapshots and query the accessibility tree.
526
+ await mcpClient.connect();
527
+
528
+ // If verbose mode is enabled, list all the tools available in the mcpClient for debugging purposes.
529
+ if (verbose) {
530
+ // list all the tools available in mcpClient for debugging
531
+ const mcpClientTools = await mcpClient.listTools();
532
+ console.error("Tools available in mcpClient:");
533
+ for (const tool of mcpClientTools) {
534
+ console.error(`- ${tool.name}: ${tool.description}`);
535
+ }
536
+ }
537
+
538
+ ///////////////////////////////////////////////////////////////////////////////
539
+ ///////////////////////////////////////////////////////////////////////////////
540
+ //
541
+ ///////////////////////////////////////////////////////////////////////////////
542
+ ///////////////////////////////////////////////////////////////////////////////
543
+
544
+ if (false) {
545
+ await mcpClient.callTool('navigate_page', {
546
+ // url: "https://www.example.com/",
547
+ url: "https://welcometothejungle.com/"
548
+ });
549
+ const responseText = await MainHelper.querySelectorsAll(mcpClient, {
550
+ selectors: [
551
+ {
552
+ selector: "link, button",
553
+ withAncestors: true,
554
+ limit: 0,
555
+ },
556
+ {
557
+ selector: 'heading[level="1"]',
558
+ withAncestors: false,
559
+ limit: 0,
560
+ },
561
+ ],
562
+ });
563
+ console.log(responseText);
564
+ await mcpClient.close();
565
+ process.exit(0);
566
+ }
567
+
568
+ ///////////////////////////////////////////////////////////////////////////////
569
+ ///////////////////////////////////////////////////////////////////////////////
570
+ //
571
+ ///////////////////////////////////////////////////////////////////////////////
572
+ ///////////////////////////////////////////////////////////////////////////////
573
+
574
+ // Create the MCP proxy server, which will expose tools to the MCP client and proxy calls to those tools to
575
+ // the mcpClient connected to the chrome-devtools-mcp.
576
+ const mcpProxy = new McpProxy();
577
+
578
+ // Proxy the 'most interesting' tools from the mcpClient to the mcpProxy, so that they can be called from the
579
+ // MCP server (and thus from an LLM agent connected to the MCP server).
580
+ const toolsToProxys: string[] = await McpTargetHelper.toolsToProxy(mcpTarget);
581
+ for (const toolName of toolsToProxys) {
582
+ await mcpProxy.proxyToolCall(mcpClient, toolName)
583
+ }
584
+ ///////////////////////////////////////////////////////////////////////////////
585
+ ///////////////////////////////////////////////////////////////////////////////
586
+ // .querySelectorsAll tool implementation
587
+ ///////////////////////////////////////////////////////////////////////////////
588
+ ///////////////////////////////////////////////////////////////////////////////
589
+ const mcpServer = await mcpProxy.getMcpServer();
590
+
591
+ await MainHelper.initExternalTools(mcpServer, mcpClient);
592
+
593
+
594
+ // Connect the MCP proxy server to start accepting connections from MCP clients (e.g. LLM agents).
595
+ await mcpProxy.connect();
444
596
  }
445
597
  }
446
598
 
@@ -451,16 +603,38 @@ class MainHelper {
451
603
  ///////////////////////////////////////////////////////////////////////////////
452
604
 
453
605
  async function main() {
454
- const program = new Command();
606
+ // Redirect process.stderr to a log file, so that the MCP communication is not polluted by logs.
607
+ const logFile = Path.resolve(import.meta.dirname, `../../outputs/fastbrowser_mcp_${new Date().toISOString()}.log`);
608
+ const logStream = Fs.createWriteStream(logFile, { flags: 'a' });
609
+ process.stderr.write = logStream.write.bind(logStream);
455
610
 
611
+
612
+ // 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'");
613
+
614
+ const program = new Command();
456
615
  program
457
616
  .command('mcp_server')
458
617
  .description('Start the MCP proxy server')
459
618
  .option('-v, --verbose', 'Enable verbose logging')
460
- .action(async ({ verbose }: { verbose?: boolean }) => {
461
- await MainHelper.commandMcpServer({ verbose });
619
+ .addOption(
620
+ new Option('-b, --mcp_target <mcpTarget>', 'the MCP> of MCP to run')
621
+ .choices(['chrome_devtools', 'playwright'])
622
+ .default('playwright')
623
+ )
624
+ .action(async (options: { verbose?: boolean, mcp_target: FastBrowserMcpTarget }) => {
625
+ await MainHelper.commandMcpServer({
626
+ mcpTarget: options.mcp_target,
627
+ verbose: options.verbose,
628
+ });
462
629
  });
463
630
 
631
+ // display help if no command is provided
632
+ if (process.argv.length < 3) {
633
+ program.help();
634
+ process.exit(1);
635
+ }
636
+
637
+ // Parse the command-line arguments and execute the appropriate command action.
464
638
  program.parse(process.argv);
465
639
  }
466
640