browser-use 0.0.1 → 0.0.2
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/LICENSE +21 -0
- package/README.md +761 -0
- package/dist/agent/cloud-events.d.ts +264 -0
- package/dist/agent/cloud-events.js +318 -0
- package/dist/agent/gif.d.ts +15 -0
- package/dist/agent/gif.js +215 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.js +8 -0
- package/dist/agent/message-manager/service.d.ts +30 -0
- package/dist/agent/message-manager/service.js +208 -0
- package/dist/agent/message-manager/utils.d.ts +2 -0
- package/dist/agent/message-manager/utils.js +41 -0
- package/dist/agent/message-manager/views.d.ts +26 -0
- package/dist/agent/message-manager/views.js +73 -0
- package/dist/agent/prompts.d.ts +52 -0
- package/dist/agent/prompts.js +259 -0
- package/dist/agent/service.d.ts +290 -0
- package/dist/agent/service.js +2200 -0
- package/dist/agent/views.d.ts +741 -0
- package/dist/agent/views.js +537 -0
- package/dist/browser/browser.d.ts +7 -0
- package/dist/browser/browser.js +5 -0
- package/dist/browser/context.d.ts +8 -0
- package/dist/browser/context.js +4 -0
- package/dist/browser/dvd-screensaver.d.ts +101 -0
- package/dist/browser/dvd-screensaver.js +270 -0
- package/dist/browser/extensions.d.ts +63 -0
- package/dist/browser/extensions.js +359 -0
- package/dist/browser/index.d.ts +10 -0
- package/dist/browser/index.js +9 -0
- package/dist/browser/playwright-manager.d.ts +47 -0
- package/dist/browser/playwright-manager.js +146 -0
- package/dist/browser/profile.d.ts +196 -0
- package/dist/browser/profile.js +815 -0
- package/dist/browser/session.d.ts +505 -0
- package/dist/browser/session.js +3409 -0
- package/dist/browser/types.d.ts +1184 -0
- package/dist/browser/types.js +1 -0
- package/dist/browser/utils.d.ts +1 -0
- package/dist/browser/utils.js +19 -0
- package/dist/browser/views.d.ts +78 -0
- package/dist/browser/views.js +72 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +44 -0
- package/dist/config.d.ts +108 -0
- package/dist/config.js +430 -0
- package/dist/controller/index.d.ts +3 -0
- package/dist/controller/index.js +3 -0
- package/dist/controller/registry/index.d.ts +2 -0
- package/dist/controller/registry/index.js +2 -0
- package/dist/controller/registry/service.d.ts +45 -0
- package/dist/controller/registry/service.js +184 -0
- package/dist/controller/registry/views.d.ts +55 -0
- package/dist/controller/registry/views.js +174 -0
- package/dist/controller/service.d.ts +49 -0
- package/dist/controller/service.js +1176 -0
- package/dist/controller/views.d.ts +241 -0
- package/dist/controller/views.js +88 -0
- package/dist/dom/clickable-element-processor/service.d.ts +11 -0
- package/dist/dom/clickable-element-processor/service.js +60 -0
- package/dist/dom/dom_tree/index.js +1400 -0
- package/dist/dom/history-tree-processor/service.d.ts +14 -0
- package/dist/dom/history-tree-processor/service.js +75 -0
- package/dist/dom/history-tree-processor/view.d.ts +54 -0
- package/dist/dom/history-tree-processor/view.js +56 -0
- package/dist/dom/playground/extraction.d.ts +19 -0
- package/dist/dom/playground/extraction.js +187 -0
- package/dist/dom/playground/process-dom.d.ts +1 -0
- package/dist/dom/playground/process-dom.js +5 -0
- package/dist/dom/playground/test-accessibility.d.ts +44 -0
- package/dist/dom/playground/test-accessibility.js +111 -0
- package/dist/dom/service.d.ts +19 -0
- package/dist/dom/service.js +227 -0
- package/dist/dom/utils.d.ts +1 -0
- package/dist/dom/utils.js +6 -0
- package/dist/dom/views.d.ts +61 -0
- package/dist/dom/views.js +247 -0
- package/dist/event-bus.d.ts +11 -0
- package/dist/event-bus.js +19 -0
- package/dist/exceptions.d.ts +10 -0
- package/dist/exceptions.js +22 -0
- package/dist/filesystem/file-system.d.ts +68 -0
- package/dist/filesystem/file-system.js +412 -0
- package/dist/filesystem/index.d.ts +1 -0
- package/dist/filesystem/index.js +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +33 -0
- package/dist/integrations/gmail/actions.d.ts +12 -0
- package/dist/integrations/gmail/actions.js +113 -0
- package/dist/integrations/gmail/index.d.ts +2 -0
- package/dist/integrations/gmail/index.js +2 -0
- package/dist/integrations/gmail/service.d.ts +61 -0
- package/dist/integrations/gmail/service.js +260 -0
- package/dist/llm/anthropic/chat.d.ts +28 -0
- package/dist/llm/anthropic/chat.js +126 -0
- package/dist/llm/anthropic/index.d.ts +2 -0
- package/dist/llm/anthropic/index.js +2 -0
- package/dist/llm/anthropic/serializer.d.ts +68 -0
- package/dist/llm/anthropic/serializer.js +285 -0
- package/dist/llm/aws/chat-anthropic.d.ts +61 -0
- package/dist/llm/aws/chat-anthropic.js +176 -0
- package/dist/llm/aws/chat-bedrock.d.ts +15 -0
- package/dist/llm/aws/chat-bedrock.js +80 -0
- package/dist/llm/aws/index.d.ts +3 -0
- package/dist/llm/aws/index.js +3 -0
- package/dist/llm/aws/serializer.d.ts +5 -0
- package/dist/llm/aws/serializer.js +68 -0
- package/dist/llm/azure/chat.d.ts +15 -0
- package/dist/llm/azure/chat.js +83 -0
- package/dist/llm/azure/index.d.ts +1 -0
- package/dist/llm/azure/index.js +1 -0
- package/dist/llm/base.d.ts +16 -0
- package/dist/llm/base.js +1 -0
- package/dist/llm/deepseek/chat.d.ts +15 -0
- package/dist/llm/deepseek/chat.js +51 -0
- package/dist/llm/deepseek/index.d.ts +2 -0
- package/dist/llm/deepseek/index.js +2 -0
- package/dist/llm/deepseek/serializer.d.ts +6 -0
- package/dist/llm/deepseek/serializer.js +57 -0
- package/dist/llm/exceptions.d.ts +10 -0
- package/dist/llm/exceptions.js +18 -0
- package/dist/llm/google/chat.d.ts +20 -0
- package/dist/llm/google/chat.js +144 -0
- package/dist/llm/google/index.d.ts +2 -0
- package/dist/llm/google/index.js +2 -0
- package/dist/llm/google/serializer.d.ts +6 -0
- package/dist/llm/google/serializer.js +64 -0
- package/dist/llm/groq/chat.d.ts +15 -0
- package/dist/llm/groq/chat.js +52 -0
- package/dist/llm/groq/index.d.ts +3 -0
- package/dist/llm/groq/index.js +3 -0
- package/dist/llm/groq/parser.d.ts +32 -0
- package/dist/llm/groq/parser.js +189 -0
- package/dist/llm/groq/serializer.d.ts +6 -0
- package/dist/llm/groq/serializer.js +56 -0
- package/dist/llm/messages.d.ts +77 -0
- package/dist/llm/messages.js +157 -0
- package/dist/llm/ollama/chat.d.ts +15 -0
- package/dist/llm/ollama/chat.js +77 -0
- package/dist/llm/ollama/index.d.ts +2 -0
- package/dist/llm/ollama/index.js +2 -0
- package/dist/llm/ollama/serializer.d.ts +6 -0
- package/dist/llm/ollama/serializer.js +53 -0
- package/dist/llm/openai/chat.d.ts +38 -0
- package/dist/llm/openai/chat.js +174 -0
- package/dist/llm/openai/index.d.ts +3 -0
- package/dist/llm/openai/index.js +3 -0
- package/dist/llm/openai/like.d.ts +17 -0
- package/dist/llm/openai/like.js +19 -0
- package/dist/llm/openai/serializer.d.ts +6 -0
- package/dist/llm/openai/serializer.js +57 -0
- package/dist/llm/openrouter/chat.d.ts +15 -0
- package/dist/llm/openrouter/chat.js +74 -0
- package/dist/llm/openrouter/index.d.ts +2 -0
- package/dist/llm/openrouter/index.js +2 -0
- package/dist/llm/openrouter/serializer.d.ts +3 -0
- package/dist/llm/openrouter/serializer.js +3 -0
- package/dist/llm/schema.d.ts +6 -0
- package/dist/llm/schema.js +77 -0
- package/dist/llm/views.d.ts +15 -0
- package/dist/llm/views.js +12 -0
- package/dist/logging-config.d.ts +25 -0
- package/dist/logging-config.js +89 -0
- package/dist/mcp/client.d.ts +142 -0
- package/dist/mcp/client.js +638 -0
- package/dist/mcp/controller.d.ts +6 -0
- package/dist/mcp/controller.js +38 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/server.d.ts +134 -0
- package/dist/mcp/server.js +759 -0
- package/dist/observability-decorators.d.ts +158 -0
- package/dist/observability-decorators.js +286 -0
- package/dist/observability.d.ts +23 -0
- package/dist/observability.js +58 -0
- package/dist/screenshots/index.d.ts +1 -0
- package/dist/screenshots/index.js +1 -0
- package/dist/screenshots/service.d.ts +6 -0
- package/dist/screenshots/service.js +28 -0
- package/dist/sync/auth.d.ts +27 -0
- package/dist/sync/auth.js +205 -0
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.js +2 -0
- package/dist/sync/service.d.ts +21 -0
- package/dist/sync/service.js +146 -0
- package/dist/telemetry/index.d.ts +2 -0
- package/dist/telemetry/index.js +2 -0
- package/dist/telemetry/service.d.ts +12 -0
- package/dist/telemetry/service.js +85 -0
- package/dist/telemetry/views.d.ts +112 -0
- package/dist/telemetry/views.js +112 -0
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.js +2 -0
- package/dist/tokens/service.d.ts +35 -0
- package/dist/tokens/service.js +423 -0
- package/dist/tokens/views.d.ts +58 -0
- package/dist/tokens/views.js +1 -0
- package/dist/utils.d.ts +128 -0
- package/dist/utils.js +529 -0
- package/package.json +94 -5
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) client integration for browser-use.
|
|
3
|
+
*
|
|
4
|
+
* This module provides integration between external MCP servers and browser-use's action registry.
|
|
5
|
+
* MCP tools are dynamically discovered and registered as browser-use actions.
|
|
6
|
+
*
|
|
7
|
+
* Example usage:
|
|
8
|
+
* import { Controller } from './controller/service.js';
|
|
9
|
+
* import { MCPClient } from './mcp/client.js';
|
|
10
|
+
*
|
|
11
|
+
* const controller = new Controller();
|
|
12
|
+
*
|
|
13
|
+
* // Connect to an MCP server
|
|
14
|
+
* const mcpClient = new MCPClient(
|
|
15
|
+
* 'my-server',
|
|
16
|
+
* 'npx',
|
|
17
|
+
* ['@mycompany/mcp-server@latest']
|
|
18
|
+
* );
|
|
19
|
+
*
|
|
20
|
+
* // Register all MCP tools as browser-use actions
|
|
21
|
+
* await mcpClient.registerToController(controller);
|
|
22
|
+
*
|
|
23
|
+
* // Now use with Agent as normal - MCP tools are available as actions
|
|
24
|
+
*/
|
|
25
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
26
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
27
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
import { createLogger } from '../logging-config.js';
|
|
30
|
+
import { ActionResult } from '../agent/views.js';
|
|
31
|
+
import { productTelemetry } from '../telemetry/service.js';
|
|
32
|
+
import { MCPClientTelemetryEvent } from '../telemetry/views.js';
|
|
33
|
+
import { get_browser_use_version, retryAsync } from '../utils.js';
|
|
34
|
+
const logger = createLogger('browser_use.mcp.client');
|
|
35
|
+
export class MCPClient {
|
|
36
|
+
client;
|
|
37
|
+
command;
|
|
38
|
+
args;
|
|
39
|
+
env;
|
|
40
|
+
serverName;
|
|
41
|
+
_tools = new Map();
|
|
42
|
+
_prompts = new Map();
|
|
43
|
+
_registeredActions = new Set();
|
|
44
|
+
_connected = false;
|
|
45
|
+
_connecting = false;
|
|
46
|
+
_toolCallCount = 0;
|
|
47
|
+
_errorCount = 0;
|
|
48
|
+
_lastConnectTime;
|
|
49
|
+
_lastHealthCheck;
|
|
50
|
+
_healthCheckInterval;
|
|
51
|
+
// Options
|
|
52
|
+
maxRetries;
|
|
53
|
+
connectionTimeout;
|
|
54
|
+
toolCallTimeout;
|
|
55
|
+
autoReconnect;
|
|
56
|
+
healthCheckIntervalSeconds;
|
|
57
|
+
constructor(serverName, command, args = [], env, options = {}) {
|
|
58
|
+
this.serverName = serverName;
|
|
59
|
+
this.command = command;
|
|
60
|
+
this.args = args;
|
|
61
|
+
this.env = env;
|
|
62
|
+
// Set options with defaults
|
|
63
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
64
|
+
this.connectionTimeout = options.connectionTimeout ?? 30;
|
|
65
|
+
this.toolCallTimeout = options.toolCallTimeout ?? 60;
|
|
66
|
+
this.autoReconnect = options.autoReconnect ?? true;
|
|
67
|
+
this.healthCheckIntervalSeconds = options.healthCheckInterval ?? 30;
|
|
68
|
+
this.client = new Client({
|
|
69
|
+
name: 'browser-use',
|
|
70
|
+
version: get_browser_use_version(),
|
|
71
|
+
}, {
|
|
72
|
+
capabilities: {},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Connect to the MCP server and discover available tools
|
|
77
|
+
*/
|
|
78
|
+
async connect(timeout) {
|
|
79
|
+
if (this._connected) {
|
|
80
|
+
logger.debug(`Already connected to ${this.serverName}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (this._connecting) {
|
|
84
|
+
logger.debug(`Connection already in progress for ${this.serverName}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this._connecting = true;
|
|
88
|
+
const actualTimeout = timeout ?? this.connectionTimeout;
|
|
89
|
+
const startTime = Date.now() / 1000;
|
|
90
|
+
let errorMsg = null;
|
|
91
|
+
try {
|
|
92
|
+
logger.info(`🔌 Connecting to MCP server '${this.serverName}': ${this.command} ${this.args.join(' ')}`);
|
|
93
|
+
// Use retry logic for connection
|
|
94
|
+
await retryAsync(async () => {
|
|
95
|
+
// Create transport with environment variables
|
|
96
|
+
const transport = new StdioClientTransport({
|
|
97
|
+
command: this.command,
|
|
98
|
+
args: this.args,
|
|
99
|
+
env: this.env,
|
|
100
|
+
});
|
|
101
|
+
// Connect with timeout
|
|
102
|
+
await this._connectWithTimeout(transport, actualTimeout);
|
|
103
|
+
}, {
|
|
104
|
+
maxAttempts: this.maxRetries,
|
|
105
|
+
delayMs: 1000,
|
|
106
|
+
backoffMultiplier: 2,
|
|
107
|
+
onRetry: (error, attempt, delay) => {
|
|
108
|
+
logger.warning(`Connection attempt ${attempt} failed for '${this.serverName}': ${error.message}. Retrying in ${delay}ms...`);
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
this._connected = true;
|
|
112
|
+
this._lastConnectTime = Date.now() / 1000;
|
|
113
|
+
// Discover available tools
|
|
114
|
+
const result = await this.client.request(ListToolsRequestSchema, {});
|
|
115
|
+
this._tools = new Map(result.tools.map((tool) => [tool.name, tool]));
|
|
116
|
+
// Try to discover prompts (optional)
|
|
117
|
+
try {
|
|
118
|
+
await this.listPrompts();
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Prompts are optional, ignore failures
|
|
122
|
+
}
|
|
123
|
+
logger.info(`📦 Discovered ${this._tools.size} tools${this._prompts.size > 0 ? ` and ${this._prompts.size} prompts` : ''} from '${this.serverName}'`);
|
|
124
|
+
// Start health checks
|
|
125
|
+
this._startHealthCheck();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
errorMsg = error instanceof Error ? error.message : String(error);
|
|
129
|
+
this._connected = false;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
this._connecting = false;
|
|
134
|
+
// Capture telemetry for connect action
|
|
135
|
+
const duration = Date.now() / 1000 - startTime;
|
|
136
|
+
productTelemetry.capture(new MCPClientTelemetryEvent({
|
|
137
|
+
server_name: this.serverName,
|
|
138
|
+
command: this.command,
|
|
139
|
+
tools_discovered: this._tools.size,
|
|
140
|
+
version: get_browser_use_version(),
|
|
141
|
+
action: 'connect',
|
|
142
|
+
duration_seconds: duration,
|
|
143
|
+
error_message: errorMsg,
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async _connectWithTimeout(transport, timeoutSeconds) {
|
|
148
|
+
return new Promise(async (resolve, reject) => {
|
|
149
|
+
const timeoutHandle = setTimeout(() => {
|
|
150
|
+
reject(new Error(`Failed to connect to MCP server '${this.serverName}' after ${timeoutSeconds} seconds`));
|
|
151
|
+
}, timeoutSeconds * 1000);
|
|
152
|
+
try {
|
|
153
|
+
await this.client.connect(transport);
|
|
154
|
+
clearTimeout(timeoutHandle);
|
|
155
|
+
resolve();
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
clearTimeout(timeoutHandle);
|
|
159
|
+
reject(error);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Disconnect from the MCP server
|
|
165
|
+
*/
|
|
166
|
+
async disconnect() {
|
|
167
|
+
if (!this._connected) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const startTime = Date.now() / 1000;
|
|
171
|
+
let errorMsg = null;
|
|
172
|
+
try {
|
|
173
|
+
logger.info(`🔌 Disconnecting from MCP server '${this.serverName}'`);
|
|
174
|
+
// Stop health checks
|
|
175
|
+
this._stopHealthCheck();
|
|
176
|
+
await this.client.close();
|
|
177
|
+
this._connected = false;
|
|
178
|
+
this._tools.clear();
|
|
179
|
+
this._prompts.clear();
|
|
180
|
+
this._registeredActions.clear();
|
|
181
|
+
const stats = this.getStats();
|
|
182
|
+
logger.info(`Disconnected from '${this.serverName}' (${stats.toolCallCount} tool calls, ${(stats.successRate * 100).toFixed(1)}% success rate)`);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
errorMsg = error instanceof Error ? error.message : String(error);
|
|
186
|
+
logger.error(`Error disconnecting from MCP server: ${errorMsg}`);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
// Capture telemetry for disconnect action
|
|
190
|
+
const duration = Date.now() / 1000 - startTime;
|
|
191
|
+
productTelemetry.capture(new MCPClientTelemetryEvent({
|
|
192
|
+
server_name: this.serverName,
|
|
193
|
+
command: this.command,
|
|
194
|
+
tools_discovered: 0, // Tools cleared on disconnect
|
|
195
|
+
version: get_browser_use_version(),
|
|
196
|
+
action: 'disconnect',
|
|
197
|
+
duration_seconds: duration,
|
|
198
|
+
error_message: errorMsg,
|
|
199
|
+
}));
|
|
200
|
+
productTelemetry.flush();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* List all available tools from the MCP server
|
|
205
|
+
*/
|
|
206
|
+
async listTools() {
|
|
207
|
+
if (!this._connected) {
|
|
208
|
+
await this.connect();
|
|
209
|
+
}
|
|
210
|
+
return Array.from(this._tools.values());
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Call a tool on the MCP server
|
|
214
|
+
*/
|
|
215
|
+
async callTool(name, args) {
|
|
216
|
+
if (!this._connected) {
|
|
217
|
+
throw new Error(`MCP server '${this.serverName}' not connected`);
|
|
218
|
+
}
|
|
219
|
+
const startTime = Date.now() / 1000;
|
|
220
|
+
let errorMsg = null;
|
|
221
|
+
try {
|
|
222
|
+
logger.debug(`🔧 Calling MCP tool '${name}' with params: ${JSON.stringify(args)}`);
|
|
223
|
+
this._toolCallCount++;
|
|
224
|
+
const result = await this.client.request(CallToolRequestSchema, {
|
|
225
|
+
name,
|
|
226
|
+
arguments: args,
|
|
227
|
+
});
|
|
228
|
+
return result.content;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
this._errorCount++;
|
|
232
|
+
errorMsg = error instanceof Error ? error.message : String(error);
|
|
233
|
+
logger.error(`MCP tool '${name}' failed: ${errorMsg}`);
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
// Capture telemetry for tool call
|
|
238
|
+
const duration = Date.now() / 1000 - startTime;
|
|
239
|
+
productTelemetry.capture(new MCPClientTelemetryEvent({
|
|
240
|
+
server_name: this.serverName,
|
|
241
|
+
command: this.command,
|
|
242
|
+
tools_discovered: this._tools.size,
|
|
243
|
+
version: get_browser_use_version(),
|
|
244
|
+
action: 'tool_call',
|
|
245
|
+
tool_name: name,
|
|
246
|
+
duration_seconds: duration,
|
|
247
|
+
error_message: errorMsg,
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Register MCP tools as actions in the browser-use controller
|
|
253
|
+
*
|
|
254
|
+
* @param controller - Browser-use controller to register actions to
|
|
255
|
+
* @param toolFilter - Optional list of tool names to register (undefined = all tools)
|
|
256
|
+
* @param prefix - Optional prefix to add to action names (e.g., "playwright_")
|
|
257
|
+
*/
|
|
258
|
+
async registerToController(controller, toolFilter, prefix) {
|
|
259
|
+
if (!this._connected) {
|
|
260
|
+
await this.connect();
|
|
261
|
+
}
|
|
262
|
+
const registry = controller.registry;
|
|
263
|
+
for (const [toolName, tool] of this._tools.entries()) {
|
|
264
|
+
// Skip if not in filter
|
|
265
|
+
if (toolFilter && !toolFilter.includes(toolName)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
// Apply prefix if specified
|
|
269
|
+
const actionName = prefix ? `${prefix}${toolName}` : toolName;
|
|
270
|
+
// Skip if already registered
|
|
271
|
+
if (this._registeredActions.has(actionName)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
// Register the tool as an action
|
|
275
|
+
this._registerToolAsAction(registry, actionName, tool);
|
|
276
|
+
this._registeredActions.add(actionName);
|
|
277
|
+
}
|
|
278
|
+
logger.info(`✅ Registered ${this._registeredActions.size} MCP tools from '${this.serverName}' as browser-use actions`);
|
|
279
|
+
}
|
|
280
|
+
_registerToolAsAction(registry, actionName, tool) {
|
|
281
|
+
/**
|
|
282
|
+
* Register a single MCP tool as a browser-use action
|
|
283
|
+
*/
|
|
284
|
+
// Create async wrapper function for the MCP tool
|
|
285
|
+
const mcpActionWrapper = async (params) => {
|
|
286
|
+
if (!this._connected) {
|
|
287
|
+
return new ActionResult({
|
|
288
|
+
error: `MCP server '${this.serverName}' not connected`,
|
|
289
|
+
success: false,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
const startTime = Date.now() / 1000;
|
|
293
|
+
let errorMsg = null;
|
|
294
|
+
try {
|
|
295
|
+
// Call the MCP tool
|
|
296
|
+
const result = await this.callTool(tool.name, params || {});
|
|
297
|
+
// Convert MCP result to ActionResult
|
|
298
|
+
const extractedContent = this._formatMcpResult(result);
|
|
299
|
+
return new ActionResult({
|
|
300
|
+
extracted_content: extractedContent,
|
|
301
|
+
long_term_memory: `Used MCP tool '${tool.name}' from ${this.serverName}`,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
errorMsg = error instanceof Error ? error.message : String(error);
|
|
306
|
+
logger.error(`MCP tool '${tool.name}' failed: ${errorMsg}`);
|
|
307
|
+
return new ActionResult({
|
|
308
|
+
error: `MCP tool '${tool.name}' failed: ${errorMsg}`,
|
|
309
|
+
success: false,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
// Set function metadata for better debugging
|
|
314
|
+
Object.defineProperty(mcpActionWrapper, 'name', { value: actionName });
|
|
315
|
+
// Register the action with browser-use
|
|
316
|
+
const description = tool.description || `MCP tool from ${this.serverName}: ${tool.name}`;
|
|
317
|
+
const paramModel = this._convertToolSchemaToParamModel(tool.inputSchema);
|
|
318
|
+
// Use the registry's action decorator
|
|
319
|
+
registry.action(description, {
|
|
320
|
+
param_model: paramModel,
|
|
321
|
+
})(mcpActionWrapper);
|
|
322
|
+
logger.debug(`✅ Registered MCP tool '${tool.name}' as action '${actionName}'`);
|
|
323
|
+
}
|
|
324
|
+
_convertToolSchemaToParamModel(inputSchema) {
|
|
325
|
+
if (!inputSchema || typeof inputSchema !== 'object') {
|
|
326
|
+
return z.object({}).strict();
|
|
327
|
+
}
|
|
328
|
+
const schemaObject = inputSchema;
|
|
329
|
+
if (Object.keys(schemaObject).length === 0) {
|
|
330
|
+
return z.object({}).strict();
|
|
331
|
+
}
|
|
332
|
+
const converted = this._jsonSchemaToZod(schemaObject, 'input');
|
|
333
|
+
if (converted instanceof z.ZodObject) {
|
|
334
|
+
return converted.strict();
|
|
335
|
+
}
|
|
336
|
+
return z.any();
|
|
337
|
+
}
|
|
338
|
+
_jsonSchemaToZod(schema, path) {
|
|
339
|
+
const typeValue = schema.type;
|
|
340
|
+
const enumValues = Array.isArray(schema.enum) ? schema.enum : null;
|
|
341
|
+
if ('const' in schema) {
|
|
342
|
+
if (this._isLiteralPrimitive(schema.const)) {
|
|
343
|
+
return this._applySchemaMetadata(z.literal(schema.const), schema);
|
|
344
|
+
}
|
|
345
|
+
return this._applySchemaMetadata(z.any(), schema);
|
|
346
|
+
}
|
|
347
|
+
if (enumValues && enumValues.length > 0) {
|
|
348
|
+
const enumSchema = this._toLiteralUnion(enumValues);
|
|
349
|
+
return this._applySchemaMetadata(enumSchema, schema);
|
|
350
|
+
}
|
|
351
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
|
|
352
|
+
const options = schema.oneOf.map((option, index) => this._jsonSchemaToZod(this._toJsonSchemaObject(option), `${path}.oneOf[${index}]`));
|
|
353
|
+
const union = options.length === 1 ? options[0] : z.union(options);
|
|
354
|
+
return this._applySchemaMetadata(union, schema);
|
|
355
|
+
}
|
|
356
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
|
|
357
|
+
const options = schema.anyOf.map((option, index) => this._jsonSchemaToZod(this._toJsonSchemaObject(option), `${path}.anyOf[${index}]`));
|
|
358
|
+
const union = options.length === 1 ? options[0] : z.union(options);
|
|
359
|
+
return this._applySchemaMetadata(union, schema);
|
|
360
|
+
}
|
|
361
|
+
if (Array.isArray(typeValue)) {
|
|
362
|
+
const hasNull = typeValue.includes('null');
|
|
363
|
+
const nonNullTypes = typeValue.filter((entry) => entry !== 'null');
|
|
364
|
+
if (nonNullTypes.length === 1) {
|
|
365
|
+
const base = this._jsonSchemaToZod({ ...schema, type: nonNullTypes[0] }, path);
|
|
366
|
+
return hasNull ? base.nullable() : base;
|
|
367
|
+
}
|
|
368
|
+
if (nonNullTypes.length > 1) {
|
|
369
|
+
const options = nonNullTypes.map((entry) => this._jsonSchemaToZod({ ...schema, type: entry }, path));
|
|
370
|
+
const union = options.length === 1 ? options[0] : z.union(options);
|
|
371
|
+
return hasNull ? union.nullable() : union;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
let zodSchema;
|
|
375
|
+
if (typeValue === 'object' || schema.properties) {
|
|
376
|
+
const properties = schema.properties && typeof schema.properties === 'object'
|
|
377
|
+
? schema.properties
|
|
378
|
+
: {};
|
|
379
|
+
const required = new Set(Array.isArray(schema.required)
|
|
380
|
+
? schema.required.filter((entry) => typeof entry === 'string')
|
|
381
|
+
: []);
|
|
382
|
+
const shape = {};
|
|
383
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
384
|
+
const propertySchema = this._toJsonSchemaObject(value);
|
|
385
|
+
const hasDefault = Object.prototype.hasOwnProperty.call(propertySchema, 'default');
|
|
386
|
+
let field = this._jsonSchemaToZod(propertySchema, `${path}.${key}`);
|
|
387
|
+
if (hasDefault) {
|
|
388
|
+
field = this._applyDefault(field, propertySchema.default);
|
|
389
|
+
}
|
|
390
|
+
else if (!required.has(key)) {
|
|
391
|
+
field = field.optional();
|
|
392
|
+
}
|
|
393
|
+
shape[key] = field;
|
|
394
|
+
}
|
|
395
|
+
zodSchema = z.object(shape);
|
|
396
|
+
}
|
|
397
|
+
else if (typeValue === 'array') {
|
|
398
|
+
const itemsSchema = this._toJsonSchemaObject(schema.items);
|
|
399
|
+
zodSchema = z.array(this._jsonSchemaToZod(itemsSchema, `${path}[]`));
|
|
400
|
+
}
|
|
401
|
+
else if (typeValue === 'string') {
|
|
402
|
+
zodSchema = z.string();
|
|
403
|
+
}
|
|
404
|
+
else if (typeValue === 'integer') {
|
|
405
|
+
zodSchema = z.number().int();
|
|
406
|
+
}
|
|
407
|
+
else if (typeValue === 'number') {
|
|
408
|
+
zodSchema = z.number();
|
|
409
|
+
}
|
|
410
|
+
else if (typeValue === 'boolean') {
|
|
411
|
+
zodSchema = z.boolean();
|
|
412
|
+
}
|
|
413
|
+
else if (typeValue === 'null') {
|
|
414
|
+
zodSchema = z.null();
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
zodSchema = z.any();
|
|
418
|
+
}
|
|
419
|
+
return this._applySchemaMetadata(zodSchema, schema);
|
|
420
|
+
}
|
|
421
|
+
_toJsonSchemaObject(input) {
|
|
422
|
+
if (!input || typeof input !== 'object') {
|
|
423
|
+
return {};
|
|
424
|
+
}
|
|
425
|
+
return input;
|
|
426
|
+
}
|
|
427
|
+
_toLiteralUnion(values) {
|
|
428
|
+
const literalValues = values.filter((value) => this._isLiteralPrimitive(value));
|
|
429
|
+
if (!literalValues.length) {
|
|
430
|
+
return z.any();
|
|
431
|
+
}
|
|
432
|
+
if (literalValues.length === 1) {
|
|
433
|
+
return z.literal(literalValues[0]);
|
|
434
|
+
}
|
|
435
|
+
const literals = literalValues.map((value) => z.literal(value));
|
|
436
|
+
return z.union(literals);
|
|
437
|
+
}
|
|
438
|
+
_isLiteralPrimitive(value) {
|
|
439
|
+
return (typeof value === 'string' ||
|
|
440
|
+
typeof value === 'number' ||
|
|
441
|
+
typeof value === 'boolean' ||
|
|
442
|
+
value === null);
|
|
443
|
+
}
|
|
444
|
+
_applyDefault(schema, value) {
|
|
445
|
+
try {
|
|
446
|
+
return schema.default(value);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return schema;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
_applySchemaMetadata(schema, sourceSchema) {
|
|
453
|
+
let result = schema;
|
|
454
|
+
if (sourceSchema.nullable === true) {
|
|
455
|
+
result = result.nullable();
|
|
456
|
+
}
|
|
457
|
+
if (typeof sourceSchema.description === 'string') {
|
|
458
|
+
result = result.describe(sourceSchema.description);
|
|
459
|
+
}
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
_formatMcpResult(result) {
|
|
463
|
+
/**
|
|
464
|
+
* Format MCP tool result into a string for ActionResult
|
|
465
|
+
*/
|
|
466
|
+
// Handle different MCP result formats
|
|
467
|
+
if (result && typeof result === 'object') {
|
|
468
|
+
if (Array.isArray(result)) {
|
|
469
|
+
// List of content items
|
|
470
|
+
const parts = [];
|
|
471
|
+
for (const item of result) {
|
|
472
|
+
if (item && typeof item === 'object' && 'text' in item) {
|
|
473
|
+
parts.push(String(item.text));
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
parts.push(String(item));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return parts.join('\n');
|
|
480
|
+
}
|
|
481
|
+
else if ('text' in result) {
|
|
482
|
+
return String(result.text);
|
|
483
|
+
}
|
|
484
|
+
else if ('content' in result) {
|
|
485
|
+
// Structured content response
|
|
486
|
+
if (Array.isArray(result.content)) {
|
|
487
|
+
const parts = [];
|
|
488
|
+
for (const item of result.content) {
|
|
489
|
+
if (item && typeof item === 'object' && 'text' in item) {
|
|
490
|
+
parts.push(String(item.text));
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
parts.push(String(item));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return parts.join('\n');
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
return String(result.content);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Direct result or unknown format
|
|
504
|
+
return String(result);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* List available prompts from the MCP server
|
|
508
|
+
*/
|
|
509
|
+
async listPrompts() {
|
|
510
|
+
if (!this._connected) {
|
|
511
|
+
await this.connect();
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const result = await this.client.request(ListPromptsRequestSchema, {});
|
|
515
|
+
this._prompts = new Map(result.prompts.map((prompt) => [prompt.name, prompt]));
|
|
516
|
+
return Array.from(this._prompts.values());
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
logger.debug(`Server '${this.serverName}' does not support prompts`);
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Get a prompt with arguments
|
|
525
|
+
*/
|
|
526
|
+
async getPrompt(name, args) {
|
|
527
|
+
if (!this._connected) {
|
|
528
|
+
await this.connect();
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
const result = await this.client.request(GetPromptRequestSchema, {
|
|
532
|
+
name,
|
|
533
|
+
arguments: args,
|
|
534
|
+
});
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
logger.error(`Failed to get prompt '${name}': ${error}`);
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Start health check monitoring
|
|
544
|
+
*/
|
|
545
|
+
_startHealthCheck() {
|
|
546
|
+
if (this.healthCheckIntervalSeconds <= 0 || this._healthCheckInterval) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
this._healthCheckInterval = setInterval(async () => {
|
|
550
|
+
try {
|
|
551
|
+
await this._performHealthCheck();
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
logger.warning(`Health check failed for '${this.serverName}': ${error}`);
|
|
555
|
+
if (this.autoReconnect) {
|
|
556
|
+
await this._attemptReconnect();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}, this.healthCheckIntervalSeconds * 1000);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Stop health check monitoring
|
|
563
|
+
*/
|
|
564
|
+
_stopHealthCheck() {
|
|
565
|
+
if (this._healthCheckInterval) {
|
|
566
|
+
clearInterval(this._healthCheckInterval);
|
|
567
|
+
this._healthCheckInterval = undefined;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Perform health check by listing tools
|
|
572
|
+
*/
|
|
573
|
+
async _performHealthCheck() {
|
|
574
|
+
if (!this._connected) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
await this.client.request(ListToolsRequestSchema, {});
|
|
579
|
+
this._lastHealthCheck = Date.now() / 1000;
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
this._connected = false;
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Attempt to reconnect to the server
|
|
588
|
+
*/
|
|
589
|
+
async _attemptReconnect() {
|
|
590
|
+
logger.info(`Attempting to reconnect to '${this.serverName}'...`);
|
|
591
|
+
this._connected = false;
|
|
592
|
+
try {
|
|
593
|
+
await this.connect(this.connectionTimeout);
|
|
594
|
+
logger.info(`✅ Reconnected to '${this.serverName}'`);
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
logger.error(`Failed to reconnect to '${this.serverName}': ${error}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Get client statistics
|
|
602
|
+
*/
|
|
603
|
+
getStats() {
|
|
604
|
+
const uptime = this._lastConnectTime
|
|
605
|
+
? Date.now() / 1000 - this._lastConnectTime
|
|
606
|
+
: undefined;
|
|
607
|
+
const successRate = this._toolCallCount > 0 ? 1 - this._errorCount / this._toolCallCount : 1;
|
|
608
|
+
return {
|
|
609
|
+
serverName: this.serverName,
|
|
610
|
+
connected: this._connected,
|
|
611
|
+
toolsDiscovered: this._tools.size,
|
|
612
|
+
promptsDiscovered: this._prompts.size,
|
|
613
|
+
toolCallCount: this._toolCallCount,
|
|
614
|
+
errorCount: this._errorCount,
|
|
615
|
+
successRate,
|
|
616
|
+
uptime,
|
|
617
|
+
lastHealthCheck: this._lastHealthCheck,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Check if client is connected
|
|
622
|
+
*/
|
|
623
|
+
isConnected() {
|
|
624
|
+
return this._connected;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Reset statistics
|
|
628
|
+
*/
|
|
629
|
+
resetStats() {
|
|
630
|
+
this._toolCallCount = 0;
|
|
631
|
+
this._errorCount = 0;
|
|
632
|
+
logger.debug(`Reset statistics for '${this.serverName}'`);
|
|
633
|
+
}
|
|
634
|
+
// Async context manager support
|
|
635
|
+
async [Symbol.asyncDispose]() {
|
|
636
|
+
await this.disconnect();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { MCPClient } from './client.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
// This controller integrates MCP tools into the browser-use registry
|
|
4
|
+
export class MCPController {
|
|
5
|
+
clients = [];
|
|
6
|
+
constructor() { }
|
|
7
|
+
async addServer(command, args) {
|
|
8
|
+
const client = new MCPClient('browser-use-client', command, args);
|
|
9
|
+
await client.connect();
|
|
10
|
+
this.clients.push(client);
|
|
11
|
+
await this.registerTools(client);
|
|
12
|
+
}
|
|
13
|
+
async registerTools(client) {
|
|
14
|
+
const tools = await client.listTools();
|
|
15
|
+
for (const tool of tools) {
|
|
16
|
+
// We need to convert JSON schema to Zod schema dynamically if we want to validate
|
|
17
|
+
// For now, we might just use a generic schema or skip validation at the registry level
|
|
18
|
+
// and let the MCP server validate.
|
|
19
|
+
// However, the Registry expects a Zod schema.
|
|
20
|
+
// We can create a dynamic Zod schema that accepts anything if we can't easily convert.
|
|
21
|
+
// Or we can try to convert using json-schema-to-zod (if we had it) or just use z.any()
|
|
22
|
+
const paramModel = z.any().describe(tool.description || '');
|
|
23
|
+
// Register the action
|
|
24
|
+
// Note: We need to access the singleton registry or pass it in.
|
|
25
|
+
// Assuming we can use the decorator or manual registration.
|
|
26
|
+
// Since we are registering dynamically, we might need a method on Registry to register actions at runtime.
|
|
27
|
+
// Let's assume Registry has a static method or we can access the instance.
|
|
28
|
+
// In the migration plan, Registry is a class.
|
|
29
|
+
// We might need to modify Registry to support runtime registration if it doesn't already.
|
|
30
|
+
// But wait, the Registry in `src/controller/registry/service.ts` has an `action` method which is a decorator.
|
|
31
|
+
// It also has a `registry` property which holds the actions.
|
|
32
|
+
// We'll assume we can access the global registry or pass it to this controller.
|
|
33
|
+
// For now, I'll just log it as a placeholder for actual registration logic which might require
|
|
34
|
+
// more complex integration with the Controller class.
|
|
35
|
+
console.log(`Discovered MCP tool: ${tool.name}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|