@vibebrowser/mcp 0.2.6 → 0.2.7

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.
@@ -0,0 +1,172 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { readFileSync } from 'node:fs';
5
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
6
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
7
+ const TOOLS_REFRESH_TIMEOUT_MS = 6_000;
8
+ const TOOL_CALL_TIMEOUT_MS = 30_000;
9
+ const DEVTOOLS_UNAVAILABLE_PREFIX = 'chrome-devtools backend unavailable';
10
+ const DEVTOOLS_NOT_INSTALLED_MESSAGE = `${DEVTOOLS_UNAVAILABLE_PREFIX}: chrome-devtools-mcp is not installed`;
11
+ function normalizeToolName(value) {
12
+ return value.replace(/[-\s]/g, '_').toLowerCase();
13
+ }
14
+ function toToolDefinition(input) {
15
+ return {
16
+ name: input.name,
17
+ description: input.description ?? '',
18
+ inputSchema: {
19
+ type: 'object',
20
+ properties: input.inputSchema.properties,
21
+ required: input.inputSchema.required,
22
+ },
23
+ };
24
+ }
25
+ export class DevtoolsFallbackConnection extends EventEmitter {
26
+ debug;
27
+ client = null;
28
+ transport = null;
29
+ tools = [];
30
+ available = false;
31
+ unavailableReason;
32
+ constructor(debug) {
33
+ super();
34
+ this.debug = debug;
35
+ }
36
+ async start() {
37
+ const binaryPath = this.resolveBinaryPath();
38
+ if (!binaryPath) {
39
+ this.unavailableReason = DEVTOOLS_NOT_INSTALLED_MESSAGE;
40
+ this.log(this.unavailableReason);
41
+ return;
42
+ }
43
+ const transport = new StdioClientTransport({
44
+ command: process.execPath,
45
+ args: [binaryPath, '--autoConnect'],
46
+ stderr: this.debug ? 'inherit' : 'pipe',
47
+ });
48
+ const client = new Client({
49
+ name: 'vibebrowser-mcp-devtools-fallback',
50
+ version: '1.0.0',
51
+ }, { capabilities: {} });
52
+ try {
53
+ await client.connect(transport, { timeout: TOOLS_REFRESH_TIMEOUT_MS });
54
+ this.client = client;
55
+ this.transport = transport;
56
+ this.available = true;
57
+ this.unavailableReason = undefined;
58
+ await this.refreshTools(TOOLS_REFRESH_TIMEOUT_MS);
59
+ this.emit('connected');
60
+ }
61
+ catch (error) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ this.unavailableReason = `${DEVTOOLS_UNAVAILABLE_PREFIX}: ${message}`;
64
+ this.log(this.unavailableReason);
65
+ this.available = false;
66
+ this.client = null;
67
+ this.transport = null;
68
+ this.tools = [];
69
+ this.emit('unavailable', this.unavailableReason);
70
+ try {
71
+ await transport.close();
72
+ }
73
+ catch {
74
+ // ignore cleanup errors
75
+ }
76
+ }
77
+ }
78
+ async stop() {
79
+ this.available = false;
80
+ this.tools = [];
81
+ if (this.client) {
82
+ try {
83
+ await this.client.close();
84
+ }
85
+ catch {
86
+ // ignore shutdown errors
87
+ }
88
+ this.client = null;
89
+ }
90
+ if (this.transport) {
91
+ try {
92
+ await this.transport.close();
93
+ }
94
+ catch {
95
+ // ignore shutdown errors
96
+ }
97
+ this.transport = null;
98
+ }
99
+ }
100
+ async refreshTools(timeoutMs = TOOLS_REFRESH_TIMEOUT_MS) {
101
+ if (!this.client || !this.available) {
102
+ return this.tools;
103
+ }
104
+ try {
105
+ const listed = await this.client.listTools(undefined, { timeout: timeoutMs });
106
+ const nextTools = listed.tools.map(toToolDefinition);
107
+ const previousNames = this.tools.map((tool) => normalizeToolName(tool.name)).join(',');
108
+ const nextNames = nextTools.map((tool) => normalizeToolName(tool.name)).join(',');
109
+ this.tools = nextTools;
110
+ if (previousNames !== nextNames) {
111
+ this.emit('tools_updated', this.tools);
112
+ }
113
+ return this.tools;
114
+ }
115
+ catch (error) {
116
+ const message = error instanceof Error ? error.message : String(error);
117
+ this.log(`Unable to refresh chrome-devtools tools: ${message}`);
118
+ return this.tools;
119
+ }
120
+ }
121
+ getTools() {
122
+ return this.tools;
123
+ }
124
+ hasTool(name) {
125
+ const needle = normalizeToolName(name);
126
+ return this.tools.some((tool) => normalizeToolName(tool.name) === needle);
127
+ }
128
+ async callTool(name, args, timeoutMs = TOOL_CALL_TIMEOUT_MS) {
129
+ if (!this.client || !this.available) {
130
+ throw new Error(this.unavailableReason || DEVTOOLS_UNAVAILABLE_PREFIX);
131
+ }
132
+ const result = await this.client.callTool({ name, arguments: args }, undefined, { timeout: timeoutMs });
133
+ const content = Array.isArray(result.content)
134
+ ? result.content
135
+ : [{ type: 'text', text: JSON.stringify(result) }];
136
+ const isError = typeof result.isError === 'boolean' ? result.isError : false;
137
+ return {
138
+ success: !isError,
139
+ isError,
140
+ content,
141
+ };
142
+ }
143
+ isAvailable() {
144
+ return this.available;
145
+ }
146
+ getUnavailableReason() {
147
+ return this.unavailableReason;
148
+ }
149
+ resolveBinaryPath() {
150
+ try {
151
+ const require = createRequire(import.meta.url);
152
+ const packageJsonPath = require.resolve('chrome-devtools-mcp/package.json');
153
+ const metadata = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
154
+ const bin = typeof metadata.bin === 'string'
155
+ ? metadata.bin
156
+ : metadata.bin?.['chrome-devtools-mcp'] ?? metadata.bin?.['chrome-devtools'];
157
+ if (!bin) {
158
+ return undefined;
159
+ }
160
+ return resolve(dirname(packageJsonPath), bin);
161
+ }
162
+ catch {
163
+ return undefined;
164
+ }
165
+ }
166
+ log(message) {
167
+ if (this.debug) {
168
+ console.error(`[vibebrowser-mcp] ${message}`);
169
+ }
170
+ }
171
+ }
172
+ //# sourceMappingURL=devtools-fallback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools-fallback.js","sourceRoot":"","sources":["../src/devtools-fallback.ts"],"names":[],"mappings":"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"}
package/dist/relay.d.ts CHANGED
@@ -13,6 +13,11 @@ export declare const EXTENSION_PORT: number;
13
13
  export declare const AGENT_PORT: number;
