@ejazullah/browser-mcp 0.0.59 → 0.0.61

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.
@@ -128,7 +128,37 @@ async function handleSSE(serverBackendFactory, req, res, url, sessions) {
128
128
  res.statusCode = 405;
129
129
  res.end('Method not allowed');
130
130
  }
131
- async function handleStreamable(serverBackendFactory, req, res, sessions) {
131
+ function cdpSessionKeyFromUrl(value) {
132
+ try {
133
+ const parsed = new URL(value);
134
+ const match = parsed.pathname.match(/\/devtools\/([^/]+)/i);
135
+ return match?.[1];
136
+ }
137
+ catch {
138
+ const match = value.match(/\/devtools\/([^/]+)/i);
139
+ return match?.[1];
140
+ }
141
+ }
142
+ function resolveClientCdpSessionKey(req) {
143
+ const explicitSessionId = req.headers['x-cdp-session-id'];
144
+ if (explicitSessionId)
145
+ return explicitSessionId;
146
+ const cdpUrl = req.headers['x-cdp-url']
147
+ ?? req.headers['x-cdp-endpoint']
148
+ ?? req.headers['x-browser-url'];
149
+ if (!cdpUrl)
150
+ return undefined;
151
+ return cdpSessionKeyFromUrl(cdpUrl);
152
+ }
153
+ function findSessionIdByCdpKey(sessionCdpKeys, sessions, cdpSessionKey) {
154
+ for (const [knownSessionId, knownCdpSessionKey] of sessionCdpKeys.entries()) {
155
+ if (knownCdpSessionKey === cdpSessionKey && sessions.has(knownSessionId))
156
+ return knownSessionId;
157
+ }
158
+ return undefined;
159
+ }
160
+ async function handleStreamable(serverBackendFactory, req, res, sessions, sessionCdpKeys) {
161
+ const cdpSessionKey = resolveClientCdpSessionKey(req);
132
162
  const sessionId = req.headers['mcp-session-id'];
133
163
  if (sessionId) {
134
164
  const transport = sessions.get(sessionId);
@@ -138,18 +168,60 @@ async function handleStreamable(serverBackendFactory, req, res, sessions) {
138
168
  res.end('Session not found');
139
169
  return;
140
170
  }
171
+ if (cdpSessionKey) {
172
+ const matchedSessionId = findSessionIdByCdpKey(sessionCdpKeys, sessions, cdpSessionKey);
173
+ if (matchedSessionId) {
174
+ const matchedTransport = sessions.get(matchedSessionId);
175
+ testDebug(`stale mcp-session-id ${sessionId}, reusing cdp-matched session: ${matchedSessionId} (${cdpSessionKey})`);
176
+ return await matchedTransport.handleRequest(req, res);
177
+ }
178
+ }
179
+ if (sessions.size === 1) {
180
+ const [singleSessionId, singleTransport] = sessions.entries().next().value;
181
+ testDebug(`stale mcp-session-id ${sessionId}, reusing single active session: ${singleSessionId}`);
182
+ return await singleTransport.handleRequest(req, res);
183
+ }
141
184
  delete req.headers['mcp-session-id'];
142
185
  testDebug(`stale http session id: ${sessionId}, creating a new session`);
143
186
  }
144
187
  else {
145
- return await transport.handleRequest(req, res);
188
+ if (cdpSessionKey) {
189
+ const knownCdpSessionKey = sessionCdpKeys.get(sessionId);
190
+ if (knownCdpSessionKey && knownCdpSessionKey !== cdpSessionKey) {
191
+ delete req.headers['mcp-session-id'];
192
+ testDebug(`browser changed for mcp session ${sessionId} (${knownCdpSessionKey} -> ${cdpSessionKey}), creating a new session`);
193
+ }
194
+ else {
195
+ if (!knownCdpSessionKey)
196
+ sessionCdpKeys.set(sessionId, cdpSessionKey);
197
+ return await transport.handleRequest(req, res);
198
+ }
199
+ }
200
+ else {
201
+ return await transport.handleRequest(req, res);
202
+ }
203
+ }
204
+ }
205
+ if (!sessionId && cdpSessionKey && req.method === 'POST') {
206
+ const matchedSessionId = findSessionIdByCdpKey(sessionCdpKeys, sessions, cdpSessionKey);
207
+ if (matchedSessionId) {
208
+ const matchedTransport = sessions.get(matchedSessionId);
209
+ testDebug(`missing mcp-session-id, reusing cdp-matched session: ${matchedSessionId} (${cdpSessionKey})`);
210
+ return await matchedTransport.handleRequest(req, res);
146
211
  }
147
212
  }
213
+ if (!sessionId && req.method === 'POST' && sessions.size === 1) {
214
+ const [singleSessionId, singleTransport] = sessions.entries().next().value;
215
+ testDebug(`missing mcp-session-id, reusing single active session: ${singleSessionId}`);
216
+ return await singleTransport.handleRequest(req, res);
217
+ }
148
218
  if (req.method === 'POST') {
149
219
  const transport = new StreamableHTTPServerTransport({
150
220
  sessionIdGenerator: () => crypto.randomUUID(),
151
221
  onsessioninitialized: async (sessionId) => {
152
222
  testDebug(`create http session: ${transport.sessionId}`);
223
+ if (cdpSessionKey)
224
+ sessionCdpKeys.set(sessionId, cdpSessionKey);
153
225
  await mcpServer.connect(serverBackendFactory, transport, true);
154
226
  sessions.set(sessionId, transport);
155
227
  }
@@ -158,6 +230,7 @@ async function handleStreamable(serverBackendFactory, req, res, sessions) {
158
230
  if (!transport.sessionId)
159
231
  return;
160
232
  sessions.delete(transport.sessionId);
233
+ sessionCdpKeys.delete(transport.sessionId);
161
234
  testDebug(`delete http session: ${transport.sessionId}`);
162
235
  };
163
236
  await transport.handleRequest(req, res);
@@ -169,6 +242,7 @@ async function handleStreamable(serverBackendFactory, req, res, sessions) {
169
242
  function startHttpTransport(httpServer, serverBackendFactory, auth) {
170
243
  const sseSessions = new Map();
171
244
  const streamableSessions = new Map();
245
+ const streamableSessionCdpKeys = new Map();
172
246
  // Configure CORS with permissive settings for MCP tools
173
247
  const corsHandler = cors({
174
248
  origin: true, // Allow all origins
@@ -193,7 +267,7 @@ function startHttpTransport(httpServer, serverBackendFactory, auth) {
193
267
  if (url.pathname.startsWith('/sse'))
194
268
  await handleSSE(serverBackendFactory, req, res, url, sseSessions);
195
269
  else
196
- await handleStreamable(serverBackendFactory, req, res, streamableSessions);
270
+ await handleStreamable(serverBackendFactory, req, res, streamableSessions, streamableSessionCdpKeys);
197
271
  });
198
272
  });
199
273
  const url = httpAddressToString(httpServer.address());
package/lib/program.js CHANGED
@@ -16,6 +16,7 @@
16
16
  import { program, Option } from 'commander';
17
17
  // @ts-ignore
18
18
  import { startTraceViewerServer } from 'playwright-core/lib/server';
19
+ import debug from 'debug';
19
20
  import * as mcpTransport from './mcp/transport.js';
20
21
  import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
21
22
  import { buildAuthConfig } from './auth.js';
@@ -25,6 +26,28 @@ import { BrowserServerBackend } from './browserServerBackend.js';
25
26
  import { Context } from './context.js';
26
27
  import { contextFactory } from './browserContextFactory.js';
27
28
  import { runLoopTools } from './loopTools/main.js';
29
+ const mcpDebugNamespace = 'pw:mcp:*';
30
+ function setMcpDebugLogging(enabled) {
31
+ if (enabled === undefined)
32
+ return;
33
+ const current = process.env.DEBUG ?? '';
34
+ const namespaces = current.split(',').map(token => token.trim()).filter(Boolean);
35
+ if (enabled) {
36
+ if (!namespaces.includes(mcpDebugNamespace)) {
37
+ namespaces.push(mcpDebugNamespace);
38
+ process.env.DEBUG = namespaces.join(',');
39
+ debug.enable(process.env.DEBUG);
40
+ }
41
+ // eslint-disable-next-line no-console
42
+ console.error(`[mcp] verbose logs enabled (${mcpDebugNamespace})`);
43
+ return;
44
+ }
45
+ const filtered = namespaces.filter(ns => ns !== mcpDebugNamespace && !ns.startsWith('pw:mcp:'));
46
+ process.env.DEBUG = filtered.join(',');
47
+ debug.enable(process.env.DEBUG);
48
+ // eslint-disable-next-line no-console
49
+ console.error('[mcp] verbose logs disabled');
50
+ }
28
51
  program
29
52
  .version('Version ' + packageJSON.version)
30
53
  .name(packageJSON.name)
@@ -53,6 +76,8 @@ program
53
76
  .option('--user-agent <ua string>', 'specify user agent string')
54
77
  .option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
55
78
  .option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"')
79
+ .option('--mcp-logs', 'Enable verbose MCP/session transport logs in CLI output')
80
+ .option('--no-mcp-logs', 'Disable verbose MCP/session transport logs in CLI output')
56
81
  .option('--mongodb-url <url>', 'MongoDB connection URL. Example: mongodb://localhost:27017')
57
82
  .option('--mongodb-db <name>', 'MongoDB database name. Default: playwright_mcp')
58
83
  .option('--mongodb-collection <name>', 'MongoDB collection name. Default: element_interactions')
@@ -67,6 +92,7 @@ program
67
92
  .addOption(new Option('--vision', 'Legacy option, use --caps=vision instead').hideHelp())
68
93
  .action(async (options) => {
69
94
  setupExitWatchdog();
95
+ setMcpDebugLogging(options.mcpLogs);
70
96
  if (options.vision) {
71
97
  // eslint-disable-next-line no-console
72
98
  console.error('The --vision option is deprecated, use --caps=vision instead');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ejazullah/browser-mcp",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "description": "@ejazullah/browser-mcp - Enhanced Playwright Tools for MCP with CDP Support",
5
5
  "type": "module",
6
6
  "repository": {