@darbotlabs/darbot-browser-mcp 0.2.0 → 1.3.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.
Files changed (80) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +222 -161
  3. package/cli.js +1 -1
  4. package/config.d.ts +77 -1
  5. package/index.d.ts +1 -1
  6. package/index.js +1 -1
  7. package/lib/ai/context.js +150 -0
  8. package/lib/ai/guardrails.js +382 -0
  9. package/lib/ai/integration.js +397 -0
  10. package/lib/ai/intent.js +237 -0
  11. package/lib/ai/manualPromise.js +111 -0
  12. package/lib/ai/memory.js +273 -0
  13. package/lib/ai/ml-scorer.js +265 -0
  14. package/lib/ai/orchestrator-tools.js +292 -0
  15. package/lib/ai/orchestrator.js +473 -0
  16. package/lib/ai/planner.js +300 -0
  17. package/lib/ai/reporter.js +493 -0
  18. package/lib/ai/workflow.js +407 -0
  19. package/lib/auth/apiKeyAuth.js +46 -0
  20. package/lib/auth/entraAuth.js +110 -0
  21. package/lib/auth/entraJwtVerifier.js +117 -0
  22. package/lib/auth/index.js +210 -0
  23. package/lib/auth/managedIdentityAuth.js +175 -0
  24. package/lib/auth/mcpOAuthProvider.js +186 -0
  25. package/lib/auth/tunnelAuth.js +120 -0
  26. package/lib/browserContextFactory.js +1 -1
  27. package/lib/browserServer.js +1 -1
  28. package/lib/cdpRelay.js +2 -2
  29. package/lib/common.js +68 -0
  30. package/lib/config.js +62 -3
  31. package/lib/connection.js +1 -1
  32. package/lib/context.js +1 -1
  33. package/lib/fileUtils.js +1 -1
  34. package/lib/guardrails.js +382 -0
  35. package/lib/health.js +178 -0
  36. package/lib/httpServer.js +1 -1
  37. package/lib/index.js +1 -1
  38. package/lib/javascript.js +1 -1
  39. package/lib/manualPromise.js +1 -1
  40. package/lib/memory.js +273 -0
  41. package/lib/openapi.js +373 -0
  42. package/lib/orchestrator.js +473 -0
  43. package/lib/package.js +1 -1
  44. package/lib/pageSnapshot.js +17 -2
  45. package/lib/planner.js +302 -0
  46. package/lib/program.js +17 -5
  47. package/lib/reporter.js +493 -0
  48. package/lib/resources/resource.js +1 -1
  49. package/lib/server.js +5 -3
  50. package/lib/tab.js +1 -1
  51. package/lib/tools/ai-native.js +298 -0
  52. package/lib/tools/autonomous.js +147 -0
  53. package/lib/tools/clock.js +183 -0
  54. package/lib/tools/common.js +1 -1
  55. package/lib/tools/console.js +1 -1
  56. package/lib/tools/diagnostics.js +132 -0
  57. package/lib/tools/dialogs.js +1 -1
  58. package/lib/tools/emulation.js +155 -0
  59. package/lib/tools/files.js +1 -1
  60. package/lib/tools/install.js +1 -1
  61. package/lib/tools/keyboard.js +1 -1
  62. package/lib/tools/navigate.js +1 -1
  63. package/lib/tools/network.js +1 -1
  64. package/lib/tools/pageSnapshot.js +58 -0
  65. package/lib/tools/pdf.js +1 -1
  66. package/lib/tools/profiles.js +76 -25
  67. package/lib/tools/screenshot.js +1 -1
  68. package/lib/tools/scroll.js +93 -0
  69. package/lib/tools/snapshot.js +1 -1
  70. package/lib/tools/storage.js +328 -0
  71. package/lib/tools/tab.js +16 -0
  72. package/lib/tools/tabs.js +1 -1
  73. package/lib/tools/testing.js +1 -1
  74. package/lib/tools/tool.js +1 -1
  75. package/lib/tools/utils.js +1 -1
  76. package/lib/tools/vision.js +1 -1
  77. package/lib/tools/wait.js +1 -1
  78. package/lib/tools.js +22 -1
  79. package/lib/transport.js +251 -31
  80. package/package.json +28 -22
package/lib/transport.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright (c) Microsoft Corporation.
2
+ * Copyright (c) DarbotLabs.
3
3
  *
4
4
  * Licensed under the Apache License, Version 2.0 (the "License");
5
5
  * you may not use this file except in compliance with the License.
@@ -16,10 +16,15 @@
16
16
  import http from 'node:http';
17
17
  import assert from 'node:assert';
