@agent360/browser-mcp 1.13.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.
Binary file
Binary file
Binary file
@@ -0,0 +1,40 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Agent360 Browser MCP",
4
+ "version": "1.13.0",
5
+ "description": "Lader Claude Code styre din browser — forbind tjenester, hent tokens, automatisér workflows. Multi-session support.",
6
+ "permissions": [
7
+ "tabs",
8
+ "tabGroups",
9
+ "cookies",
10
+ "scripting",
11
+ "activeTab",
12
+ "storage",
13
+ "alarms",
14
+ "offscreen",
15
+ "notifications",
16
+ "webNavigation",
17
+ "debugger"
18
+ ],
19
+ "host_permissions": [
20
+ "<all_urls>"
21
+ ],
22
+ "icons": {
23
+ "16": "icons/icon-16.png",
24
+ "48": "icons/icon-48.png",
25
+ "128": "icons/icon-128.png"
26
+ },
27
+ "content_security_policy": {
28
+ "extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' ws://127.0.0.1:* http://127.0.0.1:*"
29
+ },
30
+ "background": {
31
+ "service_worker": "background.js"
32
+ },
33
+ "action": {
34
+ "default_popup": "popup.html",
35
+ "default_icon": {
36
+ "16": "icons/icon-16.png",
37
+ "48": "icons/icon-48.png"
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,6 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <body>
4
+ <script src="offscreen.js"></script>
5
+ </body>
6
+ </html>
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Offscreen Document — Persistent WebSocket bridge (multi-session)
3
+ *
4
+ * Scans port range 9876-9885 and maintains connections to ALL active
5
+ * MCP servers. Each Claude Code session gets its own port automatically.
6
+ * Passes port ID with every command so background.js can track tab ownership.
7
+ *
8
+ * Flow: MCP Server(s) ←(WS)→ this ←(chrome.runtime.sendMessage)→ Service Worker → Chrome APIs
9
+ */
10
+
11
+ const BASE_PORT = 9876;
12
+ const MAX_PORT = 9885;
13
+ const connections = new Map(); // port → WebSocket
14
+
15
+ function scanPorts() {
16
+ for (let port = BASE_PORT; port <= MAX_PORT; port++) {
17
+ const existing = connections.get(port);
18
+ if (existing && (existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING)) {
19
+ continue;
20
+ }
21
+ tryConnect(port);
22
+ }
23
+ }
24
+
25
+ function tryConnect(port) {
26
+ let ws;
27
+ try {
28
+ ws = new WebSocket(`ws://127.0.0.1:${port}`);
29
+ } catch {
30
+ return;
31
+ }
32
+
33
+ const connectTimeout = setTimeout(() => {
34
+ if (ws.readyState !== WebSocket.OPEN) ws.close();
35
+ }, 2000);
36
+
37
+ ws.onopen = () => {
38
+ clearTimeout(connectTimeout);
39
+ connections.set(port, ws);
40
+ console.log(`[Offscreen] Connected to MCP server on port ${port} (${connections.size} total)`);
41
+ updateStatus();
42
+ };
43
+
44
+ ws.onmessage = async (event) => {
45
+ let cmd;
46
+ try { cmd = JSON.parse(event.data); } catch { return; }
47
+ const { id, method, params } = cmd;
48
+
49
+ try {
50
+ // Include port so background.js knows which session owns this command
51
+ const result = await chrome.runtime.sendMessage({
52
+ type: 'mcp_command',
53
+ port,
54
+ method,
55
+ params: params || {},
56
+ });
57
+
58
+ if (result && result.__error) {
59
+ ws.send(JSON.stringify({ id, error: result.__error }));
60
+ } else {
61
+ ws.send(JSON.stringify({ id, result }));
62
+ }
63
+ } catch (err) {
64
+ ws.send(JSON.stringify({ id, error: err.message || String(err) }));
65
+ }
66
+ };
67
+
68
+ ws.onclose = () => {
69
+ clearTimeout(connectTimeout);
70
+ if (connections.get(port) === ws) {
71
+ connections.delete(port);
72
+ console.log(`[Offscreen] Disconnected from port ${port} (${connections.size} remaining)`);
73
+ updateStatus();
74
+ // Notify background to release tabs for this session
75
+ chrome.runtime.sendMessage({ type: 'session_disconnect', port }).catch(() => {});
76
+ }
77
+ };
78
+
79
+ ws.onerror = () => {
80
+ clearTimeout(connectTimeout);
81
+ ws.close();
82
+ };
83
+ }
84
+
85
+ function updateStatus() {
86
+ const count = connections.size;
87
+ chrome.runtime.sendMessage({
88
+ type: 'ws_status',
89
+ connected: count > 0,
90
+ count,
91
+ ports: [...connections.keys()],
92
+ }).catch(() => {});
93
+ }
94
+
95
+ // Initial scan + frequent rescan for new servers
96
+ scanPorts();
97
+ setInterval(scanPorts, 2000);
@@ -0,0 +1,65 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <style>
6
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7
+ body { width: 320px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; padding: 16px; }
8
+ h1 { font-size: 13px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 6px; }
9
+ h1 span { color: #3b82f6; }
10
+ .status { display: flex; align-items: center; gap: 8px; padding: 10px; border-radius: 8px; background: #1e293b; margin-bottom: 8px; }
11
+ .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
12
+ .dot.on { background: #22c55e; box-shadow: 0 0 6px #22c55e; }
13
+ .dot.off { background: #ef4444; }
14
+ .label { font-size: 12px; }
15
+ .section-title { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: #475569; margin: 10px 0 4px; font-weight: 600; }
16
+ .sessions { display: flex; flex-direction: column; gap: 6px; }
17
+ .session-card { padding: 8px 10px; background: #1e293b; border-radius: 8px; border-left: 3px solid; }
18
+ .session-header { font-size: 12px; font-weight: 600; margin-bottom: 4px; }
19
+ .session-tabs { font-size: 10px; color: #94a3b8; }
20
+ .session-tabs div { margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
21
+ .empty { font-size: 11px; color: #475569; text-align: center; padding: 12px; background: #1e293b; border-radius: 8px; }
22
+ .log { max-height: 120px; overflow-y: auto; background: #1e293b; border-radius: 8px; padding: 6px 10px; }
23
+ .log-entry { font-size: 10px; padding: 2px 0; border-bottom: 1px solid #0f172a; display: flex; gap: 6px; align-items: baseline; }
24
+ .log-entry:last-child { border-bottom: none; }
25
+ .log-time { color: #475569; min-width: 45px; }
26
+ .log-method { font-weight: 500; }
27
+ .log-session { font-size: 9px; color: #64748b; }
28
+ .log-safe { color: #22c55e; }
29
+ .log-sensitive { color: #f59e0b; }
30
+ button { width: 100%; padding: 7px; border: none; border-radius: 6px; background: #1e293b; color: #94a3b8; font-size: 11px; cursor: pointer; margin-top: 4px; }
31
+ button:hover { background: #334155; color: #e2e8f0; }
32
+ .color-blue { border-color: #3b82f6; } .color-blue .session-header { color: #3b82f6; }
33
+ .color-green { border-color: #22c55e; } .color-green .session-header { color: #22c55e; }
34
+ .color-yellow { border-color: #eab308; } .color-yellow .session-header { color: #eab308; }
35
+ .color-red { border-color: #ef4444; } .color-red .session-header { color: #ef4444; }
36
+ .color-pink { border-color: #ec4899; } .color-pink .session-header { color: #ec4899; }
37
+ .color-purple { border-color: #a855f7; } .color-purple .session-header { color: #a855f7; }
38
+ .color-cyan { border-color: #06b6d4; } .color-cyan .session-header { color: #06b6d4; }
39
+ .color-orange { border-color: #f97316; } .color-orange .session-header { color: #f97316; }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <h1><span>Agent360</span> Browser MCP</h1>
44
+
45
+ <div class="status">
46
+ <div class="dot off" id="dot"></div>
47
+ <span class="label" id="label">Tjekker...</span>
48
+ </div>
49
+
50
+ <div class="section-title">Sessions</div>
51
+ <div id="sessions" class="sessions">
52
+ <div class="empty">Ingen aktive sessions</div>
53
+ </div>
54
+
55
+ <div class="section-title">Action Log</div>
56
+ <div class="log" id="log">
57
+ <div class="empty">Ingen actions endnu</div>
58
+ </div>
59
+
60
+ <button id="reconnect">Reconnect</button>
61
+ <button id="clearLog" style="margin-top:4px">Ryd log</button>
62
+
63
+ <script src="popup.js"></script>
64
+ </body>
65
+ </html>
@@ -0,0 +1,96 @@
1
+ // ── Status ──────────────────────────────────────────────────────────────────
2
+
3
+ chrome.storage.local.get(['mcpConnected', 'mcpCount', 'mcpPorts'], (result) => {
4
+ const connected = result.mcpConnected === true;
5
+ const count = result.mcpCount || 0;
6
+ document.getElementById('dot').className = `dot ${connected ? 'on' : 'off'}`;
7
+ document.getElementById('label').textContent = connected
8
+ ? `Forbundet til ${count} session${count > 1 ? 's' : ''}`
9
+ : 'Ikke forbundet';
10
+ });
11
+
12
+ // ── Sessions with tabs ─────────────────────────────────────────────────────
13
+
14
+ function renderSessions() {
15
+ chrome.storage.local.get({ sessions: {} }, ({ sessions }) => {
16
+ const container = document.getElementById('sessions');
17
+ const entries = Object.entries(sessions);
18
+
19
+ if (!entries.length) {
20
+ container.innerHTML = '<div class="empty">Ingen aktive sessions</div>';
21
+ return;
22
+ }
23
+
24
+ // Fetch tab info for each session
25
+ const promises = entries.map(async ([port, session]) => {
26
+ const tabInfos = [];
27
+ for (const tabId of session.tabIds || []) {
28
+ try {
29
+ const tab = await chrome.tabs.get(tabId);
30
+ const url = tab.url || '';
31
+ const display = url.length > 40 ? url.slice(0, 40) + '…' : url;
32
+ tabInfos.push(display);
33
+ } catch {}
34
+ }
35
+ return { port, session, tabInfos };
36
+ });
37
+
38
+ Promise.all(promises).then(results => {
39
+ container.innerHTML = results.map(({ port, session, tabInfos }) => {
40
+ const color = session.color || 'blue';
41
+ const tabHtml = tabInfos.length
42
+ ? tabInfos.map(u => `<div>• ${u}</div>`).join('')
43
+ : '<div>Ingen tabs</div>';
44
+ return `
45
+ <div class="session-card color-${color}">
46
+ <div class="session-header">${session.label} <span style="font-weight:normal;font-size:10px;color:#64748b">port ${port}</span></div>
47
+ <div class="session-tabs">${tabHtml}</div>
48
+ </div>`;
49
+ }).join('');
50
+ });
51
+ });
52
+ }
53
+
54
+ renderSessions();
55
+
56
+ // ── Action Log ──────────────────────────────────────────────────────────────
57
+
58
+ function renderLog() {
59
+ chrome.storage.local.get({ actionLog: [] }, ({ actionLog }) => {
60
+ const container = document.getElementById('log');
61
+ if (!actionLog.length) {
62
+ container.innerHTML = '<div class="empty">Ingen actions endnu</div>';
63
+ return;
64
+ }
65
+ container.innerHTML = actionLog.slice(0, 30).map(entry => {
66
+ const time = new Date(entry.time).toLocaleTimeString('da-DK', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
67
+ const cat = entry.category || 'safe';
68
+ const cls = cat === 'sensitive' ? 'log-sensitive' : 'log-safe';
69
+ return `<div class="log-entry"><span class="log-time">${time}</span><span class="log-method ${cls}">${entry.method}</span><span class="log-session">${entry.session || ''}</span></div>`;
70
+ }).join('');
71
+ });
72
+ }
73
+
74
+ renderLog();
75
+
76
+ // ── Buttons ─────────────────────────────────────────────────────────────────
77
+
78
+ document.getElementById('reconnect').addEventListener('click', () => {
79
+ chrome.runtime.sendMessage({ type: 'reconnect' });
80
+ document.getElementById('label').textContent = 'Reconnecting...';
81
+ setTimeout(() => {
82
+ chrome.storage.local.get(['mcpConnected', 'mcpCount'], (result) => {
83
+ const connected = result.mcpConnected === true;
84
+ const count = result.mcpCount || 0;
85
+ document.getElementById('dot').className = `dot ${connected ? 'on' : 'off'}`;
86
+ document.getElementById('label').textContent = connected
87
+ ? `Forbundet til ${count} session${count > 1 ? 's' : ''}`
88
+ : 'Ikke forbundet';
89
+ renderSessions();
90
+ });
91
+ }, 3000);
92
+ });
93
+
94
+ document.getElementById('clearLog').addEventListener('click', () => {
95
+ chrome.storage.local.set({ actionLog: [] }, renderLog);
96
+ });
package/index.js ADDED
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent360 Browser MCP Server
4
+ *
5
+ * Bridges Claude Code (stdio MCP) to Chrome Extension (WebSocket).
6
+ * Auto-selects first available port in range 9876-9885 for multi-session support.
7
+ *
8
+ * Architecture:
9
+ * Claude Code ←(stdio)→ this process ←(WS :port)→ Offscreen Doc ←(sendMessage)→ Service Worker → Chrome APIs
10
+ */
11
+
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
15
+ import { WebSocketServer } from 'ws';
16
+ import { execSync } from 'child_process';
17
+ import { dirname } from 'path';
18
+ import { fileURLToPath } from 'url';
19
+ import { TOOLS, PROVIDER_PAGES } from './tools.js';
20
+
21
+ // ── Auto-update on startup ─────────────────────────────────────────────────
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const repoDir = dirname(__dirname); // parent of mcp-server/
25
+
26
+ let extensionUpdated = false;
27
+ try {
28
+ const before = execSync('git rev-parse HEAD', { cwd: repoDir }).toString().trim();
29
+ execSync('git pull --ff-only 2>/dev/null', { cwd: repoDir, timeout: 10000 });
30
+ const after = execSync('git rev-parse HEAD', { cwd: repoDir }).toString().trim();
31
+ if (before !== after) {
32
+ extensionUpdated = true;
33
+ process.stderr.write(`[MCP] Updated to ${after.slice(0, 8)} — extension reload recommended\n`);
34
+ // Check if npm deps changed
35
+ try {
36
+ const diff = execSync(`git diff ${before} ${after} -- mcp-server/package.json`, { cwd: repoDir }).toString();
37
+ if (diff) {
38
+ execSync('npm install --silent', { cwd: `${repoDir}/mcp-server`, timeout: 30000 });
39
+ process.stderr.write('[MCP] Dependencies updated\n');
40
+ }
41
+ } catch {}
42
+ } else {
43
+ process.stderr.write('[MCP] Already up to date\n');
44
+ }
45
+ } catch (e) {
46
+ process.stderr.write(`[MCP] Auto-update skipped: ${e.message?.split('\n')[0]}\n`);
47
+ }
48
+
49
+ const BASE_PORT = 9876;
50
+ const MAX_PORT = 9885;
51
+ let extensionSocket = null;
52
+ let activePort = null;
53
+ let wss = null; // Track WSS for graceful shutdown
54
+ let cmdId = 0;
55
+ let lastActivity = Date.now();
56
+ const pending = new Map();
57
+
58
+ // ── WebSocket Server ───────────────────────────────────────────────────────
59
+
60
+ function createWSS(port = BASE_PORT) {
61
+ const server = new WebSocketServer({ host: '127.0.0.1', port });
62
+ wss = server;
63
+
64
+ server.on('error', (err) => {
65
+ if (err.code === 'EADDRINUSE') {
66
+ if (port < MAX_PORT) {
67
+ process.stderr.write(`[MCP] Port ${port} in use, trying ${port + 1}...\n`);
68
+ createWSS(port + 1);
69
+ } else {
70
+ process.stderr.write(`[MCP] All ports ${BASE_PORT}-${MAX_PORT} in use. Cannot start.\n`);
71
+ }
72
+ } else {
73
+ process.stderr.write(`[MCP] WebSocket error: ${err.message}\n`);
74
+ }
75
+ });
76
+
77
+ server.on('connection', (ws) => {
78
+ extensionSocket = ws;
79
+ process.stderr.write(`[MCP] Chrome extension connected on port ${port}\n`);
80
+
81
+ ws.on('message', (data) => {
82
+ let msg;
83
+ try { msg = JSON.parse(data.toString()); } catch { return; }
84
+ const { id, result, error } = msg;
85
+ const p = pending.get(id);
86
+ if (!p) return;
87
+ pending.delete(id);
88
+ clearTimeout(p.timer);
89
+ if (error) p.reject(new Error(error));
90
+ else p.resolve(result);
91
+ });
92
+
93
+ ws.on('close', () => {
94
+ if (extensionSocket === ws) {
95
+ extensionSocket = null;
96
+ process.stderr.write(`[MCP] Chrome extension disconnected\n`);
97
+ }
98
+ });
99
+ });
100
+
101
+ server.on('listening', () => {
102
+ activePort = port;
103
+ process.stderr.write(`[MCP] WebSocket server listening on ws://127.0.0.1:${port}\n`);
104
+ });
105
+
106
+ // Heartbeat + idle timeout (4 hours)
107
+ setInterval(() => {
108
+ if (extensionSocket && extensionSocket.readyState === 1) {
109
+ extensionSocket.ping();
110
+ }
111
+ if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) {
112
+ process.stderr.write('[MCP] Idle timeout (4h) — shutting down\n');
113
+ process.exit(0);
114
+ }
115
+ }, 20000);
116
+ }
117
+
118
+ createWSS();
119
+
120
+ // ── Send command to extension ───────────────────────────────────────────────
121
+
122
+ function sendToExtension(method, params = {}, timeoutMs = 30000) {
123
+ return new Promise((resolve, reject) => {
124
+ if (!extensionSocket || extensionSocket.readyState !== 1) {
125
+ reject(new Error('Chrome extension not connected. Open Chrome and ensure Agent360 Browser MCP extension is installed.'));
126
+ return;
127
+ }
128
+ const id = ++cmdId;
129
+ const timer = setTimeout(() => {
130
+ pending.delete(id);
131
+ reject(new Error(`Command timed out after ${timeoutMs}ms: ${method}`));
132
+ }, timeoutMs);
133
+ pending.set(id, { resolve, reject, timer });
134
+ extensionSocket.send(JSON.stringify({ id, method, params }));
135
+ });
136
+ }
137
+
138
+ // ── MCP Server ──────────────────────────────────────────────────────────────
139
+
140
+ const INSTRUCTIONS = `You control the user's real Chrome browser via this MCP server. Each session gets its own color-coded Chrome Tab Group.
141
+
142
+ ## Key behaviors
143
+ - **Always use browser_ask_user** when you need credentials, 2FA codes, CAPTCHA help, or any user input. Never guess passwords or tokens.
144
+ - **ALWAYS close tabs when done** with browser_close_tab after completing each task. Don't leave tabs open — close them immediately after extracting the data you need. Use browser_list_tabs to find and close all session tabs when a task is complete.
145
+ - **Check existing tabs first** with browser_list_tabs before navigating — reuse tabs instead of opening duplicates.
146
+ - **One task per tab** — navigate to a URL, do your work, then close or move on.
147
+ - **Tell the user what you're doing** in the browser. "I'm navigating to Stripe to find the API key" not just silently calling tools.
148
+
149
+ ## Tab management
150
+ - navigate creates tabs in your session's tab group (visible in Chrome as colored groups)
151
+ - list_tabs only shows YOUR session's tabs — other Claude sessions have their own
152
+ - switch_tab lets you jump between your tabs
153
+ - close_tab cleans up when you're done
154
+
155
+ ## Authentication flows
156
+ 1. Navigate to login page
157
+ 2. Use browser_ask_user with fields for email/password
158
+ 3. Fill credentials with browser_fill
159
+ 4. Click submit with browser_click
160
+ 5. If 2FA required, use browser_ask_user again: "Please enter the 2FA code shown in your authenticator app"
161
+ 6. After success, extract what you need with browser_get_page_content
162
+
163
+ ## Screenshots
164
+ - browser_screenshot captures the visible tab — useful for visual verification
165
+ - The tab is auto-activated before capture, so it always shows the right page
166
+
167
+ ## Text-based selectors (preferred for dynamic sites)
168
+ - browser_click("text=Get started") — clicks any element containing "Get started"
169
+ - browser_click("button:text(Submit)") — clicks a button containing "Submit"
170
+ - browser_fill("text=Email", "user@example.com") — fills input near "Email" label
171
+ - browser_wait("text=Success") — waits for text to appear
172
+ - These work on ALL sites including Google Cloud, Stripe, Slack (CSP-strict)
173
+
174
+ ## Keyboard
175
+ - browser_press_key("Enter") — submit forms
176
+ - browser_press_key("Tab") — navigate between fields
177
+ - browser_press_key("Escape") — close dialogs
178
+ - browser_press_key("ArrowDown") — navigate dropdowns
179
+ - browser_press_key("a", ctrl=true) — select all
180
+
181
+ ## CAPTCHA handling
182
+ - After navigate, check the response for captcha_detected field
183
+ - If captcha_detected is set, use browser_ask_user: "A CAPTCHA was detected on this page. Please solve it in the browser, then click Done."
184
+ - After user solves it, retry the navigation or continue with the page
185
+
186
+ ## OAuth popups
187
+ - OAuth popups (Google, Microsoft, GitHub, Slack, HubSpot) are automatically intercepted and added to your session's tab group
188
+ - Use browser_get_new_tab to access them, or they'll become your active tab automatically
189
+
190
+ ## Shadow DOM (Shopify, Salesforce, etc.)
191
+ - CSS selectors automatically search inside shadow DOM
192
+ - If a standard selector fails, the extension recursively searches shadow roots
193
+ - Text-based selectors ("text=Submit") also traverse shadow DOM
194
+
195
+ ## When things fail
196
+ - Element not found → try text-based selector instead of CSS
197
+ - Screenshot fails → debugger fallback is automatic
198
+ - Click doesn't work on SPA → debugger mouse events are used automatically
199
+ - CAPTCHA blocks page → use browser_ask_user, let human solve it
200
+
201
+ ## Extension updates
202
+ The MCP server auto-pulls the latest code from git on every new session startup.
203
+ If the extension files were updated, ask the user to reload it:
204
+ "The Browser MCP extension was updated. Please go to chrome://extensions, find 'Agent360 Browser MCP', and click the reload icon (🔄) to apply the update."
205
+ You cannot navigate to chrome:// pages — the user must do this manually.`;
206
+
207
+ const mcpServer = new Server(
208
+ { name: 'agent360-browser', version: '1.12.0' },
209
+ { capabilities: { tools: {} } },
210
+ { instructions: INSTRUCTIONS },
211
+ );
212
+
213
+ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
214
+ tools: TOOLS,
215
+ }));
216
+
217
+ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
218
+ const { name, arguments: args } = request.params;
219
+ lastActivity = Date.now();
220
+
221
+ try {
222
+ const methodMap = {
223
+ browser_navigate: 'navigate',
224
+ browser_get_page_content: 'get_page_content',
225
+ browser_screenshot: 'screenshot',
226
+ browser_execute_script: 'execute_script',
227
+ browser_click: 'click',
228
+ browser_fill: 'fill',
229
+ browser_wait: 'wait',
230
+ browser_press_key: 'press_key',
231
+ browser_scroll: 'scroll',
232
+ browser_hover: 'hover',
233
+ browser_fetch: 'fetch',
234
+ browser_select_option: 'select_option',
235
+ browser_handle_dialog: 'handle_dialog',
236
+ browser_wait_for_network: 'wait_for_network',
237
+ browser_list_tabs: 'list_tabs',
238
+ browser_get_cookies: 'get_cookies',
239
+ browser_get_local_storage: 'get_local_storage',
240
+ browser_ask_user: 'ask_user',
241
+ browser_select_frame: 'select_frame',
242
+ browser_list_frames: 'list_frames',
243
+ browser_get_new_tab: 'get_new_tab',
244
+ browser_switch_tab: 'switch_tab',
245
+ browser_close_tab: 'close_tab',
246
+ };
247
+
248
+ if (name === 'browser_extract_token') {
249
+ return await handleExtractToken(args);
250
+ }
251
+
252
+ const method = methodMap[name];
253
+ if (!method) {
254
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
255
+ }
256
+
257
+ const timeout = method === 'ask_user' ? (args?.timeout || 120000) + 5000 : 30000;
258
+ const result = await sendToExtension(method, args || {}, timeout);
259
+
260
+ if (name === 'browser_screenshot' && result?.image) {
261
+ const isJpeg = result.image.startsWith('data:image/jpeg');
262
+ const prefix = isJpeg ? /^data:image\/jpeg;base64,/ : /^data:image\/png;base64,/;
263
+ const mimeType = isJpeg ? 'image/jpeg' : 'image/png';
264
+ const base64 = result.image.replace(prefix, '');
265
+ return { content: [{ type: 'image', data: base64, mimeType }] };
266
+ }
267
+
268
+ const response = {
269
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
270
+ };
271
+
272
+ // Notify on first call if extension was updated
273
+ if (extensionUpdated) {
274
+ extensionUpdated = false;
275
+ response.content.push({
276
+ type: 'text',
277
+ text: '\n⚠️ Extension was updated on startup. Ask the user to reload the extension in chrome://extensions (click 🔄 on Agent360 Browser MCP).',
278
+ });
279
+ }
280
+
281
+ return response;
282
+ } catch (err) {
283
+ return {
284
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
285
+ isError: true,
286
+ };
287
+ }
288
+ });
289
+
290
+ async function handleExtractToken(args) {
291
+ const { provider, store_in_vault } = args;
292
+ const info = PROVIDER_PAGES[provider];
293
+
294
+ if (!info) {
295
+ return {
296
+ content: [{
297
+ type: 'text',
298
+ text: `Unknown provider: ${provider}. Known: ${Object.keys(PROVIDER_PAGES).join(', ')}\n\nYou can still use browser_navigate + browser_get_page_content to extract tokens from any provider manually.`,
299
+ }],
300
+ };
301
+ }
302
+
303
+ const nav = await sendToExtension('navigate', { url: info.url });
304
+ const content = [
305
+ { type: 'text', text: `Navigated to ${info.url} (${nav.title})\n\nInstructions: ${info.instructions}\n\nUse browser_get_page_content or browser_screenshot to find the token, then use browser_execute_script to extract it.` },
306
+ ];
307
+
308
+ if (store_in_vault) {
309
+ content.push({
310
+ type: 'text',
311
+ text: `\nWhen you have the token, POST it to the vault:\ncurl -X POST http://localhost:8000/v1/vault/connect -H "Authorization: Bearer {jwt}" -d '{"provider":"${provider}","token":"{extracted_token}"}'`,
312
+ });
313
+ }
314
+
315
+ return { content };
316
+ }
317
+
318
+ // ── Start ───────────────────────────────────────────────────────────────────
319
+
320
+ // Clean shutdown — release port so next session can use it
321
+ process.on('SIGTERM', () => process.exit(0));
322
+ process.on('SIGINT', () => process.exit(0));
323
+ process.on('exit', () => {
324
+ if (wss) try { wss.close(); } catch {}
325
+ if (extensionSocket) try { extensionSocket.close(); } catch {}
326
+ });
327
+
328
+ // Detect Claude Code exit (stdin closes when conversation ends)
329
+ process.stdin.on('end', () => {
330
+ process.stderr.write('[MCP] stdin closed — shutting down\n');
331
+ process.exit(0);
332
+ });
333
+
334
+ const transport = new StdioServerTransport();
335
+ await mcpServer.connect(transport);
336
+ process.stderr.write(`[MCP] Agent360 Browser MCP server running (stdio)\n`);