fastbrowser_cli 1.0.14 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.playwright-mcp/.gitignore +3 -0
- package/README.md +29 -3
- package/dist/fastbrowser_cli/fastbrowser_cli.js +52 -35
- package/dist/fastbrowser_cli/fastbrowser_cli.js.map +1 -1
- package/dist/fastbrowser_cli/libs/http-client.js +3 -7
- package/dist/fastbrowser_cli/libs/http-client.js.map +1 -1
- package/dist/fastbrowser_cli/libs/server-manager.d.ts.map +1 -1
- package/dist/fastbrowser_cli/libs/server-manager.js +31 -29
- package/dist/fastbrowser_cli/libs/server-manager.js.map +1 -1
- package/dist/fastbrowser_httpd/fastbrowser_httpd.js +22 -18
- package/dist/fastbrowser_httpd/fastbrowser_httpd.js.map +1 -1
- package/dist/fastbrowser_httpd/libs/routes.d.ts +2 -2
- package/dist/fastbrowser_httpd/libs/routes.d.ts.map +1 -1
- package/dist/fastbrowser_httpd/libs/routes.js +4 -8
- package/dist/fastbrowser_httpd/libs/routes.js.map +1 -1
- package/dist/fastbrowser_httpd/libs/tool-schemas.js +39 -42
- package/dist/fastbrowser_httpd/libs/tool-schemas.js.map +1 -1
- package/dist/fastbrowser_mcp/fastbrowser_mcp.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/fastbrowser_mcp.js +270 -187
- package/dist/fastbrowser_mcp/fastbrowser_mcp.js.map +1 -1
- package/dist/fastbrowser_mcp/fastbrowser_types.d.ts +5 -0
- package/dist/fastbrowser_mcp/fastbrowser_types.d.ts.map +1 -0
- package/dist/fastbrowser_mcp/fastbrowser_types.js +2 -0
- package/dist/fastbrowser_mcp/fastbrowser_types.js.map +1 -0
- package/dist/{fastweb_mcp/libs/mcp_client.d.ts → fastbrowser_mcp/libs/mcp_client_TOREMOVE.d.ts} +6 -2
- package/dist/fastbrowser_mcp/libs/mcp_client_TOREMOVE.d.ts.map +1 -0
- package/dist/{src/libs/mcp_client.js → fastbrowser_mcp/libs/mcp_client_TOREMOVE.js} +15 -12
- package/dist/fastbrowser_mcp/libs/mcp_client_TOREMOVE.js.map +1 -0
- package/dist/{src/libs/mcp_client.d.ts → fastbrowser_mcp/libs/mcp_my_client.d.ts} +6 -2
- package/dist/fastbrowser_mcp/libs/mcp_my_client.d.ts.map +1 -0
- package/dist/{fastweb_mcp/libs/mcp_client.js → fastbrowser_mcp/libs/mcp_my_client.js} +15 -12
- package/dist/fastbrowser_mcp/libs/mcp_my_client.js.map +1 -0
- package/dist/fastbrowser_mcp/libs/mcp_proxy.d.ts +2 -2
- package/dist/fastbrowser_mcp/libs/mcp_proxy.d.ts.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_proxy.js +9 -16
- package/dist/fastbrowser_mcp/libs/mcp_proxy.js.map +1 -1
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts +35 -0
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.d.ts.map +1 -0
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.js +161 -0
- package/dist/fastbrowser_mcp/libs/mcp_target_helper.js.map +1 -0
- package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.d.ts +28 -0
- package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.d.ts.map +1 -0
- package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.js +210 -0
- package/dist/fastbrowser_mcp/libs/playwright_a11y_helper.js.map +1 -0
- package/dist/fastbrowser_mcp/libs/response_formatter.d.ts +10 -0
- package/dist/fastbrowser_mcp/libs/response_formatter.d.ts.map +1 -0
- package/dist/fastbrowser_mcp/libs/response_formatter.js +155 -0
- package/dist/fastbrowser_mcp/libs/response_formatter.js.map +1 -0
- package/dist/fastbrowser_mcp/libs/schemas.js +14 -17
- package/dist/fastbrowser_mcp/libs/schemas.js.map +1 -1
- package/examples/mcp_client_playwright.ts +34 -0
- package/examples/welcometothejungle/wttj-job.ts +180 -0
- package/examples/welcometothejungle/wttj-search.ts +105 -0
- package/outputs/.gitignore +3 -0
- package/package.json +12 -8
- package/src/fastbrowser_cli/fastbrowser_cli.ts +34 -11
- package/src/fastbrowser_cli/libs/server-manager.ts +23 -14
- package/src/fastbrowser_httpd/fastbrowser_httpd.ts +16 -5
- package/src/fastbrowser_httpd/libs/routes.ts +2 -2
- package/src/fastbrowser_mcp/fastbrowser_mcp.ts +324 -150
- package/src/fastbrowser_mcp/fastbrowser_types.ts +4 -0
- package/src/fastbrowser_mcp/libs/{mcp_client.ts → mcp_client_TOREMOVE.ts} +13 -1
- package/src/fastbrowser_mcp/libs/mcp_my_client.ts +128 -0
- package/src/fastbrowser_mcp/libs/mcp_proxy.ts +2 -2
- package/src/fastbrowser_mcp/libs/mcp_target_helper.ts +164 -0
- package/src/fastbrowser_mcp/libs/playwright_a11y_helper.ts +249 -0
- package/src/fastbrowser_mcp/libs/response_formatter.ts +162 -0
- package/src/fastbrowser_mcp/libs/schemas.ts +2 -2
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +10 -22
- package/dist/contrib/fastweb-cli/fastweb-cli.d.ts +0 -3
- package/dist/contrib/fastweb-cli/fastweb-cli.d.ts.map +0 -1
- package/dist/contrib/fastweb-cli/fastweb-cli.js +0 -151
- package/dist/contrib/fastweb-cli/fastweb-cli.js.map +0 -1
- package/dist/contrib/fastweb-cli/http-client.d.ts +0 -7
- package/dist/contrib/fastweb-cli/http-client.d.ts.map +0 -1
- package/dist/contrib/fastweb-cli/http-client.js +0 -51
- package/dist/contrib/fastweb-cli/http-client.js.map +0 -1
- package/dist/contrib/fastweb-http-server/fastweb-http-server.d.ts +0 -3
- package/dist/contrib/fastweb-http-server/fastweb-http-server.d.ts.map +0 -1
- package/dist/contrib/fastweb-http-server/fastweb-http-server.js +0 -82
- package/dist/contrib/fastweb-http-server/fastweb-http-server.js.map +0 -1
- package/dist/contrib/fastweb-http-server/routes.d.ts +0 -6
- package/dist/contrib/fastweb-http-server/routes.d.ts.map +0 -1
- package/dist/contrib/fastweb-http-server/routes.js +0 -41
- package/dist/contrib/fastweb-http-server/routes.js.map +0 -1
- package/dist/contrib/fastweb-http-server/tool-schemas.d.ts +0 -63
- package/dist/contrib/fastweb-http-server/tool-schemas.d.ts.map +0 -1
- package/dist/contrib/fastweb-http-server/tool-schemas.js +0 -61
- package/dist/contrib/fastweb-http-server/tool-schemas.js.map +0 -1
- package/dist/fastbrowser_mcp/libs/mcp_client.d.ts +0 -120
- package/dist/fastbrowser_mcp/libs/mcp_client.d.ts.map +0 -1
- package/dist/fastbrowser_mcp/libs/mcp_client.js +0 -83
- package/dist/fastbrowser_mcp/libs/mcp_client.js.map +0 -1
- package/dist/fastweb_cli/fastweb_cli.d.ts +0 -3
- package/dist/fastweb_cli/fastweb_cli.d.ts.map +0 -1
- package/dist/fastweb_cli/fastweb_cli.js +0 -254
- package/dist/fastweb_cli/fastweb_cli.js.map +0 -1
- package/dist/fastweb_cli/http-client.d.ts +0 -7
- package/dist/fastweb_cli/http-client.d.ts.map +0 -1
- package/dist/fastweb_cli/http-client.js +0 -51
- package/dist/fastweb_cli/http-client.js.map +0 -1
- package/dist/fastweb_cli/libs/http-client.d.ts +0 -7
- package/dist/fastweb_cli/libs/http-client.d.ts.map +0 -1
- package/dist/fastweb_cli/libs/http-client.js +0 -51
- package/dist/fastweb_cli/libs/http-client.js.map +0 -1
- package/dist/fastweb_cli/libs/server-manager.d.ts +0 -12
- package/dist/fastweb_cli/libs/server-manager.d.ts.map +0 -1
- package/dist/fastweb_cli/libs/server-manager.js +0 -194
- package/dist/fastweb_cli/libs/server-manager.js.map +0 -1
- package/dist/fastweb_http_server/fastweb_http_server.d.ts +0 -3
- package/dist/fastweb_http_server/fastweb_http_server.d.ts.map +0 -1
- package/dist/fastweb_http_server/fastweb_http_server.js +0 -82
- package/dist/fastweb_http_server/fastweb_http_server.js.map +0 -1
- package/dist/fastweb_http_server/libs/routes.d.ts +0 -6
- package/dist/fastweb_http_server/libs/routes.d.ts.map +0 -1
- package/dist/fastweb_http_server/libs/routes.js +0 -41
- package/dist/fastweb_http_server/libs/routes.js.map +0 -1
- package/dist/fastweb_http_server/libs/tool-schemas.d.ts +0 -72
- package/dist/fastweb_http_server/libs/tool-schemas.d.ts.map +0 -1
- package/dist/fastweb_http_server/libs/tool-schemas.js +0 -65
- package/dist/fastweb_http_server/libs/tool-schemas.js.map +0 -1
- package/dist/fastweb_http_server/routes.d.ts +0 -6
- package/dist/fastweb_http_server/routes.d.ts.map +0 -1
- package/dist/fastweb_http_server/routes.js +0 -41
- package/dist/fastweb_http_server/routes.js.map +0 -1
- package/dist/fastweb_http_server/tool-schemas.d.ts +0 -63
- package/dist/fastweb_http_server/tool-schemas.d.ts.map +0 -1
- package/dist/fastweb_http_server/tool-schemas.js +0 -61
- package/dist/fastweb_http_server/tool-schemas.js.map +0 -1
- package/dist/fastweb_mcp/fastweb_mcp.d.ts +0 -4
- package/dist/fastweb_mcp/fastweb_mcp.d.ts.map +0 -1
- package/dist/fastweb_mcp/fastweb_mcp.js +0 -417
- package/dist/fastweb_mcp/fastweb_mcp.js.map +0 -1
- package/dist/fastweb_mcp/libs/mcp_client.d.ts.map +0 -1
- package/dist/fastweb_mcp/libs/mcp_client.js.map +0 -1
- package/dist/fastweb_mcp/libs/mcp_proxy.d.ts +0 -10
- package/dist/fastweb_mcp/libs/mcp_proxy.d.ts.map +0 -1
- package/dist/fastweb_mcp/libs/mcp_proxy.js +0 -45
- package/dist/fastweb_mcp/libs/mcp_proxy.js.map +0 -1
- package/dist/fastweb_mcp/libs/schemas.d.ts +0 -28
- package/dist/fastweb_mcp/libs/schemas.d.ts.map +0 -1
- package/dist/fastweb_mcp/libs/schemas.js +0 -38
- package/dist/fastweb_mcp/libs/schemas.js.map +0 -1
- package/dist/src/fastweb_mcp.d.ts +0 -17
- package/dist/src/fastweb_mcp.d.ts.map +0 -1
- package/dist/src/fastweb_mcp.js +0 -342
- package/dist/src/fastweb_mcp.js.map +0 -1
- package/dist/src/libs/mcp_client.d.ts.map +0 -1
- package/dist/src/libs/mcp_client.js.map +0 -1
- package/dist/src/libs/mcp_proxy.d.ts +0 -10
- package/dist/src/libs/mcp_proxy.d.ts.map +0 -1
- package/dist/src/libs/mcp_proxy.js +0 -45
- 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 "
|
|
11
|
+
import * as A11yParse from "a11y_parse";
|
|
7
12
|
|
|
8
13
|
// local imports
|
|
9
|
-
import {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
156
|
-
|
|
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
|
-
//
|
|
182
|
+
// list_pages
|
|
164
183
|
///////////////////////////////////////////////////////////////////////////////
|
|
165
184
|
///////////////////////////////////////////////////////////////////////////////
|
|
166
185
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
461
|
-
|
|
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
|
|