18
18
  import crypto from 'node:crypto';
19
+ import { execSync } from 'node:child_process';
19
20
  import debug from 'debug';
21
+ import express from 'express';
20
22
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
21
23
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
22
24
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
25
+ import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
26
+ import { createUnifiedAuthenticator } from './auth/index.js';
27
+ import { createMcpOAuthProvider, getOAuthConfig, isOAuthConfigured } from './auth/mcpOAuthProvider.js';
23
28
  export async function startStdioTransport(server) {
24
29
  await server.createConnection(new StdioServerTransport());
25
30
  }
@@ -56,67 +61,282 @@ async function handleSSE(server, req, res, url, sessions) {
56
61
  }
57
62
  async function handleStreamable(server, req, res, sessions) {
58
63
  const sessionId = req.headers['mcp-session-id'];
64
+ // If session ID provided, try to use existing session
59
65
  if (sessionId) {
60
- const transport = sessions.get(sessionId);
61
- if (!transport) {
62
- res.statusCode = 404;
63
- res.end('Session not found');
64
- return;
66
+ const existingTransport = sessions.get(sessionId);
67
+ if (existingTransport) {
68
+ return await existingTransport.handleRequest(req, res);
65
69
  }
66
- return await transport.handleRequest(req, res);
70
+ // Session not found (server may have restarted) - create new session for POST requests
71
+ // eslint-disable-next-line no-console
72
+ console.error(`[MCP] Session ${sessionId} not found, will create new session if POST request`);
67
73
  }
74
+ // Handle POST requests - create new session (or recreate if old session expired)
68
75
  if (req.method === 'POST') {
69
76
  const transport = new StreamableHTTPServerTransport({
70
77
  sessionIdGenerator: () => crypto.randomUUID(),
71
- onsessioninitialized: sessionId => {
72
- sessions.set(sessionId, transport);
78
+ onsessioninitialized: newSessionId => {
79
+ sessions.set(newSessionId, transport);
80
+ // eslint-disable-next-line no-console
81
+ console.error(`[MCP] New session created: ${newSessionId}`);
73
82
  }
74
83
  });
75
84
  transport.onclose = () => {
76
- if (transport.sessionId)
85
+ if (transport.sessionId) {
77
86
  sessions.delete(transport.sessionId);
87
+ // eslint-disable-next-line no-console
88
+ console.error(`[MCP] Session closed: ${transport.sessionId}`);
89
+ }
78
90
  };
79
91
  await server.createConnection(transport);
80
92
  await transport.handleRequest(req, res);
81
93
  return;
82
94
  }
83
- res.statusCode = 400;
84
- res.end('Invalid request');
95
+ // GET requests without valid session
96
+ if (req.method === 'GET') {
97
+ res.statusCode = 400;
98
+ res.setHeader('Content-Type', 'application/json');
99
+ res.end(JSON.stringify({
100
+ error: 'invalid_request',
101
+ message: 'GET requests require a valid session. Send a POST to /mcp first to initialize.',
102
+ }));
103
+ return;
104
+ }
105
+ res.statusCode = 405;
106
+ res.setHeader('Content-Type', 'application/json');
107
+ res.end(JSON.stringify({
108
+ error: 'method_not_allowed',
109
+ message: 'Use POST to send MCP messages',
110
+ }));
111
+ }
112
+ async function killProcessOnPort(port) {
113
+ const isWindows = process.platform === 'win32';
114
+ try {
115
+ if (isWindows) {
116
+ // Find and kill process on Windows
117
+ const result = execSync(`netstat -ano | findstr ":${port}"`, { encoding: 'utf8' });
118
+ const lines = result.split('\n').filter(line => line.includes('LISTENING'));
119
+ const pidsToKill = new Set();
120
+ for (const line of lines) {
121
+ const parts = line.trim().split(/\s+/);
122
+ const pid = parts[parts.length - 1];
123
+ if (pid && /^\d+$/.test(pid) && pid !== '0')
124
+ pidsToKill.add(pid);
125
+ }
126
+ for (const pid of pidsToKill) {
127
+ // eslint-disable-next-line no-console
128
+ console.error(`Killing process ${pid} using port ${port}...`);
129
+ try {
130
+ execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' });
131
+ }
132
+ catch (e) {
133
+ // Process may have already exited
134
+ if (!e.message?.includes('not found'))
135
+ throw e;
136
+ }
137
+ }
138
+ }
139
+ else {
140
+ // Find and kill process on Unix-like systems
141
+ const result = execSync(`lsof -ti:${port}`, { encoding: 'utf8' });
142
+ const pids = result.trim().split('\n').filter(Boolean);
143
+ for (const pid of pids) {
144
+ // eslint-disable-next-line no-console
145
+ console.error(`Killing process ${pid} using port ${port}...`);
146
+ try {
147
+ execSync(`kill -9 ${pid}`, { stdio: 'pipe' });
148
+ }
149
+ catch {
150
+ // Process may have already exited
151
+ }
152
+ }
153
+ }
154
+ // Wait for port to be released
155
+ await new Promise(resolve => setTimeout(resolve, 1000));
156
+ return true;
157
+ }
158
+ catch {
159
+ return false;
160
+ }
85
161
  }
86
162
  export async function startHttpServer(config) {
87
163
  const { host, port } = config;
88
- const httpServer = http.createServer();
89
- await new Promise((resolve, reject) => {
164
+ // Create Express app
165
+ const app = express();
166
+ // Trust proxy for Azure App Service
167
+ app.set('trust proxy', 1);
168
+ // CORS middleware - must come before body parsers
169
+ app.use((req, res, next) => {
170
+ res.setHeader('Access-Control-Allow-Origin', '*');
171
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE');
172
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, Mcp-Session-Id, Accept');
173
+ if (req.method === 'OPTIONS') {
174
+ res.status(200).end();
175
+ return;
176
+ }
177
+ next();
178
+ });
179
+ // Parse JSON bodies - but NOT for /mcp and /sse endpoints (MCP SDK handles its own body parsing)
180
+ app.use((req, res, next) => {
181
+ if (req.path === '/mcp' || req.path === '/sse') {
182
+ return next();
183
+ }
184
+ return express.json()(req, res, next);
185
+ });
186
+ // Setup OAuth router if configured
187
+ const oauthConfig = getOAuthConfig();
188
+ if (isOAuthConfigured() && oauthConfig) {
189
+ try {
190
+ const provider = createMcpOAuthProvider(oauthConfig);
191
+ const serverUrl = new URL(oauthConfig.serverBaseUrl);
192
+ const authRouter = mcpAuthRouter({
193
+ provider,
194
+ issuerUrl: serverUrl,
195
+ baseUrl: serverUrl,
196
+ serviceDocumentationUrl: new URL('https://github.com/AugustinMauworworworwy/darbot-browser-mcp'),
197
+ scopesSupported: ['openid', 'profile', 'email', 'User.Read'],
198
+ resourceName: 'Darbot Browser MCP',
199
+ });
200
+ // Mount OAuth router at root (handles /.well-known/*, /authorize, /token, /register)
201
+ app.use(authRouter);
202
+ // eslint-disable-next-line no-console
203
+ console.error('[OAuth] MCP OAuth router configured with Entra ID proxy');
204
+ }
205
+ catch (error) {
206
+ // eslint-disable-next-line no-console
207
+ console.error('[OAuth] Failed to setup OAuth router:', error);
208
+ }
209
+ }
210
+ // Create HTTP server from Express app
211
+ const httpServer = http.createServer(app);
212
+ const tryListen = () => new Promise((resolve, reject) => {
90
213
  httpServer.on('error', reject);
91
214
  httpServer.listen(port, host, () => {
92
215
  resolve();
93
216
  httpServer.removeListener('error', reject);
94
217
  });
95
218
  });
96
- return httpServer;
219
+ try {
220
+ await tryListen();
221
+ }
222
+ catch (error) {
223
+ if (error.code === 'EADDRINUSE' && port !== undefined) {
224
+ // eslint-disable-next-line no-console
225
+ console.error(`Port ${port} is in use. Attempting to terminate conflicting service...`);
226
+ const killed = await killProcessOnPort(port);
227
+ if (killed) {
228
+ // Retry after killing
229
+ await tryListen();
230
+ }
231
+ else {
232
+ throw new Error(`Port ${port} is already in use and could not terminate the conflicting process. Please free the port manually or use a different port.`);
233
+ }
234
+ }
235
+ else {
236
+ throw error;
237
+ }
238
+ }
239
+ return { httpServer, app };
97
240
  }
98
- export function startHttpTransport(httpServer, mcpServer) {
241
+ export function startHttpTransport(httpServer, mcpServer, app) {
99
242
  const sseSessions = new Map();
100
243
  const streamableSessions = new Map();
101
- httpServer.on('request', async (req, res) => {
244
+ // Use unified authenticator that supports multiple auth methods
245
+ const authenticator = createUnifiedAuthenticator();
246
+ // Initialize async auth providers (Managed Identity, Key Vault)
247
+ void authenticator.initialize().catch(err => {
248
+ // eslint-disable-next-line no-console
249
+ console.error('[Auth] Failed to initialize async auth providers:', err);
250
+ });
251
+ const enforceAuthIfEnabled = async (req, res) => {
252
+ // Check if any auth method is configured
253
+ if (!authenticator.isAuthEnabled())
254
+ return true;
255
+ const result = await authenticator.authenticate(req);
256
+ if (result.authenticated) {
257
+ // Attach user info to request
258
+ req.auth = result;
259
+ req.user = result.user;
260
+ return true;
261
+ }
262
+ // Return 401 with helpful error message
263
+ res.status(401).json({
264
+ error: 'unauthorized',
265
+ message: result.error || 'Valid authentication required.',
266
+ hint: 'Use Entra ID OAuth, VS Code tunnel, or Azure Managed Identity.',
267
+ });
268
+ return false;
269
+ };
270
+ // MCP Streamable HTTP endpoint - must be registered as Express route
271
+ // Cast to http types since Express extends them and MCP SDK needs the base types
272
+ app.all('/mcp', async (req, res) => {
273
+ if (!(await enforceAuthIfEnabled(req, res)))
274
+ return;
275
+ await handleStreamable(mcpServer, req, res, streamableSessions);
276
+ });
277
+ // SSE endpoint (legacy MCP transport)
278
+ app.all('/sse', async (req, res) => {
279
+ if (!(await enforceAuthIfEnabled(req, res)))
280
+ return;
102
281
  const url = new URL(`http://localhost${req.url}`);
103
- if (url.pathname.startsWith('/mcp'))
104
- await handleStreamable(mcpServer, req, res, streamableSessions);
105
- else
106
- await handleSSE(mcpServer, req, res, url, sseSessions);
282
+ await handleSSE(mcpServer, req, res, url, sseSessions);
107
283
  });
284
+ // Health check endpoints
285
+ app.get('/health', async (req, res) => {
286
+ try {
287
+ const { createHealthCheckService } = await import('./health.js');
288
+ const healthService = createHealthCheckService();
289
+ await healthService.handleHealthCheck(req, res);
290
+ }
291
+ catch {
292
+ res.status(500).send('Health check service unavailable');
293
+ }
294
+ });
295
+ app.get('/ready', async (req, res) => {
296
+ try {
297
+ const { createHealthCheckService } = await import('./health.js');
298
+ const healthService = createHealthCheckService();
299
+ await healthService.handleReadinessCheck(req, res);
300
+ }
301
+ catch {
302
+ res.status(503).send('Service unavailable');
303
+ }
304
+ });
305
+ app.get('/live', async (req, res) => {
306
+ try {
307
+ const { createHealthCheckService } = await import('./health.js');
308
+ const healthService = createHealthCheckService();
309
+ await healthService.handleLivenessCheck(req, res);
310
+ }
311
+ catch {
312
+ res.status(503).send('Service unavailable');
313
+ }
314
+ });
315
+ // OpenAPI specification endpoint
316
+ app.get(['/openapi.json', '/swagger.json'], async (req, res) => {
317
+ try {
318
+ const { createOpenAPIGenerator } = await import('./openapi.js');
319
+ const { snapshotTools } = await import('./tools.js');
320
+ const openApiGenerator = createOpenAPIGenerator(snapshotTools);
321
+ openApiGenerator.handleOpenAPISpec(req, res);
322
+ }
323
+ catch (error) {
324
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
325
+ res.status(500).json({ error: 'Failed to generate OpenAPI spec', message: errorMessage });
326
+ }
327
+ });
328
+ // Log server info
108
329
  const url = httpAddressToString(httpServer.address());
109
330
  const message = [
110
- `Listening on ${url}`,
111
- 'Put this in your client config:',
112
- JSON.stringify({
113
- 'mcpServers': {
114
- 'playwright': {
115
- 'url': `${url}/sse`
116
- }
117
- }
118
- }, undefined, 2),
119
- 'If your client supports streamable HTTP, you can use the /mcp endpoint instead.',
331
+ `Darbot Browser MCP Server listening on ${url}`,
332
+ '',
333
+ 'Available endpoints:',
334
+ ` Health Check: ${url}/health`,
335
+ ` Readiness: ${url}/ready`,
336
+ ` Liveness: ${url}/live`,
337
+ ` OpenAPI: ${url}/openapi.json`,
338
+ ` MCP: ${url}/mcp`,
339
+ ` SSE: ${url}/sse`,
120
340
  ].join('\n');
121
341
  // eslint-disable-next-line no-console
122
342
  console.error(message);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@darbotlabs/darbot-browser-mcp",
3
- "version": "0.2.0",
4
- "description": "🤖 Your Autonomous Browser Companion - 29 AI-driven browser tools with work profile support and VS Code GitHub Copilot agent mode integration",
3
+ "version": "1.3.0",
4
+ "description": "Darbot Browser - framework for 52 AI-driven browser tools with session state support and VS Code GitHub Copilot agent mode integration",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -13,7 +13,7 @@
13
13
  "browser",
14
14
  "autonomous",
15
15
  "darbot",
16
- "automation",
16
+ "automation",
17
17
  "testing",
18
18
  "screenshot",
19
19
  "copilot",
@@ -25,7 +25,7 @@
25
25
  "selenium-alternative",
26
26
  "browser-testing",
27
27
  "accessibility",
28
- "work-profiles",
28
+ "session-states",
29
29
  "session-management",
30
30
  "github-copilot",
31
31
  "model-context-protocol",
@@ -34,10 +34,10 @@
34
34
  "copilot-integration"
35
35
  ],
36
36
  "engines": {
37
- "node": ">=18"
37
+ "node": ">=23"
38
38
  },
39
39
  "author": {
40
- "name": "Microsoft Corporation"
40
+ "name": "DarbotLabs"
41
41
  },
42
42
  "license": "Apache-2.0",
43
43
  "scripts": {
@@ -45,8 +45,8 @@
45
45
  "lint": "npm run update-readme && eslint . && tsc --noEmit",
46
46
  "update-readme": "node utils/update-readme.js",
47
47
  "watch": "tsc --watch",
48
- "test": "npx @playwright/test",
49
- "test:msedge": "npx @playwright/test --project=msedge",
48
+ "test": "npx playwright test",
49
+ "test:msedge": "npx playwright test --project=msedge",
50
50
  "run-server": "node cli.js",
51
51
  "run-darbot": "node cli.js",
52
52
  "clean": "rmdir /s /q lib 2>nul || echo Clean completed",
@@ -62,31 +62,37 @@
62
62
  }
63
63
  },
64
64
  "dependencies": {
65
- "@modelcontextprotocol/sdk": "^1.11.0",
65
+ "@azure/identity": "^4.5.0",
66
+ "@azure/keyvault-secrets": "^4.9.0",
67
+ "@azure/msal-node": "^5.0.2",
68
+ "@modelcontextprotocol/sdk": "^1.25.3",
66
69
  "commander": "^13.1.0",
67
- "debug": "^4.4.1",
68
- "mime": "^4.0.7",
69
- "playwright": "1.55.0-alpha-1752540053000",
70
- "playwright-core": "1.55.0-alpha-1752540053000",
71
- "ws": "^8.18.1",
72
- "zod-to-json-schema": "^3.24.4"
70
+ "debug": "^4.4.3",
71
+ "express": "^4.21.2",
72
+ "mime": "^4.1.0",
73
+ "playwright": "^1.57.0",
74
+ "playwright-core": "^1.57.0",
75
+ "ws": "^8.19.0",
76
+ "zod": "^3.25.76",
77
+ "zod-to-json-schema": "^3.25.1"
73
78
  },
74
79
  "devDependencies": {
75
80
  "@eslint/eslintrc": "^3.2.0",
76
- "@eslint/js": "^9.19.0",
77
- "@playwright/test": "1.55.0-alpha-1752540053000",
81
+ "@eslint/js": "^9.39.2",
82
+ "@playwright/test": "^1.57.0",
78
83
  "@stylistic/eslint-plugin": "^3.0.1",
79
84
  "@types/chrome": "^0.0.315",
80
85
  "@types/debug": "^4.1.12",
81
- "@types/node": "^22.13.10",
86
+ "@types/express": "^5.0.0",
87
+ "@types/node": "^25.0.10",
82
88
  "@types/ws": "^8.18.1",
83
- "@typescript-eslint/eslint-plugin": "^8.26.1",
84
- "@typescript-eslint/parser": "^8.26.1",
89
+ "@typescript-eslint/eslint-plugin": "^8.53.1",
90
+ "@typescript-eslint/parser": "^8.53.1",
85
91
  "@typescript-eslint/utils": "^8.26.1",
86
- "eslint": "^9.19.0",
92
+ "eslint": "^9.39.2",
87
93
  "eslint-plugin-import": "^2.31.0",
88
94
  "eslint-plugin-notice": "^1.0.0",
89
- "typescript": "^5.8.2"
95
+ "typescript": "^5.9.3"
90
96
  },
91
97
  "bin": {
92
98
  "darbot-browser-mcp": "cli.js"