@ejazullah/browser-mcp 0.0.62 → 0.0.64

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 CHANGED
@@ -319,6 +319,14 @@ Enhanced Playwright MCP server supports following arguments. They can be provide
319
319
  specified, a temporary directory will be created.
320
320
  --viewport-size <size> specify browser viewport size in pixels, for
321
321
  example "1280, 720"
322
+ --mcp-logs Enable verbose MCP/session transport logs in CLI
323
+ output
324
+ --no-mcp-logs Disable verbose MCP/session transport logs in
325
+ CLI output
326
+ --mcp-heartbeat Enable MCP streamable transport heartbeat (may
327
+ close sessions on slow/long operations)
328
+ --no-mcp-heartbeat Disable MCP streamable transport heartbeat
329
+ (recommended for n8n item-by-item runs)
322
330
  ```
323
331
 
324
332
  <!--- End of options generated section -->
@@ -471,6 +479,34 @@ And then in MCP client config, set the `url` to the HTTP endpoint:
471
479
 
472
480
  If a client reconnects with a stale `mcp-session-id`, the server now recovers automatically for `POST` requests by creating a new session. If your client reports `Session not found`, reconnect and retry the request.
473
481
 
482
+ ### Streamable HTTP session behavior (n8n / CDP)
483
+
484
+ When using Streamable HTTP (`/mcp`) with tools like n8n (especially item-by-item execution), requests may arrive without consistent `mcp-session-id` propagation.
485
+
486
+ The server handles stale `mcp-session-id` for `POST` by creating a new session. If your client reuses an old session id, reconnect and initialize again before retrying tool calls.
487
+
488
+ For n8n-style long-running operations, use:
489
+
490
+ ```bash
491
+ npx @ejazullah/browser-mcp --port 8931 --mcp-logs --no-mcp-heartbeat
492
+ ```
493
+
494
+ Use `--mcp-heartbeat` only when you explicitly want heartbeat-driven liveness checks.
495
+
496
+ ### CLI diagnostics
497
+
498
+ - Enable verbose MCP logs in terminal:
499
+
500
+ ```bash
501
+ npx @ejazullah/browser-mcp --port 8931 --mcp-logs
502
+ ```
503
+
504
+ - Disable MCP logs explicitly:
505
+
506
+ ```bash
507
+ npx @ejazullah/browser-mcp --port 8931 --no-mcp-logs
508
+ ```
509
+
474
510
  <details>
475
511
  <summary><b>Docker</b></summary>
476
512
 
@@ -128,37 +128,7 @@ async function handleSSE(serverBackendFactory, req, res, url, sessions) {
128
128
  res.statusCode = 405;
129
129
  res.end('Method not allowed');
130
130
  }
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, runHeartbeat) {
161
- const cdpSessionKey = resolveClientCdpSessionKey(req);
131
+ async function handleStreamable(serverBackendFactory, req, res, sessions, runHeartbeat) {
162
132
  const sessionId = req.headers['mcp-session-id'];
163
133
  if (sessionId) {
164
134
  const transport = sessions.get(sessionId);
@@ -168,60 +138,18 @@ async function handleStreamable(serverBackendFactory, req, res, sessions, sessio
168
138
  res.end('Session not found');
169
139
  return;
170
140
  }
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
- }
184
141
  delete req.headers['mcp-session-id'];
185
142
  testDebug(`stale http session id: ${sessionId}, creating a new session`);
186
143
  }
187
144
  else {
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);
145
+ return await transport.handleRequest(req, res);
211
146
  }
212
147
  }
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
- }
218
148
  if (req.method === 'POST') {
219
149
  const transport = new StreamableHTTPServerTransport({
220
150
  sessionIdGenerator: () => crypto.randomUUID(),
221
151
  onsessioninitialized: async (sessionId) => {
222
152
  testDebug(`create http session: ${transport.sessionId}`);
223
- if (cdpSessionKey)
224
- sessionCdpKeys.set(sessionId, cdpSessionKey);
225
153
  await mcpServer.connect(serverBackendFactory, transport, runHeartbeat);
226
154
  sessions.set(sessionId, transport);
227
155
  }
@@ -230,7 +158,6 @@ async function handleStreamable(serverBackendFactory, req, res, sessions, sessio
230
158
  if (!transport.sessionId)
231
159
  return;
232
160
  sessions.delete(transport.sessionId);
233
- sessionCdpKeys.delete(transport.sessionId);
234
161
  testDebug(`delete http session: ${transport.sessionId}`);
235
162
  };
236
163
  await transport.handleRequest(req, res);
@@ -242,7 +169,6 @@ async function handleStreamable(serverBackendFactory, req, res, sessions, sessio
242
169
  function startHttpTransport(httpServer, serverBackendFactory, auth, runHeartbeat) {
243
170
  const sseSessions = new Map();
244
171
  const streamableSessions = new Map();
245
- const streamableSessionCdpKeys = new Map();
246
172
  // Configure CORS with permissive settings for MCP tools
247
173
  const corsHandler = cors({
248
174
  origin: true, // Allow all origins
@@ -267,7 +193,7 @@ function startHttpTransport(httpServer, serverBackendFactory, auth, runHeartbeat
267
193
  if (url.pathname.startsWith('/sse'))
268
194
  await handleSSE(serverBackendFactory, req, res, url, sseSessions);
269
195
  else
270
- await handleStreamable(serverBackendFactory, req, res, streamableSessions, streamableSessionCdpKeys, runHeartbeat);
196
+ await handleStreamable(serverBackendFactory, req, res, streamableSessions, runHeartbeat);
271
197
  });
272
198
  });
273
199
  const url = httpAddressToString(httpServer.address());
package/lib/program.js CHANGED
@@ -137,5 +137,25 @@ function setupExitWatchdog() {
137
137
  process.stdin.on('close', handleExit);
138
138
  process.on('SIGINT', handleExit);
139
139
  process.on('SIGTERM', handleExit);
140
+ const isSharedWorkerError = (err) => {
141
+ const msg = err?.message || String(err || '');
142
+ return msg.includes('shared_worker targets are not supported yet') || err?.targetInfo?.type === 'shared_worker' || msg.includes('type: "shared_worker"');
143
+ };
144
+ process.on('uncaughtException', (err) => {
145
+ if (isSharedWorkerError(err)) {
146
+ console.warn('[mcp] Ignoring Playwright shared_worker assertion error to prevent crash.');
147
+ return;
148
+ }
149
+ console.error('Uncaught Exception:', err);
150
+ process.exit(1);
151
+ });
152
+ process.on('unhandledRejection', (err) => {
153
+ if (isSharedWorkerError(err)) {
154
+ console.warn('[mcp] Ignoring Playwright shared_worker assertion error to prevent crash.');
155
+ return;
156
+ }
157
+ console.error('Unhandled Rejection:', err);
158
+ process.exit(1);
159
+ });
140
160
  }
141
161
  void program.parseAsync(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ejazullah/browser-mcp",
3
- "version": "0.0.62",
3
+ "version": "0.0.64",
4
4
  "description": "@ejazullah/browser-mcp - Enhanced Playwright Tools for MCP with CDP Support",
5
5
  "type": "module",
6
6
  "repository": {