@ejazullah/browser-mcp 0.0.62 → 0.0.63
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 +36 -0
- package/lib/mcp/transport.js +3 -77
- package/package.json +1 -1
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
|
|
package/lib/mcp/transport.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
196
|
+
await handleStreamable(serverBackendFactory, req, res, streamableSessions, runHeartbeat);
|
|
271
197
|
});
|
|
272
198
|
});
|
|
273
199
|
const url = httpAddressToString(httpServer.address());
|