@mcp-b/chrome-devtools-mcp 1.5.7 → 1.6.0
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/build/src/McpContext.js +122 -5
- package/build/src/McpResponse.js +9 -0
- package/build/src/main.js +30 -4
- package/build/src/third_party/index.js +1 -1
- package/build/src/tools/webmcp.js +161 -23
- package/package.json +3 -3
package/build/src/McpContext.js
CHANGED
|
@@ -98,6 +98,13 @@ export class McpContext {
|
|
|
98
98
|
#toolHub;
|
|
99
99
|
/** Tracks pages that have WebMCP auto-detection listeners installed. */
|
|
100
100
|
#pagesWithWebMCPListeners = new WeakSet();
|
|
101
|
+
/**
|
|
102
|
+
* The windowId that this MCP session owns.
|
|
103
|
+
* When set, page operations are scoped to only pages in this window.
|
|
104
|
+
*/
|
|
105
|
+
#sessionWindowId;
|
|
106
|
+
/** Cached browser-level CDP session for window operations. */
|
|
107
|
+
#browserCdpSession;
|
|
101
108
|
constructor(browser, logger, options, locatorClass) {
|
|
102
109
|
this.browser = browser;
|
|
103
110
|
this.logger = logger;
|
|
@@ -193,6 +200,41 @@ export class McpContext {
|
|
|
193
200
|
getToolHub() {
|
|
194
201
|
return this.#toolHub;
|
|
195
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Get or create a browser-level CDP session for window operations.
|
|
205
|
+
*/
|
|
206
|
+
async #getBrowserCdpSession() {
|
|
207
|
+
if (!this.#browserCdpSession) {
|
|
208
|
+
this.#browserCdpSession = await this.browser.target().createCDPSession();
|
|
209
|
+
}
|
|
210
|
+
return this.#browserCdpSession;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get the windowId for a given page using CDP.
|
|
214
|
+
*/
|
|
215
|
+
async getWindowIdForPage(page) {
|
|
216
|
+
const cdpSession = await this.#getBrowserCdpSession();
|
|
217
|
+
// @ts-expect-error _targetId is internal but stable
|
|
218
|
+
const targetId = page.target()._targetId;
|
|
219
|
+
const { windowId } = await cdpSession.send('Browser.getWindowForTarget', {
|
|
220
|
+
targetId,
|
|
221
|
+
});
|
|
222
|
+
return windowId;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Set the window that this session owns.
|
|
226
|
+
* When set, page operations are scoped to only pages in this window.
|
|
227
|
+
*/
|
|
228
|
+
setSessionWindowId(windowId) {
|
|
229
|
+
this.#sessionWindowId = windowId;
|
|
230
|
+
this.logger(`Session bound to windowId: ${windowId}`);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get the session's window ID, or undefined if not set.
|
|
234
|
+
*/
|
|
235
|
+
getSessionWindowId() {
|
|
236
|
+
return this.#sessionWindowId;
|
|
237
|
+
}
|
|
196
238
|
/**
|
|
197
239
|
* Set up automatic WebMCP detection for a page.
|
|
198
240
|
* This installs listeners that detect WebMCP after navigation and sync tools.
|
|
@@ -361,7 +403,34 @@ export class McpContext {
|
|
|
361
403
|
return this.#consoleCollector.getById(this.getSelectedPage(), id);
|
|
362
404
|
}
|
|
363
405
|
async newPage() {
|
|
406
|
+
// If we have a session window, ensure our window is focused first
|
|
407
|
+
// This increases the chance that Chrome creates the new tab in our window
|
|
408
|
+
if (this.#sessionWindowId !== undefined && this.#pages.length > 0) {
|
|
409
|
+
try {
|
|
410
|
+
const existingPage = this.#pages[0];
|
|
411
|
+
if (existingPage) {
|
|
412
|
+
await existingPage.bringToFront();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// Best effort - focus might fail if page is closing
|
|
417
|
+
}
|
|
418
|
+
}
|
|
364
419
|
const page = await this.browser.newPage();
|
|
420
|
+
// Verify the new page is in our window (if session scoping is active)
|
|
421
|
+
if (this.#sessionWindowId !== undefined) {
|
|
422
|
+
try {
|
|
423
|
+
const newPageWindowId = await this.getWindowIdForPage(page);
|
|
424
|
+
if (newPageWindowId !== this.#sessionWindowId) {
|
|
425
|
+
// New tab went to wrong window - this is a known Chrome behavior issue
|
|
426
|
+
this.logger(`Warning: new_page created tab in window ${newPageWindowId} instead of session window ${this.#sessionWindowId}. ` +
|
|
427
|
+
`Tab may not be visible in list_pages.`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
// Failed to get windowId - page might be in an unexpected state
|
|
432
|
+
}
|
|
433
|
+
}
|
|
365
434
|
await this.createPagesSnapshot();
|
|
366
435
|
// Mark as explicitly selected so this session sticks to this page
|
|
367
436
|
this.selectPage(page, true);
|
|
@@ -380,7 +449,6 @@ export class McpContext {
|
|
|
380
449
|
url: 'about:blank',
|
|
381
450
|
newWindow: true,
|
|
382
451
|
});
|
|
383
|
-
await cdpSession.detach();
|
|
384
452
|
// Wait for the new page to be available
|
|
385
453
|
const target = await this.browser.waitForTarget(target => {
|
|
386
454
|
// @ts-expect-error _targetId is internal but stable
|
|
@@ -390,6 +458,30 @@ export class McpContext {
|
|
|
390
458
|
if (!page) {
|
|
391
459
|
throw new Error('Failed to get page from new window target');
|
|
392
460
|
}
|
|
461
|
+
// Get window ID for this target (required for session scoping)
|
|
462
|
+
const { windowId } = await cdpSession.send('Browser.getWindowForTarget', {
|
|
463
|
+
targetId,
|
|
464
|
+
});
|
|
465
|
+
// Set window to nearly full screen (large size that fits most displays)
|
|
466
|
+
try {
|
|
467
|
+
// Set to large dimensions (works well on 1920x1080 and larger displays)
|
|
468
|
+
// This is ~95% of common display sizes without being truly fullscreen
|
|
469
|
+
await cdpSession.send('Browser.setWindowBounds', {
|
|
470
|
+
windowId,
|
|
471
|
+
bounds: {
|
|
472
|
+
left: 20,
|
|
473
|
+
top: 20,
|
|
474
|
+
width: 1800,
|
|
475
|
+
height: 1200,
|
|
476
|
+
windowState: 'normal',
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
// Non-fatal: window sizing is best-effort
|
|
482
|
+
this.logger('Failed to resize window:', err);
|
|
483
|
+
}
|
|
484
|
+
await cdpSession.detach();
|
|
393
485
|
await this.createPagesSnapshot();
|
|
394
486
|
// Mark as explicitly selected so this session sticks to this window
|
|
395
487
|
this.selectPage(page, true);
|
|
@@ -397,7 +489,7 @@ export class McpContext {
|
|
|
397
489
|
this.#consoleCollector.addPage(page);
|
|
398
490
|
// Set up WebMCP auto-detection for the new page
|
|
399
491
|
this.#setupWebMCPAutoDetection(page);
|
|
400
|
-
return page;
|
|
492
|
+
return { page, windowId };
|
|
401
493
|
}
|
|
402
494
|
async closePage(pageIdx) {
|
|
403
495
|
if (this.#pages.length === 1) {
|
|
@@ -535,15 +627,40 @@ export class McpContext {
|
|
|
535
627
|
}
|
|
536
628
|
/**
|
|
537
629
|
* Creates a snapshot of the pages.
|
|
630
|
+
* If a sessionWindowId is set, only pages from that window are included.
|
|
538
631
|
*/
|
|
539
632
|
async createPagesSnapshot() {
|
|
540
633
|
const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
// If we are in regular mode, the user should only see non-DevTools page.
|
|
634
|
+
// First filter: DevTools pages (unless experimental mode is enabled)
|
|
635
|
+
let filteredPages = allPages.filter(page => {
|
|
544
636
|
return (this.#options.experimentalDevToolsDebugging ||
|
|
545
637
|
!page.url().startsWith('devtools://'));
|
|
546
638
|
});
|
|
639
|
+
// Second filter: Session window scoping
|
|
640
|
+
// If we have a sessionWindowId, only include pages from that window
|
|
641
|
+
if (this.#sessionWindowId !== undefined) {
|
|
642
|
+
const windowFilteredPages = [];
|
|
643
|
+
// Check window IDs in parallel for better performance
|
|
644
|
+
const windowIdResults = await Promise.allSettled(filteredPages.map(async (page) => {
|
|
645
|
+
try {
|
|
646
|
+
const windowId = await this.getWindowIdForPage(page);
|
|
647
|
+
return { page, windowId };
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
// Page might be closing, exclude it
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
}));
|
|
654
|
+
for (const result of windowIdResults) {
|
|
655
|
+
if (result.status === 'fulfilled' &&
|
|
656
|
+
result.value &&
|
|
657
|
+
result.value.windowId === this.#sessionWindowId) {
|
|
658
|
+
windowFilteredPages.push(result.value.page);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
filteredPages = windowFilteredPages;
|
|
662
|
+
}
|
|
663
|
+
this.#pages = filteredPages;
|
|
547
664
|
// Only auto-select pages[0] if:
|
|
548
665
|
// 1. No page has been explicitly selected for this session AND
|
|
549
666
|
// 2. Either there's no selected page OR the selected page is no longer valid
|
package/build/src/McpResponse.js
CHANGED
|
@@ -310,6 +310,15 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
|
|
310
310
|
}
|
|
311
311
|
else {
|
|
312
312
|
response.push('<no console messages found>');
|
|
313
|
+
// Provide helpful hint about preserved messages if not already enabled
|
|
314
|
+
if (!this.#consoleDataOptions.includePreservedMessages) {
|
|
315
|
+
response.push('');
|
|
316
|
+
response.push('Tip: Use includePreservedMessages: true to see messages from previous navigations.');
|
|
317
|
+
}
|
|
318
|
+
// Provide hint about type filtering if specified
|
|
319
|
+
if (this.#consoleDataOptions.types?.length) {
|
|
320
|
+
response.push(`(Filtering by types: ${this.#consoleDataOptions.types.join(', ')})`);
|
|
321
|
+
}
|
|
313
322
|
}
|
|
314
323
|
}
|
|
315
324
|
const text = {
|
package/build/src/main.js
CHANGED
|
@@ -127,13 +127,39 @@ async function getContext() {
|
|
|
127
127
|
// Fresh browser launch - use the existing default page
|
|
128
128
|
// Mark it as explicitly selected so this session stays pinned to it
|
|
129
129
|
context.selectPage(context.getSelectedPage(), true);
|
|
130
|
-
|
|
130
|
+
// Capture windowId for session scoping and resize the window
|
|
131
|
+
try {
|
|
132
|
+
const page = context.getSelectedPage();
|
|
133
|
+
const windowId = await context.getWindowIdForPage(page);
|
|
134
|
+
context.setSessionWindowId(windowId);
|
|
135
|
+
logger(`Using existing window for this MCP session, windowId: ${windowId}`);
|
|
136
|
+
// Resize to nearly full screen
|
|
137
|
+
const browserTarget = browser.target();
|
|
138
|
+
const cdpSession = await browserTarget.createCDPSession();
|
|
139
|
+
await cdpSession.send('Browser.setWindowBounds', {
|
|
140
|
+
windowId,
|
|
141
|
+
bounds: {
|
|
142
|
+
left: 20,
|
|
143
|
+
top: 20,
|
|
144
|
+
width: 1800,
|
|
145
|
+
height: 1200,
|
|
146
|
+
windowState: 'normal',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
await cdpSession.detach();
|
|
150
|
+
logger('Resized window to nearly full screen');
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
// Non-fatal: window sizing is best-effort, but windowId capture is important
|
|
154
|
+
logger('Failed to capture windowId or resize window:', err);
|
|
155
|
+
}
|
|
131
156
|
}
|
|
132
157
|
else {
|
|
133
158
|
// Connected to existing browser - create new window for isolation
|
|
134
159
|
// This ensures multiple MCP clients don't step on each other's toes
|
|
135
|
-
await context.newWindow();
|
|
136
|
-
|
|
160
|
+
const { windowId } = await context.newWindow();
|
|
161
|
+
context.setSessionWindowId(windowId);
|
|
162
|
+
logger(`Created new window for this MCP session, windowId: ${windowId}`);
|
|
137
163
|
}
|
|
138
164
|
// Initialize WebMCP tool hub for dynamic tool registration
|
|
139
165
|
const toolHub = new WebMCPToolHub(server, context);
|
|
@@ -179,7 +205,7 @@ function registerTool(tool) {
|
|
|
179
205
|
}
|
|
180
206
|
server.registerTool(tool.name, {
|
|
181
207
|
description: tool.description,
|
|
182
|
-
inputSchema: zod.object(tool.schema).
|
|
208
|
+
inputSchema: zod.object(tool.schema).strict(),
|
|
183
209
|
annotations: tool.annotations,
|
|
184
210
|
}, async (params) => {
|
|
185
211
|
const guard = await toolMutex.acquire();
|
|
@@ -11,6 +11,6 @@ export { default as debug } from 'debug';
|
|
|
11
11
|
export { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
12
|
export { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
13
|
export { SetLevelRequestSchema, ToolListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
14
|
-
export { z as zod } from 'zod';
|
|
14
|
+
export { z as zod } from 'zod/v4';
|
|
15
15
|
export { Locator, PredefinedNetworkConditions, CDPSessionEvent, } from 'puppeteer-core';
|
|
16
16
|
export { default as puppeteer } from 'puppeteer-core';
|
|
@@ -78,40 +78,133 @@ import { defineTool } from './ToolDefinition.js';
|
|
|
78
78
|
// response.appendResponseLine(' 2. take_snapshot - verify page state');
|
|
79
79
|
// }
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
81
|
+
* Convert a glob-style pattern to a RegExp.
|
|
82
|
+
* Supports * (any chars) and ? (single char).
|
|
83
|
+
*/
|
|
84
|
+
function globToRegex(pattern) {
|
|
85
|
+
const escaped = pattern
|
|
86
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except * and ?
|
|
87
|
+
.replace(/\*/g, '.*') // * -> .*
|
|
88
|
+
.replace(/\?/g, '.'); // ? -> .
|
|
89
|
+
return new RegExp(`^${escaped}$`, 'i'); // Case insensitive, full match
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* List WebMCP tools registered on browser pages.
|
|
93
|
+
* By default returns tools from the currently selected page only.
|
|
94
|
+
* Use all_pages=true to see tools from all tabs.
|
|
82
95
|
*/
|
|
83
96
|
export const listWebMCPTools = defineTool({
|
|
84
97
|
name: 'list_webmcp_tools',
|
|
85
|
-
description: 'List
|
|
86
|
-
'
|
|
87
|
-
'
|
|
98
|
+
description: 'List WebMCP tools registered on browser pages. ' +
|
|
99
|
+
'By default, returns tools from the currently selected page only. ' +
|
|
100
|
+
'Use all_pages=true to see tools from all tabs. ' +
|
|
101
|
+
'Returns tool definitions including name, description, input schema, and page index. ' +
|
|
102
|
+
'Use call_webmcp_tool to invoke a tool.',
|
|
88
103
|
annotations: {
|
|
89
104
|
title: 'List Website MCP Tools',
|
|
90
105
|
category: ToolCategory.WEBMCP,
|
|
91
106
|
readOnlyHint: true,
|
|
92
107
|
},
|
|
93
|
-
schema: {
|
|
94
|
-
|
|
108
|
+
schema: {
|
|
109
|
+
page_index: zod
|
|
110
|
+
.number()
|
|
111
|
+
.int()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('Only show tools from this specific page index'),
|
|
114
|
+
all_pages: zod
|
|
115
|
+
.boolean()
|
|
116
|
+
.optional()
|
|
117
|
+
.describe('If true, return tools from all pages instead of just the selected page (default: false)'),
|
|
118
|
+
pattern: zod
|
|
119
|
+
.string()
|
|
120
|
+
.optional()
|
|
121
|
+
.describe('Glob pattern to filter tool names (e.g., "skill*", "*_config")'),
|
|
122
|
+
summary: zod
|
|
123
|
+
.boolean()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe('If true, return only name and first line of description, omitting full schemas (default: false)'),
|
|
126
|
+
},
|
|
127
|
+
handler: async (request, response, context) => {
|
|
128
|
+
const { page_index, all_pages, pattern, summary } = request.params;
|
|
95
129
|
const toolHub = context.getToolHub();
|
|
96
130
|
if (!toolHub) {
|
|
97
131
|
response.appendResponseLine('WebMCPToolHub not initialized.');
|
|
98
132
|
return;
|
|
99
133
|
}
|
|
100
|
-
|
|
134
|
+
let tools = toolHub.getRegisteredTools();
|
|
135
|
+
// If no tools found, try connecting to WebMCP on the current page
|
|
136
|
+
// This handles cases where auto-detection is still in progress or timed out
|
|
137
|
+
if (tools.length === 0) {
|
|
138
|
+
const page = context.getSelectedPage();
|
|
139
|
+
const result = await context.getWebMCPClient(page);
|
|
140
|
+
if (result.connected) {
|
|
141
|
+
// Re-fetch tools after connection (sync happens in getWebMCPClient)
|
|
142
|
+
tools = toolHub.getRegisteredTools();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Determine which page(s) to show
|
|
146
|
+
const selectedPageIdx = context.getPages().indexOf(context.getSelectedPage());
|
|
147
|
+
// Filter by page
|
|
148
|
+
if (page_index !== undefined) {
|
|
149
|
+
// Specific page requested
|
|
150
|
+
tools = tools.filter(t => t.pageIdx === page_index);
|
|
151
|
+
}
|
|
152
|
+
else if (!all_pages) {
|
|
153
|
+
// Default: selected page only
|
|
154
|
+
tools = tools.filter(t => t.pageIdx === selectedPageIdx);
|
|
155
|
+
}
|
|
156
|
+
// else: all_pages=true, show everything
|
|
157
|
+
// Filter by pattern
|
|
158
|
+
if (pattern) {
|
|
159
|
+
const regex = globToRegex(pattern);
|
|
160
|
+
tools = tools.filter(t => regex.test(t.originalName));
|
|
161
|
+
}
|
|
101
162
|
if (tools.length === 0) {
|
|
102
|
-
|
|
163
|
+
const filters = [];
|
|
164
|
+
if (page_index !== undefined) {
|
|
165
|
+
filters.push(`page_index=${page_index}`);
|
|
166
|
+
}
|
|
167
|
+
else if (!all_pages) {
|
|
168
|
+
filters.push(`selected page (${selectedPageIdx})`);
|
|
169
|
+
}
|
|
170
|
+
if (pattern) {
|
|
171
|
+
filters.push(`pattern="${pattern}"`);
|
|
172
|
+
}
|
|
173
|
+
const filterMsg = filters.length > 0 ? ` (filters: ${filters.join(', ')})` : '';
|
|
174
|
+
response.appendResponseLine(`No WebMCP tools found${filterMsg}.`);
|
|
175
|
+
if (!all_pages && page_index === undefined) {
|
|
176
|
+
response.appendResponseLine('');
|
|
177
|
+
response.appendResponseLine('Tip: Use all_pages=true to search across all pages.');
|
|
178
|
+
}
|
|
179
|
+
response.appendResponseLine('');
|
|
103
180
|
response.appendResponseLine('Navigate to a page with @mcp-b/global loaded to discover tools.');
|
|
104
181
|
return;
|
|
105
182
|
}
|
|
106
|
-
// Format
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
183
|
+
// Format output
|
|
184
|
+
if (summary) {
|
|
185
|
+
// Compact output: name + first line of description
|
|
186
|
+
const toolSummaries = tools.map(tool => {
|
|
187
|
+
const firstLine = tool.description.split('\n')[0].split('. ')[0];
|
|
188
|
+
const truncated = firstLine.length > 60 ? firstLine.slice(0, 57) + '...' : firstLine;
|
|
189
|
+
return {
|
|
190
|
+
name: tool.originalName,
|
|
191
|
+
description: truncated,
|
|
192
|
+
pageIdx: tool.pageIdx,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
response.appendResponseLine(JSON.stringify({ tools: toolSummaries, count: tools.length }, null, 2));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// Full output with schemas
|
|
199
|
+
const toolDefinitions = tools.map(tool => ({
|
|
200
|
+
name: tool.originalName,
|
|
201
|
+
description: tool.description,
|
|
202
|
+
inputSchema: tool.inputSchema,
|
|
203
|
+
pageIdx: tool.pageIdx,
|
|
204
|
+
domain: tool.domain,
|
|
205
|
+
}));
|
|
206
|
+
response.appendResponseLine(JSON.stringify({ tools: toolDefinitions, count: tools.length }, null, 2));
|
|
207
|
+
}
|
|
115
208
|
},
|
|
116
209
|
});
|
|
117
210
|
/**
|
|
@@ -120,10 +213,9 @@ export const listWebMCPTools = defineTool({
|
|
|
120
213
|
*/
|
|
121
214
|
export const callWebMCPTool = defineTool({
|
|
122
215
|
name: 'call_webmcp_tool',
|
|
123
|
-
description: 'Call a tool registered on a webpage
|
|
124
|
-
'
|
|
125
|
-
'Use list_webmcp_tools to see available tools and their schemas.
|
|
126
|
-
'Use page_index to target a specific page.',
|
|
216
|
+
description: 'Call a WebMCP tool registered on a webpage. ' +
|
|
217
|
+
'Auto-connects to the page if needed. ' +
|
|
218
|
+
'Use list_webmcp_tools first to see available tools and their input schemas.',
|
|
127
219
|
annotations: {
|
|
128
220
|
title: 'Call Website MCP Tool',
|
|
129
221
|
category: ToolCategory.WEBMCP,
|
|
@@ -132,9 +224,31 @@ export const callWebMCPTool = defineTool({
|
|
|
132
224
|
schema: {
|
|
133
225
|
name: zod.string().describe('The name of the tool to call'),
|
|
134
226
|
arguments: zod
|
|
135
|
-
.
|
|
227
|
+
.union([
|
|
228
|
+
zod.record(zod.string(), zod.any()),
|
|
229
|
+
zod.string().transform((str, ctx) => {
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(str);
|
|
232
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
233
|
+
ctx.addIssue({
|
|
234
|
+
code: zod.ZodIssueCode.custom,
|
|
235
|
+
message: 'Arguments must be a JSON object, not an array or primitive',
|
|
236
|
+
});
|
|
237
|
+
return zod.NEVER;
|
|
238
|
+
}
|
|
239
|
+
return parsed;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
ctx.addIssue({
|
|
243
|
+
code: zod.ZodIssueCode.custom,
|
|
244
|
+
message: `Invalid JSON string for arguments: ${str.slice(0, 100)}${str.length > 100 ? '...' : ''}`,
|
|
245
|
+
});
|
|
246
|
+
return zod.NEVER;
|
|
247
|
+
}
|
|
248
|
+
}),
|
|
249
|
+
])
|
|
136
250
|
.optional()
|
|
137
|
-
.describe('Arguments to pass to the tool as a JSON object'),
|
|
251
|
+
.describe('Arguments to pass to the tool as a JSON object (or JSON string that will be parsed)'),
|
|
138
252
|
page_index: zod
|
|
139
253
|
.number()
|
|
140
254
|
.int()
|
|
@@ -166,6 +280,30 @@ export const callWebMCPTool = defineTool({
|
|
|
166
280
|
return;
|
|
167
281
|
}
|
|
168
282
|
const client = result.client;
|
|
283
|
+
// Check if arguments are empty but the tool expects required fields
|
|
284
|
+
const toolHub = context.getToolHub();
|
|
285
|
+
const toolDef = toolHub?.getToolByName(name, page);
|
|
286
|
+
const argsEmpty = !args || Object.keys(args).length === 0;
|
|
287
|
+
if (argsEmpty && toolDef?.inputSchema) {
|
|
288
|
+
const schema = toolDef.inputSchema;
|
|
289
|
+
const requiredFields = schema.required ?? [];
|
|
290
|
+
const properties = schema.properties;
|
|
291
|
+
if (requiredFields.length > 0) {
|
|
292
|
+
response.appendResponseLine(`⚠️ Warning: Calling "${name}" with empty arguments, but tool expects required fields: ${requiredFields.join(', ')}`);
|
|
293
|
+
response.appendResponseLine('');
|
|
294
|
+
// Show the expected schema
|
|
295
|
+
if (properties) {
|
|
296
|
+
response.appendResponseLine('Expected schema:');
|
|
297
|
+
for (const field of requiredFields) {
|
|
298
|
+
const prop = properties[field];
|
|
299
|
+
const typeStr = prop?.type || 'unknown';
|
|
300
|
+
const descStr = prop?.description ? ` - ${prop.description}` : '';
|
|
301
|
+
response.appendResponseLine(` • ${field} (${typeStr})${descStr}`);
|
|
302
|
+
}
|
|
303
|
+
response.appendResponseLine('');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
169
307
|
try {
|
|
170
308
|
const callResult = await client.callTool({
|
|
171
309
|
name,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-b/chrome-devtools-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "MCP server for Chrome DevTools with WebMCP integration for connecting to website MCP tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"puppeteer": "24.32.0",
|
|
57
57
|
"puppeteer-core": "24.32.0",
|
|
58
58
|
"yargs": "18.0.0",
|
|
59
|
-
"zod": "3.
|
|
59
|
+
"zod": "4.3.5"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@eslint/js": "^9.35.0",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"typescript": "^5.9.2",
|
|
90
90
|
"typescript-eslint": "^8.43.0",
|
|
91
91
|
"yargs": "18.0.0",
|
|
92
|
-
"zod": "3.
|
|
92
|
+
"zod": "4.3.5"
|
|
93
93
|
},
|
|
94
94
|
"engines": {
|
|
95
95
|
"node": "^20.19.0 || ^22.12.0 || >=23"
|