sessionmcp 0.2.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.
- package/README.md +253 -0
- package/index.js +476 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# SessionMCP Relay
|
|
2
|
+
|
|
3
|
+
> Universal MCP browser bridge — connect Claude Code, Cursor, VS Code Copilot, Windsurf, and Cline to your **existing** Chrome session.
|
|
4
|
+
> No new browser window. Your logins, your cookies, your tabs.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## How It Works
|
|
9
|
+
|
|
10
|
+
SessionMCP has two parts that work together:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
AI Tool (Claude Code / Cursor / etc.)
|
|
14
|
+
↓ MCP stdio (JSON-RPC 2.0)
|
|
15
|
+
relay/index.js ←→ ws://127.0.0.1:7337
|
|
16
|
+
↕ WebSocket + token auth
|
|
17
|
+
Chrome Extension (SessionMCP)
|
|
18
|
+
↕ chrome.debugger API
|
|
19
|
+
Your Chrome Browser
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
1. **Chrome Extension** — installs into Chrome, holds your session
|
|
23
|
+
2. **Relay** (this folder) — runs locally, bridges MCP ↔ extension via WebSocket
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Prerequisites
|
|
28
|
+
|
|
29
|
+
- **Node.js 18+** — [nodejs.org](https://nodejs.org)
|
|
30
|
+
- **Chrome** with the **SessionMCP extension** installed and connected (badge shows ✓)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
### Option A — npm (recommended, global)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g sessionmcp
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Option B — run directly from this folder
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cd relay
|
|
46
|
+
npm install
|
|
47
|
+
node index.js
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Get Your Token
|
|
53
|
+
|
|
54
|
+
1. Click the **SessionMCP** icon in Chrome's toolbar
|
|
55
|
+
2. Click **Setup Wizard**
|
|
56
|
+
3. Copy your token from Step 2
|
|
57
|
+
|
|
58
|
+
The token is generated once on first install and persists. It authenticates the relay to your extension.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Setup by AI Tool
|
|
63
|
+
|
|
64
|
+
### Claude Code
|
|
65
|
+
|
|
66
|
+
Config file: `~/.claude/claude_mcp_config.json`
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"sessionmcp": {
|
|
72
|
+
"command": "sessionmcp",
|
|
73
|
+
"args": [],
|
|
74
|
+
"env": {
|
|
75
|
+
"SESSIONMCP_TOKEN": "YOUR_TOKEN_HERE"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or if running directly from source:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"sessionmcp": {
|
|
88
|
+
"command": "node",
|
|
89
|
+
"args": ["/absolute/path/to/sessionmcp/relay/index.js"],
|
|
90
|
+
"env": {
|
|
91
|
+
"SESSIONMCP_TOKEN": "YOUR_TOKEN_HERE"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Restart Claude Code. Verify with: `claude mcp list`
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### Cursor
|
|
103
|
+
|
|
104
|
+
Config file: `~/.cursor/mcp.json`
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"mcpServers": {
|
|
109
|
+
"sessionmcp": {
|
|
110
|
+
"command": "sessionmcp",
|
|
111
|
+
"args": [],
|
|
112
|
+
"env": {
|
|
113
|
+
"SESSIONMCP_TOKEN": "YOUR_TOKEN_HERE"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Restart Cursor. The MCP server appears in **Settings → MCP**.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### Windsurf (Codeium)
|
|
125
|
+
|
|
126
|
+
Config file: `~/.codeium/windsurf/mcp_config.json`
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"mcpServers": {
|
|
131
|
+
"sessionmcp": {
|
|
132
|
+
"command": "sessionmcp",
|
|
133
|
+
"args": [],
|
|
134
|
+
"env": {
|
|
135
|
+
"SESSIONMCP_TOKEN": "YOUR_TOKEN_HERE"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Restart Windsurf. Check **Cascade → MCP** panel to confirm connected.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### Cline / RooCode (VS Code extension)
|
|
147
|
+
|
|
148
|
+
1. Open VS Code
|
|
149
|
+
2. Go to Cline extension sidebar → **MCP Servers** tab
|
|
150
|
+
3. Click **Add Server** and use:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"sessionmcp": {
|
|
155
|
+
"command": "sessionmcp",
|
|
156
|
+
"args": [],
|
|
157
|
+
"env": {
|
|
158
|
+
"SESSIONMCP_TOKEN": "YOUR_TOKEN_HERE"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Or edit `~/.vscode/cline_mcp_settings.json` directly (path varies by OS).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### VS Code Copilot (GitHub Copilot)
|
|
169
|
+
|
|
170
|
+
Config file: `.vscode/mcp.json` in your workspace, or user settings.
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"servers": {
|
|
175
|
+
"sessionmcp": {
|
|
176
|
+
"type": "stdio",
|
|
177
|
+
"command": "sessionmcp",
|
|
178
|
+
"args": [],
|
|
179
|
+
"env": {
|
|
180
|
+
"SESSIONMCP_TOKEN": "YOUR_TOKEN_HERE"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Enable via **Command Palette → MCP: List Servers**.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Available Tools
|
|
192
|
+
|
|
193
|
+
| Tool | Description |
|
|
194
|
+
|---|---|
|
|
195
|
+
| `tabs_context` | List all open Chrome tabs with tabId, title, URL, active state |
|
|
196
|
+
| `navigate` | Navigate a tab to a URL and wait for load |
|
|
197
|
+
| `screenshot` | Take a screenshot of a tab — returns base64 PNG |
|
|
198
|
+
| `click` | Click at x,y coordinates on a tab |
|
|
199
|
+
| `type_text` | Type text into the focused element |
|
|
200
|
+
| `evaluate` | Execute JavaScript in a tab and return the result |
|
|
201
|
+
| `get_page_text` | Get the full visible text content of a page |
|
|
202
|
+
| `scroll` | Scroll a tab by x/y pixels |
|
|
203
|
+
| `get_console_logs` | Retrieve console messages from a tab |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Known Limitations
|
|
208
|
+
|
|
209
|
+
| Limitation | Details |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `chrome://` pages | Chrome blocks debugger access to its own internal pages (`chrome://extensions`, `chrome://settings`, etc.). screenshot, evaluate, click and all CDP tools will return an error on these URLs. Use a regular web page tab instead. |
|
|
212
|
+
| `file://` pages | Local HTML files opened via `file://` also block debugger access by default. |
|
|
213
|
+
| Extension pages | `chrome-extension://` URLs are blocked the same way. |
|
|
214
|
+
| PDFs opened in Chrome | Chrome's built-in PDF viewer blocks CDP commands. |
|
|
215
|
+
| Multiple relay instances | Running two AI tool terminals simultaneously causes port 7337 conflict — second relay loses WebSocket connection. Fix coming in v0.2.0. |
|
|
216
|
+
| Multiple Chrome windows | `tabs_context` returns one active tab per window. If you have 2+ Chrome windows open, it's ambiguous which tab the user is actually looking at. The focused/foreground window is not indicated. Fix coming in v0.2.0 — `tabs_context` will include a `windowFocused` field. |
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Troubleshooting
|
|
221
|
+
|
|
222
|
+
**Extension badge shows ✗ (disconnected)**
|
|
223
|
+
- Is the relay running? Start it: `sessionmcp` or `node index.js`
|
|
224
|
+
- Check the port isn't blocked: `ws://127.0.0.1:7337`
|
|
225
|
+
- Reload the extension in `chrome://extensions`
|
|
226
|
+
|
|
227
|
+
**"Tool not found" in AI tool**
|
|
228
|
+
- Verify token in `env.SESSIONMCP_TOKEN` matches the extension popup
|
|
229
|
+
- Restart your AI tool after adding the MCP config
|
|
230
|
+
|
|
231
|
+
**Screenshot returns empty**
|
|
232
|
+
- The debugger permission requires user confirmation on some Chrome versions
|
|
233
|
+
- Click "Allow" if Chrome prompts for debugger access
|
|
234
|
+
|
|
235
|
+
**Windows path issues**
|
|
236
|
+
- Use forward slashes or escaped backslashes in JSON config paths
|
|
237
|
+
- Prefer the global `npm install -g sessionmcp` to avoid path problems
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Security
|
|
242
|
+
|
|
243
|
+
- Token is 32-byte random (44-char base64url), generated once on install
|
|
244
|
+
- Relay only accepts WebSocket connections with a valid token header
|
|
245
|
+
- All traffic stays on `127.0.0.1` — nothing leaves your machine
|
|
246
|
+
- No cloud, no telemetry, no accounts
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT — © 2026 [Limitless GMTech Solutions, Lda](https://www.limitless-gmtech.com/)
|
|
253
|
+
Built by [Ginquel Moreira](https://www.linkedin.com/in/ginquel-moreira/)
|
package/index.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) 2026 Limitless GMTech Solutions, Lda. All rights reserved.
|
|
3
|
+
// Proprietary and confidential. Unauthorized copying or use is prohibited.
|
|
4
|
+
// See LICENSE file for full terms.
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { WebSocketServer, WebSocket } = require('ws');
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
|
|
10
|
+
const PORT = parseInt(process.env.SESSIONMCP_PORT || '7337', 10);
|
|
11
|
+
const TOKEN = process.env.SESSIONMCP_TOKEN || '';
|
|
12
|
+
|
|
13
|
+
// ── STATE ───────────────────────────────────────────────────────────────────
|
|
14
|
+
let isPrimary = false;
|
|
15
|
+
|
|
16
|
+
// Primary-only
|
|
17
|
+
let extensionSocket = null;
|
|
18
|
+
const relayPeers = new Set();
|
|
19
|
+
|
|
20
|
+
// Secondary-only
|
|
21
|
+
let primarySocket = null;
|
|
22
|
+
|
|
23
|
+
// Shared: in-flight requests waiting for a response
|
|
24
|
+
const pendingRequests = new Map(); // id → { resolve, reject, timer }
|
|
25
|
+
let nextId = 1;
|
|
26
|
+
|
|
27
|
+
// ── PRIMARY: WS SERVER ──────────────────────────────────────────────────────
|
|
28
|
+
function startServer() {
|
|
29
|
+
const wss = new WebSocketServer({ port: PORT, host: '127.0.0.1' });
|
|
30
|
+
|
|
31
|
+
wss.once('listening', () => {
|
|
32
|
+
isPrimary = true;
|
|
33
|
+
process.stderr.write(`[SessionMCP Relay] PRIMARY — listening on ws://127.0.0.1:${PORT}\n`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
wss.once('error', (err) => {
|
|
37
|
+
wss.close();
|
|
38
|
+
if (err.code === 'EADDRINUSE') {
|
|
39
|
+
process.stderr.write(`[SessionMCP Relay] SECONDARY — port ${PORT} in use, connecting to primary\n`);
|
|
40
|
+
connectToPrimary();
|
|
41
|
+
} else {
|
|
42
|
+
process.stderr.write(`[SessionMCP Relay] WS server error: ${err.message}\n`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
wss.on('connection', (socket, req) => {
|
|
47
|
+
const url = new URL(req.url, `http://127.0.0.1:${PORT}`);
|
|
48
|
+
|
|
49
|
+
// Token auth
|
|
50
|
+
if (TOKEN && url.searchParams.get('token') !== TOKEN) {
|
|
51
|
+
socket.close(4001, 'Invalid token');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (url.searchParams.get('relay') === '1') {
|
|
56
|
+
handleRelayPeer(socket);
|
|
57
|
+
} else {
|
|
58
|
+
handleExtension(socket);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handleExtension(socket) {
|
|
64
|
+
extensionSocket = socket;
|
|
65
|
+
process.stderr.write('[SessionMCP Relay] Extension connected\n');
|
|
66
|
+
|
|
67
|
+
socket.on('message', (data) => {
|
|
68
|
+
try {
|
|
69
|
+
const msg = JSON.parse(data.toString());
|
|
70
|
+
if (msg.type === 'ping') return;
|
|
71
|
+
resolveRequest(msg);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
process.stderr.write(`[SessionMCP Relay] Bad message from extension: ${e}\n`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
socket.on('close', () => {
|
|
78
|
+
if (extensionSocket === socket) extensionSocket = null;
|
|
79
|
+
process.stderr.write('[SessionMCP Relay] Extension disconnected\n');
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function handleRelayPeer(socket) {
|
|
84
|
+
relayPeers.add(socket);
|
|
85
|
+
process.stderr.write(`[SessionMCP Relay] Secondary relay connected (${relayPeers.size} peers)\n`);
|
|
86
|
+
|
|
87
|
+
socket.on('message', async (data) => {
|
|
88
|
+
try {
|
|
89
|
+
const { id, method, params } = JSON.parse(data.toString());
|
|
90
|
+
try {
|
|
91
|
+
const result = await sendToExtension(method, params);
|
|
92
|
+
socket.send(JSON.stringify({ id, result }));
|
|
93
|
+
} catch (err) {
|
|
94
|
+
socket.send(JSON.stringify({ id, error: err.message }));
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
process.stderr.write(`[SessionMCP Relay] Bad message from peer: ${e}\n`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
socket.on('close', () => {
|
|
102
|
+
relayPeers.delete(socket);
|
|
103
|
+
process.stderr.write(`[SessionMCP Relay] Secondary relay disconnected (${relayPeers.size} peers)\n`);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── SECONDARY: CONNECT TO PRIMARY ───────────────────────────────────────────
|
|
108
|
+
function connectToPrimary() {
|
|
109
|
+
const url = `ws://127.0.0.1:${PORT}${TOKEN ? `?token=${TOKEN}&relay=1` : '?relay=1'}`;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
primarySocket = new WebSocket(url);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
primarySocket = null;
|
|
115
|
+
scheduleBecomePrimary();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
primarySocket.on('open', () => {
|
|
120
|
+
process.stderr.write('[SessionMCP Relay] Connected to primary relay\n');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
primarySocket.on('message', (data) => {
|
|
124
|
+
try {
|
|
125
|
+
const msg = JSON.parse(data.toString());
|
|
126
|
+
resolveRequest(msg);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
process.stderr.write(`[SessionMCP Relay] Bad message from primary: ${e}\n`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
primarySocket.on('close', () => {
|
|
133
|
+
primarySocket = null;
|
|
134
|
+
process.stderr.write('[SessionMCP Relay] Primary relay gone — attempting takeover\n');
|
|
135
|
+
scheduleBecomePrimary();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
primarySocket.on('error', () => {
|
|
139
|
+
primarySocket = null;
|
|
140
|
+
scheduleBecomePrimary();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Jitter prevents thundering herd when multiple secondaries race to become primary
|
|
145
|
+
function scheduleBecomePrimary() {
|
|
146
|
+
const delay = 200 + Math.floor(Math.random() * 400);
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
isPrimary = false;
|
|
149
|
+
startServer();
|
|
150
|
+
}, delay);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── SHARED: REQUEST RESOLUTION ───────────────────────────────────────────────
|
|
154
|
+
function resolveRequest(msg) {
|
|
155
|
+
if (!msg.id || !pendingRequests.has(msg.id)) return;
|
|
156
|
+
const { resolve, reject, timer } = pendingRequests.get(msg.id);
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
pendingRequests.delete(msg.id);
|
|
159
|
+
if (msg.error) reject(new Error(msg.error));
|
|
160
|
+
else resolve(msg.result);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── SHARED: SEND TOOL CALL ───────────────────────────────────────────────────
|
|
164
|
+
function sendToExtension(method, params) {
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
let target;
|
|
167
|
+
|
|
168
|
+
if (isPrimary) {
|
|
169
|
+
if (!extensionSocket || extensionSocket.readyState !== WebSocket.OPEN) {
|
|
170
|
+
reject(new Error('SessionMCP extension not connected. Is Chrome open with the SessionMCP extension installed?'));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
target = extensionSocket;
|
|
174
|
+
} else {
|
|
175
|
+
if (!primarySocket || primarySocket.readyState !== WebSocket.OPEN) {
|
|
176
|
+
reject(new Error('SessionMCP not connected to primary relay. Retry in a moment.'));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
target = primarySocket;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const id = nextId++;
|
|
183
|
+
const timer = setTimeout(() => {
|
|
184
|
+
if (pendingRequests.has(id)) {
|
|
185
|
+
pendingRequests.delete(id);
|
|
186
|
+
reject(new Error(`Extension command timed out after 30s`));
|
|
187
|
+
}
|
|
188
|
+
}, 30000);
|
|
189
|
+
|
|
190
|
+
pendingRequests.set(id, { resolve, reject, timer });
|
|
191
|
+
target.send(JSON.stringify({ id, method, params }));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── MCP STDIO SERVER ─────────────────────────────────────────────────────────
|
|
196
|
+
const TOOLS = [
|
|
197
|
+
{
|
|
198
|
+
name: 'tabs_context',
|
|
199
|
+
description: 'List all open Chrome tabs. Call this first to get tabId values for other tools.',
|
|
200
|
+
inputSchema: { type: 'object', properties: {}, required: [] }
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'navigate',
|
|
204
|
+
description: 'Navigate a Chrome tab to a URL.',
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
tabId: { type: 'number', description: 'Tab ID from tabs_context' },
|
|
209
|
+
url: { type: 'string', description: 'URL to navigate to' }
|
|
210
|
+
},
|
|
211
|
+
required: ['tabId', 'url']
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'screenshot',
|
|
216
|
+
description: 'Take a screenshot of a Chrome tab. Returns base64 PNG. Optionally clip to a specific region.',
|
|
217
|
+
inputSchema: {
|
|
218
|
+
type: 'object',
|
|
219
|
+
properties: {
|
|
220
|
+
tabId: { type: 'number', description: 'Tab ID from tabs_context' },
|
|
221
|
+
clip: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
description: 'Optional crop region in CSS pixels',
|
|
224
|
+
properties: {
|
|
225
|
+
x: { type: 'number' },
|
|
226
|
+
y: { type: 'number' },
|
|
227
|
+
width: { type: 'number' },
|
|
228
|
+
height: { type: 'number' }
|
|
229
|
+
},
|
|
230
|
+
required: ['x', 'y', 'width', 'height']
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
required: ['tabId']
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'click',
|
|
238
|
+
description: 'Click at coordinates in a Chrome tab.',
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
tabId: { type: 'number' },
|
|
243
|
+
x: { type: 'number', description: 'X coordinate in CSS pixels' },
|
|
244
|
+
y: { type: 'number', description: 'Y coordinate in CSS pixels' }
|
|
245
|
+
},
|
|
246
|
+
required: ['tabId', 'x', 'y']
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'type_text',
|
|
251
|
+
description: 'Type text into the focused element in a Chrome tab.',
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: 'object',
|
|
254
|
+
properties: {
|
|
255
|
+
tabId: { type: 'number' },
|
|
256
|
+
text: { type: 'string', description: 'Text to type' }
|
|
257
|
+
},
|
|
258
|
+
required: ['tabId', 'text']
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'evaluate',
|
|
263
|
+
description: 'Execute JavaScript in a Chrome tab and return the result.',
|
|
264
|
+
inputSchema: {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: {
|
|
267
|
+
tabId: { type: 'number' },
|
|
268
|
+
expression: { type: 'string', description: 'JavaScript expression to evaluate. No return statements.' }
|
|
269
|
+
},
|
|
270
|
+
required: ['tabId', 'expression']
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'get_page_text',
|
|
275
|
+
description: 'Get the visible text content of a Chrome tab.',
|
|
276
|
+
inputSchema: {
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: {
|
|
279
|
+
tabId: { type: 'number' }
|
|
280
|
+
},
|
|
281
|
+
required: ['tabId']
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'scroll',
|
|
286
|
+
description: 'Scroll a Chrome tab by pixels.',
|
|
287
|
+
inputSchema: {
|
|
288
|
+
type: 'object',
|
|
289
|
+
properties: {
|
|
290
|
+
tabId: { type: 'number' },
|
|
291
|
+
deltaY: { type: 'number', description: 'Pixels to scroll (positive = down, negative = up)' },
|
|
292
|
+
x: { type: 'number', description: 'X coordinate (default: center)' },
|
|
293
|
+
y: { type: 'number', description: 'Y coordinate (default: center)' }
|
|
294
|
+
},
|
|
295
|
+
required: ['tabId', 'deltaY']
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'get_console_logs',
|
|
300
|
+
description: 'Get browser console log messages from a Chrome tab.',
|
|
301
|
+
inputSchema: {
|
|
302
|
+
type: 'object',
|
|
303
|
+
properties: {
|
|
304
|
+
tabId: { type: 'number' },
|
|
305
|
+
limit: { type: 'number', description: 'Max messages to return (default: 50)' }
|
|
306
|
+
},
|
|
307
|
+
required: ['tabId']
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'press_key',
|
|
312
|
+
description: 'Press a keyboard key or shortcut in a Chrome tab. Supports named keys (Enter, Tab, Escape, Backspace, Delete, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End, PageUp, PageDown, F5, Space) and modifier combos like Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+Z, Shift+Tab.',
|
|
313
|
+
inputSchema: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
properties: {
|
|
316
|
+
tabId: { type: 'number' },
|
|
317
|
+
key: { type: 'string', description: 'Key name or combo, e.g. "Enter", "Tab", "Escape", "Ctrl+A", "Shift+Tab"' }
|
|
318
|
+
},
|
|
319
|
+
required: ['tabId', 'key']
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: 'click_element',
|
|
324
|
+
description: 'Click a page element by CSS selector. More reliable than click() with coordinates — finds the element, computes its center, and clicks it. Use get_dom_snapshot first to discover selectors.',
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: 'object',
|
|
327
|
+
properties: {
|
|
328
|
+
tabId: { type: 'number' },
|
|
329
|
+
selector: { type: 'string', description: 'CSS selector, e.g. "#submit-btn", "button[type=submit]", ".nav-link:first-child"' }
|
|
330
|
+
},
|
|
331
|
+
required: ['tabId', 'selector']
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'get_dom_snapshot',
|
|
336
|
+
description: 'Get a structured map of all interactive elements on the page (buttons, inputs, links, selects) with their text, CSS selectors, and x/y coordinates. Call this before click_element to discover what is on the page.',
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: 'object',
|
|
339
|
+
properties: {
|
|
340
|
+
tabId: { type: 'number' }
|
|
341
|
+
},
|
|
342
|
+
required: ['tabId']
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: 'wait_for',
|
|
347
|
+
description: 'Wait until a condition is met in a Chrome tab. Polls every 300ms up to timeout. Use selector to wait for an element to appear, selector_hidden for it to disappear, text to wait for text, or network_idle to wait for requests to settle.',
|
|
348
|
+
inputSchema: {
|
|
349
|
+
type: 'object',
|
|
350
|
+
properties: {
|
|
351
|
+
tabId: { type: 'number' },
|
|
352
|
+
selector: { type: 'string', description: 'CSS selector — wait until visible' },
|
|
353
|
+
selector_hidden: { type: 'string', description: 'CSS selector — wait until gone/hidden' },
|
|
354
|
+
text: { type: 'string', description: 'Text — wait until present in body' },
|
|
355
|
+
network_idle: { type: 'boolean', description: 'Wait for network activity to settle (500ms heuristic)' },
|
|
356
|
+
timeout: { type: 'number', description: 'Max wait in ms (default 10000)' }
|
|
357
|
+
},
|
|
358
|
+
required: ['tabId']
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'fill_form',
|
|
363
|
+
description: 'Fill a form field by CSS selector. Works with input, textarea, select, checkbox, and radio. Dispatches native input/change events so React, Angular, and Vue frameworks detect the change.',
|
|
364
|
+
inputSchema: {
|
|
365
|
+
type: 'object',
|
|
366
|
+
properties: {
|
|
367
|
+
tabId: { type: 'number' },
|
|
368
|
+
selector: { type: 'string', description: 'CSS selector for the field, e.g. "#email", "input[name=username]"' },
|
|
369
|
+
value: { description: 'Value to set. String for text/select, true/false for checkbox/radio.' }
|
|
370
|
+
},
|
|
371
|
+
required: ['tabId', 'selector', 'value']
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: 'network_requests',
|
|
376
|
+
description: 'Return the last N completed network requests (XHR, Fetch, etc.) captured since the debugger attached to this tab.',
|
|
377
|
+
inputSchema: {
|
|
378
|
+
type: 'object',
|
|
379
|
+
properties: {
|
|
380
|
+
tabId: { type: 'number' },
|
|
381
|
+
limit: { type: 'number', description: 'Max requests to return (default 20)' }
|
|
382
|
+
},
|
|
383
|
+
required: ['tabId']
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: 'handle_dialog',
|
|
388
|
+
description: 'Accept or dismiss a JavaScript dialog (alert, confirm, prompt) in a Chrome tab.',
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: 'object',
|
|
391
|
+
properties: {
|
|
392
|
+
tabId: { type: 'number' },
|
|
393
|
+
accept: { type: 'boolean', description: 'true to accept/OK, false to dismiss/Cancel (default true)' },
|
|
394
|
+
promptText: { type: 'string', description: 'Text to fill in for prompt dialogs' }
|
|
395
|
+
},
|
|
396
|
+
required: ['tabId']
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: 'hover',
|
|
401
|
+
description: 'Move the mouse to coordinates in a Chrome tab (triggers hover/tooltip effects).',
|
|
402
|
+
inputSchema: {
|
|
403
|
+
type: 'object',
|
|
404
|
+
properties: {
|
|
405
|
+
tabId: { type: 'number' },
|
|
406
|
+
x: { type: 'number', description: 'X coordinate in CSS pixels' },
|
|
407
|
+
y: { type: 'number', description: 'Y coordinate in CSS pixels' }
|
|
408
|
+
},
|
|
409
|
+
required: ['tabId', 'x', 'y']
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
async function handleMcpRequest(req) {
|
|
415
|
+
const { id, method, params = {} } = req;
|
|
416
|
+
|
|
417
|
+
if (method === 'initialize') {
|
|
418
|
+
return {
|
|
419
|
+
jsonrpc: '2.0',
|
|
420
|
+
id,
|
|
421
|
+
result: {
|
|
422
|
+
protocolVersion: '2024-11-05',
|
|
423
|
+
capabilities: { tools: {} },
|
|
424
|
+
serverInfo: { name: 'sessionmcp', version: '0.1.0' }
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (method === 'tools/list') {
|
|
430
|
+
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (method === 'tools/call') {
|
|
434
|
+
const { name, arguments: args = {} } = params;
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
const result = await sendToExtension(name, args);
|
|
438
|
+
return {
|
|
439
|
+
jsonrpc: '2.0',
|
|
440
|
+
id,
|
|
441
|
+
result: {
|
|
442
|
+
content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }]
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
} catch (err) {
|
|
446
|
+
return {
|
|
447
|
+
jsonrpc: '2.0',
|
|
448
|
+
id,
|
|
449
|
+
result: {
|
|
450
|
+
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
451
|
+
isError: true
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
461
|
+
rl.on('line', async (line) => {
|
|
462
|
+
if (!line.trim()) return;
|
|
463
|
+
try {
|
|
464
|
+
const req = JSON.parse(line);
|
|
465
|
+
if (req.id === undefined || req.id === null) return;
|
|
466
|
+
const response = await handleMcpRequest(req);
|
|
467
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
468
|
+
} catch (e) {
|
|
469
|
+
process.stderr.write(`[SessionMCP Relay] Parse error: ${e}\n`);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
rl.on('close', () => process.exit(0));
|
|
474
|
+
|
|
475
|
+
// Boot
|
|
476
|
+
startServer();
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sessionmcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Universal MCP browser bridge — connect Claude Code, Cursor, VS Code, Windsurf & Cline to your existing Chrome session. No new browser. Real sessions, real cookies.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"sessionmcp": "./index.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"ws": "^8.18.0"
|
|
15
|
+
},
|
|
16
|
+
"engines": { "node": ">=18" },
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"browser",
|
|
20
|
+
"chrome",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"cursor",
|
|
23
|
+
"windsurf",
|
|
24
|
+
"cline",
|
|
25
|
+
"bridge",
|
|
26
|
+
"model-context-protocol"
|
|
27
|
+
],
|
|
28
|
+
"author": "Ginquel Moreira <contact@limitless-gmtech.com> (https://limitless-gmtech.com)",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"homepage": "https://sessionmcp.limitless-gmtech.com",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/devGinMoreira/sessionmcp"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/devGinMoreira/sessionmcp/issues"
|
|
37
|
+
}
|
|
38
|
+
}
|