@mcp-b/chrome-devtools-mcp 1.2.0 → 1.3.1
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/README.md +71 -38
- package/build/src/McpContext.js +195 -7
- package/build/src/PageCollector.js +18 -8
- package/build/src/browser.js +123 -5
- package/build/src/cli.js +20 -1
- package/build/src/main.js +88 -8
- package/build/src/prompts/index.js +5 -5
- package/build/src/third_party/index.js +1 -1
- package/build/src/tools/WebMCPToolHub.js +372 -0
- package/build/src/tools/pages.js +8 -2
- package/build/src/tools/webmcp.js +54 -122
- package/build/src/transports/WebMCPClientTransport.js +161 -74
- package/package.json +2 -1
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { jsonSchemaToZod } from '@composio/json-schema-to-zod';
|
|
7
|
+
import { zod, } from '../third_party/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Convert a JSON Schema inputSchema from WebMCP to a Zod schema.
|
|
10
|
+
*
|
|
11
|
+
* Falls back to a permissive passthrough schema if conversion fails,
|
|
12
|
+
* allowing the tool to still be registered and used.
|
|
13
|
+
*
|
|
14
|
+
* @param inputSchema - The JSON Schema from the WebMCP tool definition.
|
|
15
|
+
* @param logger - Optional logger for conversion errors.
|
|
16
|
+
* @returns A Zod schema for parameter validation.
|
|
17
|
+
*/
|
|
18
|
+
function convertInputSchema(inputSchema, logger) {
|
|
19
|
+
if (!inputSchema) {
|
|
20
|
+
return zod.object({}).passthrough();
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return jsonSchemaToZod(inputSchema);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
logger?.('Failed to convert inputSchema to Zod:', err);
|
|
27
|
+
return zod.object({}).passthrough();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* WebMCPToolHub manages dynamic registration of WebMCP tools as first-class MCP tools.
|
|
32
|
+
*
|
|
33
|
+
* When a page has WebMCP tools available, this hub:
|
|
34
|
+
* 1. Syncs those tools to the MCP server as native tools
|
|
35
|
+
* 2. Uses naming convention: webmcp_{domain}_page{idx}_{toolName}
|
|
36
|
+
* 3. Updates tools when WebMCP sends list_changed notifications
|
|
37
|
+
* 4. Removes tools when pages navigate or close
|
|
38
|
+
*
|
|
39
|
+
* This allows Claude Code to call WebMCP tools directly without the two-step
|
|
40
|
+
* diff_webmcp_tools -> call_webmcp_tool process.
|
|
41
|
+
*/
|
|
42
|
+
export class WebMCPToolHub {
|
|
43
|
+
#server;
|
|
44
|
+
#context;
|
|
45
|
+
#logger;
|
|
46
|
+
/** Map of tool IDs to their registration metadata. */
|
|
47
|
+
#registeredTools = new Map();
|
|
48
|
+
/** Tracks which tool IDs belong to each page (for cleanup on navigation). */
|
|
49
|
+
#pageTools = new WeakMap();
|
|
50
|
+
/** Guards against concurrent sync operations per page. */
|
|
51
|
+
#syncInProgress = new WeakSet();
|
|
52
|
+
/** Whether automatic tool registration is enabled. */
|
|
53
|
+
#enabled = true;
|
|
54
|
+
/** Global diff state for diff_webmcp_tools - tracks last seen tool IDs. */
|
|
55
|
+
#lastSeenToolIds = null;
|
|
56
|
+
constructor(server, context, enabled = true) {
|
|
57
|
+
this.#server = server;
|
|
58
|
+
this.#context = context;
|
|
59
|
+
this.#logger = context.logger;
|
|
60
|
+
this.#enabled = enabled;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Disable automatic tool registration
|
|
64
|
+
*/
|
|
65
|
+
disable() {
|
|
66
|
+
this.#enabled = false;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Enable automatic tool registration
|
|
70
|
+
*/
|
|
71
|
+
enable() {
|
|
72
|
+
this.#enabled = true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if the hub is enabled
|
|
76
|
+
*/
|
|
77
|
+
isEnabled() {
|
|
78
|
+
return this.#enabled;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Sync tools for a page. Called on:
|
|
82
|
+
* 1. Initial WebMCP connection
|
|
83
|
+
* 2. ToolListChangedNotificationSchema notification
|
|
84
|
+
*
|
|
85
|
+
* @param page - The browser page
|
|
86
|
+
* @param client - The MCP client (passed to avoid infinite loop via getWebMCPClient)
|
|
87
|
+
*/
|
|
88
|
+
async syncToolsForPage(page, client) {
|
|
89
|
+
if (!this.#enabled) {
|
|
90
|
+
return { synced: 0, removed: 0, updated: 0 };
|
|
91
|
+
}
|
|
92
|
+
if (this.#syncInProgress.has(page)) {
|
|
93
|
+
this.#logger('Sync already in progress for page, skipping');
|
|
94
|
+
return { synced: 0, removed: 0, updated: 0 };
|
|
95
|
+
}
|
|
96
|
+
this.#syncInProgress.add(page);
|
|
97
|
+
const urlAtStart = page.url();
|
|
98
|
+
try {
|
|
99
|
+
const { tools } = await client.listTools();
|
|
100
|
+
// Guard: page navigated during async operation
|
|
101
|
+
if (page.url() !== urlAtStart) {
|
|
102
|
+
this.#logger('Page navigated during sync, aborting');
|
|
103
|
+
return { synced: 0, removed: 0, updated: 0 };
|
|
104
|
+
}
|
|
105
|
+
return this.#applyToolChanges(page, tools);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
this.#logger('Failed to sync WebMCP tools:', err);
|
|
109
|
+
return { synced: 0, removed: 0, updated: 0 };
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
this.#syncInProgress.delete(page);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Remove all tools for a page. Called on:
|
|
117
|
+
* 1. Transport close (navigation/page close)
|
|
118
|
+
* 2. Manual removal
|
|
119
|
+
*/
|
|
120
|
+
removeToolsForPage(page) {
|
|
121
|
+
const toolIds = this.#pageTools.get(page);
|
|
122
|
+
if (!toolIds) {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
let removed = 0;
|
|
126
|
+
for (const toolId of toolIds) {
|
|
127
|
+
const registered = this.#registeredTools.get(toolId);
|
|
128
|
+
if (registered) {
|
|
129
|
+
this.#logger(`Removing WebMCP tool: ${toolId}`);
|
|
130
|
+
registered.handle.remove();
|
|
131
|
+
this.#registeredTools.delete(toolId);
|
|
132
|
+
removed++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
this.#pageTools.delete(page);
|
|
136
|
+
this.#logger(`Removed ${removed} WebMCP tools for page`);
|
|
137
|
+
return removed;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Apply tool changes by comparing current tools with new tools from WebMCP.
|
|
141
|
+
* Handles add, update, and remove operations.
|
|
142
|
+
*/
|
|
143
|
+
#applyToolChanges(page, tools) {
|
|
144
|
+
const domain = extractDomain(page.url());
|
|
145
|
+
const pageIdx = this.#context.getPages().indexOf(page);
|
|
146
|
+
// Guard: page not found in pages list (may have been closed)
|
|
147
|
+
if (pageIdx === -1) {
|
|
148
|
+
this.#logger('Page not found in pages list, skipping tool sync');
|
|
149
|
+
return { synced: 0, removed: 0, updated: 0 };
|
|
150
|
+
}
|
|
151
|
+
const newToolIds = new Set();
|
|
152
|
+
let synced = 0;
|
|
153
|
+
let updated = 0;
|
|
154
|
+
for (const tool of tools) {
|
|
155
|
+
const toolId = this.#generateToolId(domain, pageIdx, tool.name);
|
|
156
|
+
newToolIds.add(toolId);
|
|
157
|
+
const existing = this.#registeredTools.get(toolId);
|
|
158
|
+
if (existing) {
|
|
159
|
+
// Remove and re-register to ensure schema updates are applied
|
|
160
|
+
// (MCP SDK's update() doesn't support schema changes)
|
|
161
|
+
this.#logger(`Re-registering WebMCP tool: ${toolId}`);
|
|
162
|
+
existing.handle.remove();
|
|
163
|
+
this.#registeredTools.delete(toolId);
|
|
164
|
+
this.#registerTool(page, domain, pageIdx, tool);
|
|
165
|
+
updated++;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Register new tool
|
|
169
|
+
this.#registerTool(page, domain, pageIdx, tool);
|
|
170
|
+
synced++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Remove tools that no longer exist
|
|
174
|
+
const existingToolIds = this.#pageTools.get(page) || new Set();
|
|
175
|
+
let removed = 0;
|
|
176
|
+
for (const toolId of existingToolIds) {
|
|
177
|
+
if (!newToolIds.has(toolId)) {
|
|
178
|
+
const registered = this.#registeredTools.get(toolId);
|
|
179
|
+
if (registered) {
|
|
180
|
+
this.#logger(`Removing stale WebMCP tool: ${toolId}`);
|
|
181
|
+
registered.handle.remove();
|
|
182
|
+
this.#registeredTools.delete(toolId);
|
|
183
|
+
removed++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
this.#pageTools.set(page, newToolIds);
|
|
188
|
+
this.#logger(`WebMCP tool sync: ${synced} added, ${updated} updated, ${removed} removed`);
|
|
189
|
+
return { synced, removed, updated };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Register a single tool with the MCP server
|
|
193
|
+
*/
|
|
194
|
+
#registerTool(page, domain, pageIdx, tool) {
|
|
195
|
+
const toolId = this.#generateToolId(domain, pageIdx, tool.name);
|
|
196
|
+
const description = this.#generateDescription(domain, pageIdx, tool.description);
|
|
197
|
+
this.#logger(`Registering WebMCP tool: ${toolId}`);
|
|
198
|
+
// Store tool name for use in the callback closure
|
|
199
|
+
const originalToolName = tool.name;
|
|
200
|
+
// Convert the JSON Schema inputSchema to Zod for MCP SDK registration
|
|
201
|
+
const zodSchema = convertInputSchema(tool.inputSchema, this.#logger);
|
|
202
|
+
const handle = this.#server.registerTool(toolId, {
|
|
203
|
+
description,
|
|
204
|
+
inputSchema: zodSchema,
|
|
205
|
+
}, async (params) => {
|
|
206
|
+
// Look up page dynamically to handle potential stale references
|
|
207
|
+
const currentPage = this.#getPageForTool(toolId);
|
|
208
|
+
if (!currentPage) {
|
|
209
|
+
return {
|
|
210
|
+
content: [
|
|
211
|
+
{
|
|
212
|
+
type: 'text',
|
|
213
|
+
text: 'Tool no longer available - page may have closed or navigated',
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
isError: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// Pass params directly - they are already parsed by MCP SDK using the Zod schema
|
|
220
|
+
return this.#executeTool(currentPage, originalToolName, params);
|
|
221
|
+
});
|
|
222
|
+
this.#registeredTools.set(toolId, {
|
|
223
|
+
handle,
|
|
224
|
+
page,
|
|
225
|
+
originalName: tool.name,
|
|
226
|
+
domain,
|
|
227
|
+
toolId,
|
|
228
|
+
description,
|
|
229
|
+
});
|
|
230
|
+
// Track tool for this page
|
|
231
|
+
const pageToolSet = this.#pageTools.get(page) || new Set();
|
|
232
|
+
pageToolSet.add(toolId);
|
|
233
|
+
this.#pageTools.set(page, pageToolSet);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Execute a WebMCP tool on a page
|
|
237
|
+
*/
|
|
238
|
+
async #executeTool(page, toolName, args) {
|
|
239
|
+
try {
|
|
240
|
+
const result = await this.#context.getWebMCPClient(page);
|
|
241
|
+
if (!result.connected) {
|
|
242
|
+
return {
|
|
243
|
+
content: [{ type: 'text', text: 'WebMCP connection lost' }],
|
|
244
|
+
isError: true,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const callResult = await result.client.callTool({
|
|
248
|
+
name: toolName,
|
|
249
|
+
arguments: args,
|
|
250
|
+
});
|
|
251
|
+
// The SDK's callTool returns CallToolResult, but we need to handle
|
|
252
|
+
// the content array properly
|
|
253
|
+
return callResult;
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
return {
|
|
257
|
+
content: [
|
|
258
|
+
{
|
|
259
|
+
type: 'text',
|
|
260
|
+
text: `Tool error: ${err instanceof Error ? err.message : String(err)}`,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
isError: true,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get the page associated with a registered tool
|
|
269
|
+
*/
|
|
270
|
+
#getPageForTool(toolId) {
|
|
271
|
+
const registered = this.#registeredTools.get(toolId);
|
|
272
|
+
return registered?.page;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Generate a unique tool ID following the naming convention
|
|
276
|
+
*/
|
|
277
|
+
#generateToolId(domain, pageIdx, toolName) {
|
|
278
|
+
return `webmcp_${domain}_page${pageIdx}_${sanitizeName(toolName)}`;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Generate a tool description with WebMCP context
|
|
282
|
+
*/
|
|
283
|
+
#generateDescription(domain, pageIdx, originalDescription) {
|
|
284
|
+
const displayDomain = getDisplayDomain(domain);
|
|
285
|
+
return `[WebMCP - ${displayDomain} - Page ${pageIdx}] ${originalDescription || 'No description'}`;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get the total number of registered tools
|
|
289
|
+
*/
|
|
290
|
+
getToolCount() {
|
|
291
|
+
return this.#registeredTools.size;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get all registered tool IDs
|
|
295
|
+
*/
|
|
296
|
+
getRegisteredToolIds() {
|
|
297
|
+
return Array.from(this.#registeredTools.keys());
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Get all registered tools with their metadata (for diff_webmcp_tools)
|
|
301
|
+
*/
|
|
302
|
+
getRegisteredTools() {
|
|
303
|
+
return Array.from(this.#registeredTools.values()).map(rt => ({
|
|
304
|
+
toolId: rt.toolId,
|
|
305
|
+
originalName: rt.originalName,
|
|
306
|
+
domain: rt.domain,
|
|
307
|
+
pageIdx: this.#context.getPages().indexOf(rt.page),
|
|
308
|
+
description: rt.description,
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get the last seen tool IDs for diff tracking
|
|
313
|
+
*/
|
|
314
|
+
getLastSeenToolIds() {
|
|
315
|
+
return this.#lastSeenToolIds;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Set the last seen tool IDs for diff tracking
|
|
319
|
+
*/
|
|
320
|
+
setLastSeenToolIds(toolIds) {
|
|
321
|
+
this.#lastSeenToolIds = toolIds;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Clear the last seen tool IDs (resets diff state)
|
|
325
|
+
*/
|
|
326
|
+
clearLastSeenToolIds() {
|
|
327
|
+
this.#lastSeenToolIds = null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Sanitize a name to be used in tool IDs.
|
|
332
|
+
* Replaces any non-alphanumeric characters (except underscore) with underscore.
|
|
333
|
+
*/
|
|
334
|
+
export function sanitizeName(name) {
|
|
335
|
+
return name.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Extract and sanitize domain from a URL.
|
|
339
|
+
* Handles localhost specially to include port in the domain.
|
|
340
|
+
* Returns 'unknown' for URLs without a valid hostname (about:blank, file://, data:, etc.)
|
|
341
|
+
*/
|
|
342
|
+
export function extractDomain(url) {
|
|
343
|
+
try {
|
|
344
|
+
const urlObj = new URL(url);
|
|
345
|
+
const hostname = urlObj.hostname;
|
|
346
|
+
// Handle empty hostname (about:blank, file://, data:, etc.)
|
|
347
|
+
if (!hostname) {
|
|
348
|
+
return 'unknown';
|
|
349
|
+
}
|
|
350
|
+
const isLocalhost = hostname === 'localhost' ||
|
|
351
|
+
hostname === '127.0.0.1' ||
|
|
352
|
+
hostname === '[::1]';
|
|
353
|
+
const domain = isLocalhost
|
|
354
|
+
? `localhost_${urlObj.port || '80'}`
|
|
355
|
+
: hostname;
|
|
356
|
+
return sanitizeName(domain);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return 'unknown';
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Convert a sanitized domain back to display format.
|
|
364
|
+
* Handles localhost port conversion and general underscore-to-dot conversion.
|
|
365
|
+
*
|
|
366
|
+
* IMPORTANT: Handle localhost FIRST before general underscore replacement
|
|
367
|
+
*/
|
|
368
|
+
export function getDisplayDomain(sanitizedDomain) {
|
|
369
|
+
return sanitizedDomain
|
|
370
|
+
.replace(/^localhost_(\d+)$/, 'localhost:$1')
|
|
371
|
+
.replace(/_/g, '.');
|
|
372
|
+
}
|
package/build/src/tools/pages.js
CHANGED
|
@@ -29,12 +29,18 @@ export const selectPage = defineTool({
|
|
|
29
29
|
schema: {
|
|
30
30
|
pageIdx: zod
|
|
31
31
|
.number()
|
|
32
|
-
.describe(
|
|
32
|
+
.describe(`The index of the page to select. Call ${listPages.name} to get available pages.`),
|
|
33
|
+
bringToFront: zod
|
|
34
|
+
.boolean()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('Whether to focus the page and bring it to the top.'),
|
|
33
37
|
},
|
|
34
38
|
handler: async (request, response, context) => {
|
|
35
39
|
const page = context.getPageByIdx(request.params.pageIdx);
|
|
36
|
-
await page.bringToFront();
|
|
37
40
|
context.selectPage(page);
|
|
41
|
+
if (request.params.bringToFront) {
|
|
42
|
+
await page.bringToFront();
|
|
43
|
+
}
|
|
38
44
|
response.setIncludePages(true);
|
|
39
45
|
},
|
|
40
46
|
});
|
|
@@ -7,153 +7,85 @@ import { zod } from '../third_party/index.js';
|
|
|
7
7
|
import { ToolCategory } from './categories.js';
|
|
8
8
|
import { defineTool } from './ToolDefinition.js';
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Show all WebMCP tools registered across all pages, with diff since last call.
|
|
11
|
+
* First call returns full list. Subsequent calls return only added/removed tools.
|
|
12
12
|
*/
|
|
13
|
-
export const
|
|
14
|
-
name: '
|
|
15
|
-
description: '
|
|
16
|
-
'
|
|
17
|
-
'Use
|
|
13
|
+
export const diffWebMCPTools = defineTool({
|
|
14
|
+
name: 'diff_webmcp_tools',
|
|
15
|
+
description: 'Show all WebMCP tools registered across all pages, with diff since last call. ' +
|
|
16
|
+
'First call returns full list. Subsequent calls return only added/removed tools. ' +
|
|
17
|
+
'Use full=true to force complete list. ' +
|
|
18
|
+
'Tools are shown with their callable names (e.g., webmcp_localhost_3000_page0_test_add). ' +
|
|
19
|
+
'Call these tools directly by name instead of using a separate call tool.',
|
|
18
20
|
annotations: {
|
|
19
|
-
title: '
|
|
21
|
+
title: 'Diff Website MCP Tools',
|
|
20
22
|
category: ToolCategory.WEBMCP,
|
|
21
23
|
readOnlyHint: true,
|
|
22
24
|
},
|
|
23
25
|
schema: {
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.int()
|
|
26
|
+
full: zod
|
|
27
|
+
.boolean()
|
|
27
28
|
.optional()
|
|
28
|
-
.describe('
|
|
29
|
-
'Use list_pages to see available pages and their indices.'),
|
|
29
|
+
.describe('Force full tool list instead of diff. Default: false'),
|
|
30
30
|
},
|
|
31
31
|
handler: async (request, response, context) => {
|
|
32
|
-
const {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
: context.getSelectedPage();
|
|
37
|
-
// Get client from context (handles auto-connect and stale connection detection)
|
|
38
|
-
const result = await context.getWebMCPClient(page);
|
|
39
|
-
if (!result.connected) {
|
|
40
|
-
response.appendResponseLine(result.error || 'No WebMCP tools available on this page.');
|
|
32
|
+
const { full } = request.params;
|
|
33
|
+
const toolHub = context.getToolHub();
|
|
34
|
+
if (!toolHub) {
|
|
35
|
+
response.appendResponseLine('WebMCPToolHub not initialized.');
|
|
41
36
|
return;
|
|
42
37
|
}
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
const tools = toolHub.getRegisteredTools();
|
|
39
|
+
const currentToolIds = new Set(tools.map(t => t.toolId));
|
|
40
|
+
const lastSeen = toolHub.getLastSeenToolIds();
|
|
41
|
+
// First call or full=true: return full list
|
|
42
|
+
if (!lastSeen || full) {
|
|
43
|
+
toolHub.setLastSeenToolIds(currentToolIds);
|
|
44
|
+
if (tools.length === 0) {
|
|
45
|
+
response.appendResponseLine('No WebMCP tools registered.');
|
|
46
|
+
response.appendResponseLine('Navigate to a page with @mcp-b/global loaded to discover tools.');
|
|
47
|
+
return;
|
|
49
48
|
}
|
|
50
|
-
response.appendResponseLine(`${tools.length} tool(s)
|
|
49
|
+
response.appendResponseLine(`${tools.length} WebMCP tool(s) registered:`);
|
|
51
50
|
response.appendResponseLine('');
|
|
52
51
|
for (const tool of tools) {
|
|
53
|
-
response.appendResponseLine(`- ${tool.
|
|
52
|
+
response.appendResponseLine(`- ${tool.toolId}`);
|
|
53
|
+
response.appendResponseLine(` Original: ${tool.originalName}`);
|
|
54
|
+
response.appendResponseLine(` Domain: ${tool.domain} (page ${tool.pageIdx})`);
|
|
54
55
|
if (tool.description) {
|
|
55
56
|
response.appendResponseLine(` Description: ${tool.description}`);
|
|
56
57
|
}
|
|
57
|
-
if (tool.inputSchema) {
|
|
58
|
-
const schemaStr = JSON.stringify(tool.inputSchema, null, 2);
|
|
59
|
-
// Only show schema if it's not too long
|
|
60
|
-
if (schemaStr.length < 500) {
|
|
61
|
-
response.appendResponseLine(` Input Schema: ${schemaStr}`);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
response.appendResponseLine(` Input Schema: (complex schema, ${schemaStr.length} chars)`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
58
|
response.appendResponseLine('');
|
|
68
59
|
}
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
response.appendResponseLine(`Failed to list tools: ${err instanceof Error ? err.message : String(err)}`);
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
/**
|
|
76
|
-
* Call a tool registered on a webpage via WebMCP.
|
|
77
|
-
* Auto-connects to WebMCP if not already connected.
|
|
78
|
-
*/
|
|
79
|
-
export const callWebMCPTool = defineTool({
|
|
80
|
-
name: 'call_webmcp_tool',
|
|
81
|
-
description: 'Call a tool registered on a webpage via WebMCP. ' +
|
|
82
|
-
'Automatically connects if the page has @mcp-b/global loaded. ' +
|
|
83
|
-
'Use list_webmcp_tools to see available tools and their schemas. ' +
|
|
84
|
-
'Use page_index to target a specific page.',
|
|
85
|
-
annotations: {
|
|
86
|
-
title: 'Call Website MCP Tool',
|
|
87
|
-
category: ToolCategory.WEBMCP,
|
|
88
|
-
readOnlyHint: false, // Tools may have side effects
|
|
89
|
-
},
|
|
90
|
-
schema: {
|
|
91
|
-
name: zod.string().describe('The name of the tool to call'),
|
|
92
|
-
arguments: zod
|
|
93
|
-
.record(zod.any())
|
|
94
|
-
.optional()
|
|
95
|
-
.describe('Arguments to pass to the tool as a JSON object'),
|
|
96
|
-
page_index: zod
|
|
97
|
-
.number()
|
|
98
|
-
.int()
|
|
99
|
-
.optional()
|
|
100
|
-
.describe('Index of the page to call the tool on. If not specified, uses the currently selected page. ' +
|
|
101
|
-
'Use list_pages to see available pages and their indices.'),
|
|
102
|
-
},
|
|
103
|
-
handler: async (request, response, context) => {
|
|
104
|
-
const { name, arguments: args, page_index } = request.params;
|
|
105
|
-
// Get the target page
|
|
106
|
-
const page = page_index !== undefined
|
|
107
|
-
? context.getPageByIdx(page_index)
|
|
108
|
-
: context.getSelectedPage();
|
|
109
|
-
// Get client from context (handles auto-connect and stale connection detection)
|
|
110
|
-
const result = await context.getWebMCPClient(page);
|
|
111
|
-
if (!result.connected) {
|
|
112
|
-
response.appendResponseLine(result.error || 'No WebMCP tools available on this page.');
|
|
113
60
|
return;
|
|
114
61
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
response.appendResponseLine(
|
|
121
|
-
if (
|
|
122
|
-
|
|
62
|
+
// Subsequent calls: return diff
|
|
63
|
+
const added = tools.filter(t => !lastSeen.has(t.toolId));
|
|
64
|
+
const removed = [...lastSeen].filter(id => !currentToolIds.has(id));
|
|
65
|
+
toolHub.setLastSeenToolIds(currentToolIds);
|
|
66
|
+
if (added.length === 0 && removed.length === 0) {
|
|
67
|
+
response.appendResponseLine('No changes since last poll.');
|
|
68
|
+
if (tools.length > 0) {
|
|
69
|
+
const toolNames = tools.map(t => t.originalName).join(', ');
|
|
70
|
+
response.appendResponseLine(`${tools.length} tools available: ${toolNames}`);
|
|
123
71
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
for (const content of callResult.content) {
|
|
133
|
-
if (content.type === 'text') {
|
|
134
|
-
response.appendResponseLine(content.text);
|
|
135
|
-
}
|
|
136
|
-
else if (content.type === 'image') {
|
|
137
|
-
response.appendResponseLine(`[Image: ${content.mimeType}, ${content.data.length} bytes]`);
|
|
138
|
-
}
|
|
139
|
-
else if (content.type === 'resource') {
|
|
140
|
-
response.appendResponseLine(`[Resource: ${JSON.stringify(content.resource)}]`);
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
response.appendResponseLine(JSON.stringify(content, null, 2));
|
|
144
|
-
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (added.length > 0) {
|
|
75
|
+
response.appendResponseLine(`Added (${added.length}):`);
|
|
76
|
+
for (const tool of added) {
|
|
77
|
+
response.appendResponseLine(`+ ${tool.toolId}`);
|
|
78
|
+
if (tool.description) {
|
|
79
|
+
response.appendResponseLine(` ${tool.description}`);
|
|
145
80
|
}
|
|
146
81
|
}
|
|
147
|
-
|
|
148
|
-
response.appendResponseLine(JSON.stringify(callResult, null, 2));
|
|
149
|
-
}
|
|
150
|
-
if (callResult.isError) {
|
|
151
|
-
response.appendResponseLine('');
|
|
152
|
-
response.appendResponseLine('(Tool returned an error)');
|
|
153
|
-
}
|
|
82
|
+
response.appendResponseLine('');
|
|
154
83
|
}
|
|
155
|
-
|
|
156
|
-
response.appendResponseLine(`
|
|
84
|
+
if (removed.length > 0) {
|
|
85
|
+
response.appendResponseLine(`Removed (${removed.length}):`);
|
|
86
|
+
for (const id of removed) {
|
|
87
|
+
response.appendResponseLine(`- ${id}`);
|
|
88
|
+
}
|
|
157
89
|
}
|
|
158
90
|
},
|
|
159
91
|
});
|