14
14
  /**
15
15
  * Vibe MCP Relay Server
16
+ *
17
+ * Owns exactly one shared chrome-devtools fallback backend process per relay
18
+ * daemon instance. All connected MCP clients (vibebrowser-mcp and
19
+ * vibebrowser-cli) route through this relay, so fallback lifecycle and tool
20
+ * routing stay deterministic across concurrent agents.
16
21
  */
17
22
  export declare class RelayServer extends EventEmitter {
18
23
  private extensionWss;
@@ -24,6 +29,7 @@ export declare class RelayServer extends EventEmitter {
24
29
  private requestIdCounter;
25
30
  private anonymousSessionCounter;
26
31
  private debug;
32
+ private readonly devtoolsFallback;
27
33
  constructor(debug?: boolean);
28
34
  /**
29
35
  * Start the relay server
@@ -87,6 +93,12 @@ export declare class RelayServer extends EventEmitter {
87
93
  private buildSessionsList;
88
94
  private getDefaultSession;
89
95
  private resolveTargetSession;
96
+ private parseToolDefinitions;
97
+ private getToolsForSession;
98
+ private findExtensionTool;
99
+ private findFallbackTool;
100
+ private broadcastDefaultToolsToAgents;
101
+ private hasConnectedExtensionSession;
90
102
  private sendSessionStateToAgent;
91
103
  private broadcastSessionState;
92
104
  private rejectPendingRequestsForSession;
@@ -1 +1 @@
1
- {"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAgBtC,eAAO,MAAM,cAAc,QAAiD,CAAC;AAC7E,eAAO,MAAM,UAAU,QAA6C,CAAC;AAkErE;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;IAC3C,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,iBAAiB,CAA4C;IACrE,OAAO,CAAC,iBAAiB,CAAqC;IAC9D,OAAO,CAAC,MAAM,CAA2C;IACzD,OAAO,CAAC,eAAe,CAA0C;IACjE,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,uBAAuB,CAAK;IACpC,OAAO,CAAC,KAAK,CAAU;gBAEX,KAAK,GAAE,OAAe;IAKlC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAqB5B;;OAEG;YACW,oBAAoB;IAoBlC;;OAEG;YACW,gBAAgB;IAoB9B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IA0CjC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA6C7B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAwD9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA2D1B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAUjC;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB;IAmB7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,iBAAiB;IAOzB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAYzB;;OAEG;YACW,QAAQ;IAgDtB;;OAEG;IACH,OAAO,CAAC,GAAG;IAiBX,OAAO,CAAC,sBAAsB;IA2C9B,OAAO,CAAC,yBAAyB;IAajC,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,uBAAuB;IAkB/B,OAAO,CAAC,qBAAqB;IAkB7B,OAAO,CAAC,+BAA+B;CAiBxC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAmBxC;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,MAAM,GAAG,IAAI,CAY3C;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,GAAE,OAAe,GAAG,IAAI,CAgB7D;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAM5E"}
1
+ {"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAsBtC,eAAO,MAAM,cAAc,QAAiD,CAAC;AAC7E,eAAO,MAAM,UAAU,QAA6C,CAAC;AAkErE;;;;;;;GAOG;AACH,qBAAa,WAAY,SAAQ,YAAY;IAC3C,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,iBAAiB,CAA4C;IACrE,OAAO,CAAC,iBAAiB,CAAqC;IAC9D,OAAO,CAAC,MAAM,CAA2C;IACzD,OAAO,CAAC,eAAe,CAA0C;IACjE,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,uBAAuB,CAAK;IACpC,OAAO,CAAC,KAAK,CAAU;IACvB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA6B;gBAElD,KAAK,GAAE,OAAe;IAMlC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmC5B;;OAEG;YACW,oBAAoB;IAoBlC;;OAEG;YACW,gBAAgB;IAoB9B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IA2CjC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAiD7B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAiE9B;;OAEG;YACW,kBAAkB;IA+KhC;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAUjC;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB;IAmB7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,iBAAiB;IAOzB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAYzB;;OAEG;YACW,QAAQ;IAiDtB;;OAEG;IACH,OAAO,CAAC,GAAG;IAiBX,OAAO,CAAC,sBAAsB;IA2C9B,OAAO,CAAC,yBAAyB;IAajC,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,6BAA6B;IAUrC,OAAO,CAAC,4BAA4B;IAIpC,OAAO,CAAC,uBAAuB;IAkB/B,OAAO,CAAC,qBAAqB;IAkB7B,OAAO,CAAC,+BAA+B;CAiBxC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAmBxC;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,MAAM,GAAG,IAAI,CAY3C;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,GAAE,OAAe,GAAG,IAAI,CAgB7D;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAM5E"}
package/dist/relay.js CHANGED
@@ -13,6 +13,7 @@ import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync } from '
13
13
  import { join } from 'path';
14
14
  import { homedir } from 'os';
15
15
  import { EventEmitter } from 'events';
16
+ import { DevtoolsFallbackConnection } from './devtools-fallback.js';
16
17
  function parseEnvPort(name, fallback) {
17
18
  const raw = process.env[name];
18
19
  if (!raw) {
@@ -24,6 +25,9 @@ function parseEnvPort(name, fallback) {
24
25
  }
25
26
  return port;
26
27
  }
28
+ function normalizeToolName(value) {
29
+ return value.replace(/[-\s]/g, '_').toLowerCase();
30
+ }
27
31
  // Ports (19888/19889 to avoid conflict with Playwriter MCP which uses 19988/19989)
28
32
  export const EXTENSION_PORT = parseEnvPort('VIBE_MCP_EXTENSION_PORT', 19889);
29
33
  export const AGENT_PORT = parseEnvPort('VIBE_MCP_AGENT_PORT', 19888);
@@ -33,6 +37,11 @@ const PID_FILE = join(VIBE_DIR, 'relay.pid');
33
37
  const LOG_FILE = join(VIBE_DIR, 'relay.log');
34
38
  /**
35
39
  * Vibe MCP Relay Server
40
+ *
41
+ * Owns exactly one shared chrome-devtools fallback backend process per relay
42
+ * daemon instance. All connected MCP clients (vibebrowser-mcp and
43
+ * vibebrowser-cli) route through this relay, so fallback lifecycle and tool
44
+ * routing stay deterministic across concurrent agents.
36
45
  */
37
46
  export class RelayServer extends EventEmitter {
38
47
  extensionWss = null;
@@ -44,9 +53,11 @@ export class RelayServer extends EventEmitter {
44
53
  requestIdCounter = 0;
45
54
  anonymousSessionCounter = 0;
46
55
  debug;
56
+ devtoolsFallback;
47
57
  constructor(debug = false) {
48
58
  super();
49
59
  this.debug = debug;
60
+ this.devtoolsFallback = new DevtoolsFallbackConnection(debug);
50
61
  }
51
62
  /**
52
63
  * Start the relay server
@@ -60,6 +71,19 @@ export class RelayServer extends EventEmitter {
60
71
  await this.startExtensionServer();
61
72
  // Start agent WebSocket server
62
73
  await this.startAgentServer();
74
+ await this.devtoolsFallback.start();
75
+ this.devtoolsFallback.on('tools_updated', () => {
76
+ this.broadcastDefaultToolsToAgents();
77
+ this.broadcastSessionState();
78
+ });
79
+ this.devtoolsFallback.on('connected', () => {
80
+ this.broadcastDefaultToolsToAgents();
81
+ this.broadcastSessionState();
82
+ });
83
+ this.devtoolsFallback.on('unavailable', () => {
84
+ this.broadcastDefaultToolsToAgents();
85
+ this.broadcastSessionState();
86
+ });
63
87
  // Write PID file
64
88
  writeFileSync(PID_FILE, String(process.pid));
65
89
  this.log(`Relay started (PID: ${process.pid})`);
@@ -138,6 +162,7 @@ export class RelayServer extends EventEmitter {
138
162
  this.broadcastSessionState();
139
163
  if (this.extensionSessions.size === 0) {
140
164
  this.broadcastToAgents({ type: 'extension_disconnected' });
165
+ this.broadcastDefaultToolsToAgents();
141
166
  }
142
167
  });
143
168
  ws.on('error', (error) => {
@@ -158,13 +183,17 @@ export class RelayServer extends EventEmitter {
158
183
  this.log(`Agent connected: ${agentId} (total: ${this.agents.size})`);
159
184
  this.sendSessionStateToAgent(ws);
160
185
  const defaultSession = this.getDefaultSession();
161
- if (defaultSession && defaultSession.tools.length > 0) {
162
- ws.send(JSON.stringify({ type: 'tools_list', data: defaultSession.tools, sessionId: defaultSession.sessionId }));
186
+ const tools = this.getToolsForSession(defaultSession);
187
+ if (tools.length > 0) {
188
+ ws.send(JSON.stringify({ type: 'tools_list', data: tools, sessionId: defaultSession?.sessionId }));
163
189
  }
164
190
  ws.on('message', (data) => {
165
191
  try {
166
192
  const message = JSON.parse(data.toString());
167
- this.handleAgentMessage(agentId, message);
193
+ this.handleAgentMessage(agentId, message).catch((error) => {
194
+ const reason = error instanceof Error ? error.message : String(error);
195
+ this.log(`Unhandled agent message failure (${agentId}): ${reason}`);
196
+ });
168
197
  }
169
198
  catch (error) {
170
199
  this.log(`Failed to parse agent message: ${error}`);
@@ -207,18 +236,28 @@ export class RelayServer extends EventEmitter {
207
236
  // Forward response to the requesting agent
208
237
  const agent = this.agents.get(pending.agentId);
209
238
  if (agent) {
210
- agent.ws.send(JSON.stringify({
211
- ...message,
212
- sessionId: session.sessionId,
213
- requestId: pending.originalRequestId,
214
- }));
239
+ if (message.type === 'tools_list') {
240
+ agent.ws.send(JSON.stringify({
241
+ type: 'tools_list',
242
+ requestId: pending.originalRequestId,
243
+ data: this.getToolsForSession(this.extensionSessions.get(pending.sessionId) || null),
244
+ sessionId: pending.sessionId,
245
+ }));
246
+ }
247
+ else {
248
+ agent.ws.send(JSON.stringify({
249
+ ...message,
250
+ sessionId: session.sessionId,
251
+ requestId: pending.originalRequestId,
252
+ }));
253
+ }
215
254
  }
216
255
  // For tools_list we still want to cache + broadcast to *other* agents
217
256
  // so they stay in sync, but the requesting agent already got its copy.
218
257
  if (message.type === 'tools_list') {
219
- session.tools = message.data;
258
+ session.tools = this.parseToolDefinitions(message.data);
220
259
  this.stopToolsSyncLoop(session);
221
- this.broadcastToAgents({ type: 'tools_list', data: session.tools, sessionId: session.sessionId }, pending.agentId);
260
+ this.broadcastDefaultToolsToAgents(pending.agentId);
222
261
  this.broadcastSessionState();
223
262
  }
224
263
  return;
@@ -226,9 +265,9 @@ export class RelayServer extends EventEmitter {
226
265
  }
227
266
  // Handle unsolicited tools list (e.g. extension announces on connect)
228
267
  if (message.type === 'tools_list') {
229
- session.tools = message.data;
268
+ session.tools = this.parseToolDefinitions(message.data);
230
269
  this.stopToolsSyncLoop(session);
231
- this.broadcastToAgents({ type: 'tools_list', data: session.tools, sessionId: session.sessionId });
270
+ this.broadcastDefaultToolsToAgents();
232
271
  this.broadcastSessionState();
233
272
  return;
234
273
  }
@@ -238,25 +277,118 @@ export class RelayServer extends EventEmitter {
238
277
  /**
239
278
  * Handle message from an agent
240
279
  */
241
- handleAgentMessage(agentId, message) {
242
- this.log(`Agent ${agentId} message: ${message.type}`);
243
- if (message.type === 'list_sessions') {
280
+ async handleAgentMessage(agentId, message) {
281
+ try {
282
+ this.log(`Agent ${agentId} message: ${message.type}`);
283
+ if (message.type === 'list_sessions') {
284
+ const agent = this.agents.get(agentId);
285
+ if (agent && message.requestId) {
286
+ agent.ws.send(JSON.stringify({
287
+ type: 'sessions_list',
288
+ requestId: message.requestId,
289
+ sessions: this.buildSessionsList(),
290
+ }));
291
+ }
292
+ return;
293
+ }
294
+ const requestedSessionId = typeof message.data?.sessionId === 'string' ? message.data.sessionId : undefined;
295
+ const targetSession = this.resolveTargetSession(requestedSessionId);
244
296
  const agent = this.agents.get(agentId);
245
- if (agent && message.requestId) {
297
+ if (!agent || !message.requestId) {
298
+ return;
299
+ }
300
+ if (message.type === 'list_tools') {
301
+ if (requestedSessionId && !targetSession) {
302
+ agent.ws.send(JSON.stringify({
303
+ type: 'error',
304
+ requestId: message.requestId,
305
+ error: `No browser session connected for sessionId=${requestedSessionId}`,
306
+ }));
307
+ return;
308
+ }
246
309
  agent.ws.send(JSON.stringify({
247
- type: 'sessions_list',
310
+ type: 'tools_list',
248
311
  requestId: message.requestId,
249
- sessions: this.buildSessionsList(),
312
+ data: this.getToolsForSession(targetSession),
313
+ sessionId: targetSession?.sessionId,
250
314
  }));
315
+ return;
251
316
  }
252
- return;
253
- }
254
- const requestedSessionId = typeof message.data?.sessionId === 'string' ? message.data.sessionId : undefined;
255
- const targetSession = this.resolveTargetSession(requestedSessionId);
256
- if (!targetSession) {
257
- // No extension connected, send error back
258
- const agent = this.agents.get(agentId);
259
- if (agent && message.requestId) {
317
+ if (message.type === 'call_tool') {
318
+ const requestedToolName = message.data?.name;
319
+ if (typeof requestedToolName !== 'string' || requestedToolName.trim().length === 0) {
320
+ agent.ws.send(JSON.stringify({
321
+ type: 'error',
322
+ requestId: message.requestId,
323
+ error: 'Tool name is required',
324
+ }));
325
+ return;
326
+ }
327
+ if (requestedSessionId && !targetSession) {
328
+ agent.ws.send(JSON.stringify({
329
+ type: 'error',
330
+ requestId: message.requestId,
331
+ error: `No browser session connected for sessionId=${requestedSessionId}`,
332
+ }));
333
+ return;
334
+ }
335
+ const extensionTool = this.findExtensionTool(targetSession, requestedToolName);
336
+ if (extensionTool && targetSession) {
337
+ const relayRequestId = `relay_${++this.requestIdCounter}`;
338
+ const cleanData = message.data ? { ...message.data } : undefined;
339
+ if (cleanData && 'sessionId' in cleanData) {
340
+ delete cleanData.sessionId;
341
+ }
342
+ const forwardMessage = {
343
+ ...message,
344
+ requestId: relayRequestId,
345
+ ...(cleanData ? { data: cleanData } : {}),
346
+ };
347
+ this.pendingRequests.set(relayRequestId, {
348
+ agentId,
349
+ originalRequestId: message.requestId,
350
+ lastSentAt: Date.now(),
351
+ forwardMessage,
352
+ sessionId: targetSession.sessionId,
353
+ });
354
+ targetSession.ws.send(JSON.stringify(forwardMessage));
355
+ return;
356
+ }
357
+ if (!this.hasConnectedExtensionSession() && this.findFallbackTool(requestedToolName)) {
358
+ try {
359
+ const args = message.data?.arguments && typeof message.data.arguments === 'object'
360
+ ? message.data.arguments
361
+ : {};
362
+ const result = await this.devtoolsFallback.callTool(requestedToolName, args);
363
+ agent.ws.send(JSON.stringify({
364
+ type: 'tool_result',
365
+ requestId: message.requestId,
366
+ data: result,
367
+ sessionId: targetSession?.sessionId,
368
+ }));
369
+ }
370
+ catch (error) {
371
+ const reason = error instanceof Error ? error.message : String(error);
372
+ agent.ws.send(JSON.stringify({
373
+ type: 'error',
374
+ requestId: message.requestId,
375
+ error: reason,
376
+ sessionId: targetSession?.sessionId,
377
+ }));
378
+ }
379
+ return;
380
+ }
381
+ agent.ws.send(JSON.stringify({
382
+ type: 'error',
383
+ requestId: message.requestId,
384
+ error: targetSession
385
+ ? `Tool not found: ${requestedToolName}`
386
+ : 'No extension connected',
387
+ }));
388
+ return;
389
+ }
390
+ if (!targetSession) {
391
+ // No extension connected, send error back
260
392
  agent.ws.send(JSON.stringify({
261
393
  type: 'error',
262
394
  requestId: message.requestId,
@@ -264,31 +396,43 @@ export class RelayServer extends EventEmitter {
264
396
  ? `No browser session connected for sessionId=${requestedSessionId}`
265
397
  : 'No extension connected',
266
398
  }));
399
+ return;
267
400
  }
268
- return;
401
+ // Generate relay request ID
402
+ const relayRequestId = `relay_${++this.requestIdCounter}`;
403
+ const cleanData = message.data ? { ...message.data } : undefined;
404
+ if (cleanData && 'sessionId' in cleanData) {
405
+ delete cleanData.sessionId;
406
+ }
407
+ const forwardMessage = {
408
+ ...message,
409
+ requestId: relayRequestId,
410
+ ...(cleanData ? { data: cleanData } : {}),
411
+ };
412
+ // Store pending request mapping so it can be replayed if the extension
413
+ // swaps sockets mid-flight.
414
+ this.pendingRequests.set(relayRequestId, {
415
+ agentId,
416
+ originalRequestId: message.requestId,
417
+ lastSentAt: Date.now(),
418
+ forwardMessage,
419
+ sessionId: targetSession.sessionId,
420
+ });
421
+ // Forward to extension with relay request ID
422
+ targetSession.ws.send(JSON.stringify(forwardMessage));
269
423
  }
270
- // Generate relay request ID
271
- const relayRequestId = `relay_${++this.requestIdCounter}`;
272
- const cleanData = message.data ? { ...message.data } : undefined;
273
- if (cleanData && 'sessionId' in cleanData) {
274
- delete cleanData.sessionId;
424
+ catch (error) {
425
+ const reason = error instanceof Error ? error.message : String(error);
426
+ this.log(`Failed to handle agent message (${agentId}, ${message.type}): ${reason}`);
427
+ const agent = this.agents.get(agentId);
428
+ if (agent && message.requestId && agent.ws.readyState === WebSocket.OPEN) {
429
+ agent.ws.send(JSON.stringify({
430
+ type: 'error',
431
+ requestId: message.requestId,
432
+ error: `Relay error: ${reason}`,
433
+ }));
434
+ }
275
435
  }
276
- const forwardMessage = {
277
- ...message,
278
- requestId: relayRequestId,
279
- ...(cleanData ? { data: cleanData } : {}),
280
- };
281
- // Store pending request mapping so it can be replayed if the extension
282
- // swaps sockets mid-flight.
283
- this.pendingRequests.set(relayRequestId, {
284
- agentId,
285
- originalRequestId: message.requestId,
286
- lastSentAt: Date.now(),
287
- forwardMessage,
288
- sessionId: targetSession.sessionId,
289
- });
290
- // Forward to extension with relay request ID
291
- targetSession.ws.send(JSON.stringify(forwardMessage));
292
436
  }
293
437
  /**
294
438
  * Request tools list from extension
@@ -400,6 +544,7 @@ export class RelayServer extends EventEmitter {
400
544
  }
401
545
  this.extensionSessions.clear();
402
546
  this.socketToSessionId.clear();
547
+ await this.devtoolsFallback.stop();
403
548
  // Close servers
404
549
  if (this.agentWss) {
405
550
  this.agentWss.close();
@@ -507,6 +652,46 @@ export class RelayServer extends EventEmitter {
507
652
  }
508
653
  return this.getDefaultSession();
509
654
  }
655
+ parseToolDefinitions(data) {
656
+ if (!Array.isArray(data)) {
657
+ return [];
658
+ }
659
+ return data
660
+ .filter((entry) => Boolean(entry) && typeof entry === 'object' && typeof entry.name === 'string')
661
+ .map((entry) => entry);
662
+ }
663
+ getToolsForSession(session) {
664
+ if (session) {
665
+ return [...session.tools];
666
+ }
667
+ if (this.hasConnectedExtensionSession()) {
668
+ return [];
669
+ }
670
+ return [...this.devtoolsFallback.getTools()];
671
+ }
672
+ findExtensionTool(session, toolName) {
673
+ if (!session) {
674
+ return undefined;
675
+ }
676
+ const key = normalizeToolName(toolName);
677
+ return session.tools.find((tool) => normalizeToolName(tool.name) === key);
678
+ }
679
+ findFallbackTool(toolName) {
680
+ const key = normalizeToolName(toolName);
681
+ return this.devtoolsFallback.getTools().find((tool) => normalizeToolName(tool.name) === key);
682
+ }
683
+ broadcastDefaultToolsToAgents(excludeAgentId) {
684
+ const defaultSession = this.getDefaultSession();
685
+ const tools = this.getToolsForSession(defaultSession);
686
+ this.broadcastToAgents({
687
+ type: 'tools_list',
688
+ data: tools,
689
+ sessionId: defaultSession?.sessionId,
690
+ }, excludeAgentId);
691
+ }
692
+ hasConnectedExtensionSession() {
693
+ return this.getDefaultSession() !== null;
694
+ }
510
695
  sendSessionStateToAgent(ws) {
511
696
  const sessions = this.buildSessionsList();
512
697
  const defaultSession = this.getDefaultSession();