@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.
@@ -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
- this.#pages = allPages.filter(page => {
542
- // If we allow debugging DevTools windows, return all pages.
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
@@ -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
- logger('Using existing window for this MCP session');
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
- logger('Created new window for this MCP session');
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).passthrough(),
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
- * List all WebMCP tools registered across all pages with full definitions including schemas.
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 all WebMCP tools registered across all pages. ' +
86
- 'Returns full tool definitions including input schemas. ' +
87
- 'To call a tool, use call_webmcp_tool({ name: "tool_name", arguments: {...} }).',
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
- handler: async (_request, response, context) => {
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
- const tools = toolHub.getRegisteredTools();
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
- response.appendResponseLine('No WebMCP tools registered.');
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 as JSON for easy parsing by the model
107
- const toolDefinitions = tools.map(tool => ({
108
- name: tool.originalName,
109
- description: tool.description,
110
- inputSchema: tool.inputSchema,
111
- pageIdx: tool.pageIdx,
112
- domain: tool.domain,
113
- }));
114
- response.appendResponseLine(JSON.stringify({ tools: toolDefinitions }, null, 2));
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 via WebMCP. ' +
124
- 'Usage: call_webmcp_tool({ name: "tool_name", arguments: { key: "value" } }). ' +
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
- .record(zod.any())
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.5.7",
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.25.76"
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.25.76"
92
+ "zod": "4.3.5"
93
93
  },
94
94
  "engines": {
95
95
  "node": "^20.19.0 || ^22.12.0 || >=23"