@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.
- package/lib/mcp/transport.js +77 -3
- package/lib/program.js +26 -0
- package/package.json +1 -1
package/lib/mcp/transport.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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');
|