chrome-relay 0.1.2 → 0.2.1
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 +40 -0
- package/dist/cli.js +67 -211
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/native-host.js +20 -244
- package/package.json +3 -5
- package/dist/stdio.d.ts +0 -4
- package/dist/stdio.js +0 -178
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# chrome-relay
|
|
2
|
+
|
|
3
|
+
`chrome-relay` connects your local Chrome browser to coding agents through a local bridge and a Chrome extension.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add -g chrome-relay
|
|
9
|
+
chrome-relay install
|
|
10
|
+
chrome-relay doctor
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then load the Browser Relay extension in Chrome.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
chrome-relay tabs
|
|
19
|
+
chrome-relay read -i
|
|
20
|
+
chrome-relay navigate "https://example.com" --new
|
|
21
|
+
chrome-relay navigate --tab <tabId> "https://example.com"
|
|
22
|
+
chrome-relay click "<selector>"
|
|
23
|
+
chrome-relay fill "<selector>" "value"
|
|
24
|
+
chrome-relay keys "Enter"
|
|
25
|
+
chrome-relay screenshot --tab <tabId> -o page.png
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
`chrome-relay` is a CLI-first browser bridge:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
chrome-relay CLI
|
|
34
|
+
-> local bridge on your machine
|
|
35
|
+
-> Chrome native host
|
|
36
|
+
-> Browser Relay extension
|
|
37
|
+
-> Chrome APIs
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The CLI does not need separate MCP configuration. It talks to the local bridge for you.
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import { writeFileSync } from "fs";
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
|
-
var CHROME_RELAY_VERSION = "0.1
|
|
8
|
+
var CHROME_RELAY_VERSION = "0.2.1";
|
|
9
9
|
|
|
10
10
|
// src/install/install.ts
|
|
11
11
|
import os from "os";
|
|
@@ -17,144 +17,6 @@ import { fileURLToPath } from "url";
|
|
|
17
17
|
var NATIVE_HOST_NAME = "dev.chrome_relay.native_host";
|
|
18
18
|
var DEFAULT_HTTP_PORT = 12122;
|
|
19
19
|
var DEFAULT_EXTENSION_ID = "cdmmkpadhnpcfjljhgpdnnljhjafmhop";
|
|
20
|
-
var TOOL_NAMES = {
|
|
21
|
-
GET_WINDOWS_AND_TABS: "get_windows_and_tabs",
|
|
22
|
-
NAVIGATE: "chrome_navigate",
|
|
23
|
-
SWITCH_TAB: "chrome_switch_tab",
|
|
24
|
-
CLOSE_TABS: "chrome_close_tabs",
|
|
25
|
-
SCREENSHOT: "chrome_screenshot",
|
|
26
|
-
READ_PAGE: "chrome_read_page",
|
|
27
|
-
CLICK: "chrome_click_element",
|
|
28
|
-
FILL: "chrome_fill_or_select",
|
|
29
|
-
KEYBOARD: "chrome_keyboard",
|
|
30
|
-
JAVASCRIPT: "chrome_javascript"
|
|
31
|
-
};
|
|
32
|
-
var TOOL_SCHEMAS = [
|
|
33
|
-
{
|
|
34
|
-
name: TOOL_NAMES.GET_WINDOWS_AND_TABS,
|
|
35
|
-
description: "List open Chrome windows and tabs.",
|
|
36
|
-
inputSchema: { type: "object", properties: {}, required: [] }
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: TOOL_NAMES.NAVIGATE,
|
|
40
|
-
description: "Navigate the current tab or a specific tab to a URL.",
|
|
41
|
-
inputSchema: {
|
|
42
|
-
type: "object",
|
|
43
|
-
properties: {
|
|
44
|
-
url: { type: "string", description: "Destination URL." },
|
|
45
|
-
tabId: { type: "number", description: "Optional target tab ID." },
|
|
46
|
-
newTab: { type: "boolean", description: "Open the URL in a new tab." },
|
|
47
|
-
active: { type: "boolean", description: "Whether the navigated tab should be active." }
|
|
48
|
-
},
|
|
49
|
-
required: ["url"]
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: TOOL_NAMES.SWITCH_TAB,
|
|
54
|
-
description: "Switch the active browser tab.",
|
|
55
|
-
inputSchema: {
|
|
56
|
-
type: "object",
|
|
57
|
-
properties: {
|
|
58
|
-
tabId: { type: "number", description: "Tab ID to activate." }
|
|
59
|
-
},
|
|
60
|
-
required: ["tabId"]
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: TOOL_NAMES.CLOSE_TABS,
|
|
65
|
-
description: "Close one or more tabs.",
|
|
66
|
-
inputSchema: {
|
|
67
|
-
type: "object",
|
|
68
|
-
properties: {
|
|
69
|
-
tabIds: {
|
|
70
|
-
type: "array",
|
|
71
|
-
description: "Tab IDs to close.",
|
|
72
|
-
items: { type: "number" }
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
required: ["tabIds"]
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
name: TOOL_NAMES.SCREENSHOT,
|
|
80
|
-
description: "Capture a screenshot of the current page.",
|
|
81
|
-
inputSchema: {
|
|
82
|
-
type: "object",
|
|
83
|
-
properties: {
|
|
84
|
-
tabId: { type: "number", description: "Optional target tab ID." },
|
|
85
|
-
fullPage: { type: "boolean", description: "Capture the full page when supported." }
|
|
86
|
-
},
|
|
87
|
-
required: []
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: TOOL_NAMES.READ_PAGE,
|
|
92
|
-
description: "Extract visible page structure and interactive elements.",
|
|
93
|
-
inputSchema: {
|
|
94
|
-
type: "object",
|
|
95
|
-
properties: {
|
|
96
|
-
tabId: { type: "number", description: "Optional target tab ID." },
|
|
97
|
-
interactiveOnly: {
|
|
98
|
-
type: "boolean",
|
|
99
|
-
description: "Return only interactive elements."
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
required: []
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
name: TOOL_NAMES.CLICK,
|
|
107
|
-
description: "Click a page element by selector.",
|
|
108
|
-
inputSchema: {
|
|
109
|
-
type: "object",
|
|
110
|
-
properties: {
|
|
111
|
-
selector: { type: "string", description: "CSS selector to click." },
|
|
112
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
113
|
-
},
|
|
114
|
-
required: ["selector"]
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
name: TOOL_NAMES.FILL,
|
|
119
|
-
description: "Fill an input or textarea by selector.",
|
|
120
|
-
inputSchema: {
|
|
121
|
-
type: "object",
|
|
122
|
-
properties: {
|
|
123
|
-
selector: { type: "string", description: "CSS selector to fill." },
|
|
124
|
-
value: { type: "string", description: "Text to insert." },
|
|
125
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
126
|
-
},
|
|
127
|
-
required: ["selector", "value"]
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
name: TOOL_NAMES.KEYBOARD,
|
|
132
|
-
description: "Send keyboard input to the active page.",
|
|
133
|
-
inputSchema: {
|
|
134
|
-
type: "object",
|
|
135
|
-
properties: {
|
|
136
|
-
keys: {
|
|
137
|
-
type: "string",
|
|
138
|
-
description: "Literal key text or chord, for example Enter or Meta+L."
|
|
139
|
-
},
|
|
140
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
141
|
-
},
|
|
142
|
-
required: ["keys"]
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
name: TOOL_NAMES.JAVASCRIPT,
|
|
147
|
-
description: "Evaluate JavaScript in the page context.",
|
|
148
|
-
inputSchema: {
|
|
149
|
-
type: "object",
|
|
150
|
-
properties: {
|
|
151
|
-
expression: { type: "string", description: "JavaScript expression to run." },
|
|
152
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
153
|
-
},
|
|
154
|
-
required: ["expression"]
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
];
|
|
158
20
|
|
|
159
21
|
// src/install/install.ts
|
|
160
22
|
var APP_DIR = path.join(os.homedir(), ".chrome-relay");
|
|
@@ -206,7 +68,7 @@ async function runInstall() {
|
|
|
206
68
|
console.log(`Installed Chrome Relay native host.`);
|
|
207
69
|
console.log(`Wrapper: ${wrapperPath}`);
|
|
208
70
|
console.log(`Manifest: ${manifestPath}`);
|
|
209
|
-
console.log(`
|
|
71
|
+
console.log(`Local bridge port: ${DEFAULT_HTTP_PORT}`);
|
|
210
72
|
}
|
|
211
73
|
async function runDoctor() {
|
|
212
74
|
try {
|
|
@@ -225,7 +87,7 @@ async function runDoctor() {
|
|
|
225
87
|
console.log(`Wrapper present: yes`);
|
|
226
88
|
console.log(`Manifest present: yes`);
|
|
227
89
|
console.log(`Allowed origin: ${manifest.allowed_origins?.[0] ?? "missing"}`);
|
|
228
|
-
console.log(`
|
|
90
|
+
console.log(`Local bridge reachable: ${serverReachable ? "yes" : "no"}`);
|
|
229
91
|
if (!serverReachable) {
|
|
230
92
|
console.log(`Tip: load the extension so it can launch the native host.`);
|
|
231
93
|
}
|
|
@@ -236,68 +98,47 @@ async function runDoctor() {
|
|
|
236
98
|
}
|
|
237
99
|
}
|
|
238
100
|
|
|
239
|
-
// src/stdio.ts
|
|
240
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
241
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
242
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
243
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
244
|
-
import {
|
|
245
|
-
CallToolRequestSchema,
|
|
246
|
-
ListPromptsRequestSchema,
|
|
247
|
-
ListResourcesRequestSchema,
|
|
248
|
-
ListToolsRequestSchema
|
|
249
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
250
|
-
async function runStdioProxy() {
|
|
251
|
-
const client = new Client({ name: "chrome-relay-stdio", version: "0.1.0" }, { capabilities: {} });
|
|
252
|
-
const transport = new StreamableHTTPClientTransport(
|
|
253
|
-
new URL(`http://127.0.0.1:${DEFAULT_HTTP_PORT}/mcp`)
|
|
254
|
-
);
|
|
255
|
-
await client.connect(transport);
|
|
256
|
-
const server = new Server(
|
|
257
|
-
{ name: "chrome-relay-stdio", version: "0.1.0" },
|
|
258
|
-
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
|
|
259
|
-
);
|
|
260
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
|
|
261
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
|
|
262
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
|
|
263
|
-
server.setRequestHandler(
|
|
264
|
-
CallToolRequestSchema,
|
|
265
|
-
async (request) => client.callTool({ name: request.params.name, arguments: request.params.arguments ?? {} })
|
|
266
|
-
);
|
|
267
|
-
await server.connect(new StdioServerTransport());
|
|
268
|
-
}
|
|
269
|
-
|
|
270
101
|
// src/client/call.ts
|
|
271
|
-
import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
|
|
272
|
-
import { StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
273
102
|
async function callTool(name, args) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
return JSON.parse(text);
|
|
289
|
-
} catch {
|
|
290
|
-
return text;
|
|
291
|
-
}
|
|
292
|
-
} finally {
|
|
293
|
-
await client.close().catch(() => {
|
|
294
|
-
});
|
|
103
|
+
const response = await fetch(`http://127.0.0.1:${DEFAULT_HTTP_PORT}/call`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
"content-type": "application/json"
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
name,
|
|
110
|
+
args
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
const payload = await response.json().catch(() => null);
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new Error(payload?.error || `Bridge request failed with ${response.status}`);
|
|
295
116
|
}
|
|
117
|
+
if (!payload?.ok) {
|
|
118
|
+
throw new Error(payload?.error || "Bridge call failed.");
|
|
119
|
+
}
|
|
120
|
+
return payload.data;
|
|
296
121
|
}
|
|
297
122
|
|
|
298
123
|
// src/cli.ts
|
|
299
124
|
var program = new Command();
|
|
300
|
-
program.name("chrome-relay").description("Connect your local Chrome browser to coding agents through
|
|
125
|
+
program.name("chrome-relay").description("Connect your local Chrome browser to coding agents through a local bridge.").version(CHROME_RELAY_VERSION).showHelpAfterError().addHelpText(
|
|
126
|
+
"after",
|
|
127
|
+
`
|
|
128
|
+
|
|
129
|
+
Common agent flow:
|
|
130
|
+
chrome-relay tabs
|
|
131
|
+
chrome-relay navigate --tab <tabId> "https://example.com"
|
|
132
|
+
chrome-relay read --tab <tabId> -i
|
|
133
|
+
chrome-relay click --tab <tabId> "<selector>"
|
|
134
|
+
chrome-relay fill --tab <tabId> "<selector>" "value"
|
|
135
|
+
chrome-relay screenshot --tab <tabId> -o evidence.png
|
|
136
|
+
|
|
137
|
+
Notes:
|
|
138
|
+
navigate takes a URL. Use --tab to target an existing tab.
|
|
139
|
+
screenshot --tab <tabId> auto-activates that tab first because Chrome only screenshots visible tabs.
|
|
140
|
+
`
|
|
141
|
+
);
|
|
301
142
|
program.command("install").description("Install and register the local Chrome Relay host.").action(async () => {
|
|
302
143
|
await runInstall();
|
|
303
144
|
});
|
|
@@ -305,9 +146,6 @@ program.command("doctor").description("Validate the local Chrome Relay installat
|
|
|
305
146
|
const ok = await runDoctor();
|
|
306
147
|
process.exit(ok ? 0 : 1);
|
|
307
148
|
});
|
|
308
|
-
program.command("stdio").description("Expose Chrome Relay over stdio for MCP clients that do not support HTTP.").action(async () => {
|
|
309
|
-
await runStdioProxy();
|
|
310
|
-
});
|
|
311
149
|
async function run(name, args) {
|
|
312
150
|
try {
|
|
313
151
|
const result = await callTool(name, args);
|
|
@@ -326,14 +164,29 @@ async function run(name, args) {
|
|
|
326
164
|
function tabOpt(cmd) {
|
|
327
165
|
return cmd.option("-t, --tab <id>", "target tab ID", (v) => Number(v));
|
|
328
166
|
}
|
|
329
|
-
|
|
330
|
-
program.command("tabs").description("List open Chrome windows and tabs.")
|
|
331
|
-
).action(async () => {
|
|
167
|
+
program.command("tabs").description("List open Chrome windows and tabs.").action(async () => {
|
|
332
168
|
await run("get_windows_and_tabs", {});
|
|
333
169
|
});
|
|
334
170
|
tabOpt(
|
|
335
|
-
program.command("navigate <url>").description("Navigate a tab to a URL.").option("--new", "open in a new tab").option("--inactive", "do not activate the tab")
|
|
171
|
+
program.command("navigate <url>").description("Navigate a tab to a URL. Use --tab <id> to target an existing tab.").option("--new", "open in a new tab").option("--inactive", "do not activate the tab").addHelpText(
|
|
172
|
+
"after",
|
|
173
|
+
`
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
chrome-relay navigate "https://example.com"
|
|
177
|
+
chrome-relay navigate --tab 123456789 "https://example.com"
|
|
178
|
+
chrome-relay navigate "https://example.com" --new --inactive
|
|
179
|
+
`
|
|
180
|
+
)
|
|
336
181
|
).action(async (url, opts) => {
|
|
182
|
+
if (/^\d+$/.test(url)) {
|
|
183
|
+
process.stderr.write(
|
|
184
|
+
`navigate expects a URL, but "${url}" looks like a tab ID.
|
|
185
|
+
Use "chrome-relay switch ${url}" to activate that tab, or "chrome-relay navigate --tab ${url} https://example.com" to navigate it.
|
|
186
|
+
`
|
|
187
|
+
);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
337
190
|
const args = { url };
|
|
338
191
|
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
339
192
|
if (opts.new) args.newTab = true;
|
|
@@ -341,7 +194,17 @@ tabOpt(
|
|
|
341
194
|
await run("chrome_navigate", args);
|
|
342
195
|
});
|
|
343
196
|
tabOpt(
|
|
344
|
-
program.command("screenshot").description("Capture a screenshot
|
|
197
|
+
program.command("screenshot").description("Capture a screenshot. With --tab, the tab is auto-activated first.").option("--full", "capture full page").option("-o, --out <path>", "save image to path (base64 PNG decoded)").addHelpText(
|
|
198
|
+
"after",
|
|
199
|
+
`
|
|
200
|
+
|
|
201
|
+
Examples:
|
|
202
|
+
chrome-relay screenshot -o active-tab.png
|
|
203
|
+
chrome-relay screenshot --tab 123456789 -o evidence.png
|
|
204
|
+
|
|
205
|
+
Chrome can only capture visible tabs, so --tab will focus/activate the tab before capturing.
|
|
206
|
+
`
|
|
207
|
+
)
|
|
345
208
|
).action(async (opts) => {
|
|
346
209
|
const args = {};
|
|
347
210
|
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
@@ -395,13 +258,6 @@ tabOpt(
|
|
|
395
258
|
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
396
259
|
await run("chrome_keyboard", args);
|
|
397
260
|
});
|
|
398
|
-
tabOpt(
|
|
399
|
-
program.command("js <expression>").description("Evaluate JavaScript in the page context.")
|
|
400
|
-
).action(async (expression, opts) => {
|
|
401
|
-
const args = { expression };
|
|
402
|
-
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
403
|
-
await run("chrome_javascript", args);
|
|
404
|
-
});
|
|
405
261
|
program.command("switch <tabId>").description("Activate a tab by ID.").action(async (tabId) => {
|
|
406
262
|
await run("chrome_switch_tab", { tabId: Number(tabId) });
|
|
407
263
|
});
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/native-host.js
CHANGED
|
@@ -5,202 +5,9 @@ import process from "process";
|
|
|
5
5
|
|
|
6
6
|
// src/http/server.ts
|
|
7
7
|
import Fastify from "fastify";
|
|
8
|
-
import cors from "@fastify/cors";
|
|
9
|
-
import { randomUUID } from "crypto";
|
|
10
|
-
import {
|
|
11
|
-
StreamableHTTPServerTransport
|
|
12
|
-
} from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13
|
-
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
14
8
|
|
|
15
9
|
// ../protocol/dist/index.js
|
|
16
10
|
var DEFAULT_HTTP_PORT = 12122;
|
|
17
|
-
var TOOL_NAMES = {
|
|
18
|
-
GET_WINDOWS_AND_TABS: "get_windows_and_tabs",
|
|
19
|
-
NAVIGATE: "chrome_navigate",
|
|
20
|
-
SWITCH_TAB: "chrome_switch_tab",
|
|
21
|
-
CLOSE_TABS: "chrome_close_tabs",
|
|
22
|
-
SCREENSHOT: "chrome_screenshot",
|
|
23
|
-
READ_PAGE: "chrome_read_page",
|
|
24
|
-
CLICK: "chrome_click_element",
|
|
25
|
-
FILL: "chrome_fill_or_select",
|
|
26
|
-
KEYBOARD: "chrome_keyboard",
|
|
27
|
-
JAVASCRIPT: "chrome_javascript"
|
|
28
|
-
};
|
|
29
|
-
var TOOL_SCHEMAS = [
|
|
30
|
-
{
|
|
31
|
-
name: TOOL_NAMES.GET_WINDOWS_AND_TABS,
|
|
32
|
-
description: "List open Chrome windows and tabs.",
|
|
33
|
-
inputSchema: { type: "object", properties: {}, required: [] }
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
name: TOOL_NAMES.NAVIGATE,
|
|
37
|
-
description: "Navigate the current tab or a specific tab to a URL.",
|
|
38
|
-
inputSchema: {
|
|
39
|
-
type: "object",
|
|
40
|
-
properties: {
|
|
41
|
-
url: { type: "string", description: "Destination URL." },
|
|
42
|
-
tabId: { type: "number", description: "Optional target tab ID." },
|
|
43
|
-
newTab: { type: "boolean", description: "Open the URL in a new tab." },
|
|
44
|
-
active: { type: "boolean", description: "Whether the navigated tab should be active." }
|
|
45
|
-
},
|
|
46
|
-
required: ["url"]
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
name: TOOL_NAMES.SWITCH_TAB,
|
|
51
|
-
description: "Switch the active browser tab.",
|
|
52
|
-
inputSchema: {
|
|
53
|
-
type: "object",
|
|
54
|
-
properties: {
|
|
55
|
-
tabId: { type: "number", description: "Tab ID to activate." }
|
|
56
|
-
},
|
|
57
|
-
required: ["tabId"]
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: TOOL_NAMES.CLOSE_TABS,
|
|
62
|
-
description: "Close one or more tabs.",
|
|
63
|
-
inputSchema: {
|
|
64
|
-
type: "object",
|
|
65
|
-
properties: {
|
|
66
|
-
tabIds: {
|
|
67
|
-
type: "array",
|
|
68
|
-
description: "Tab IDs to close.",
|
|
69
|
-
items: { type: "number" }
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
required: ["tabIds"]
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: TOOL_NAMES.SCREENSHOT,
|
|
77
|
-
description: "Capture a screenshot of the current page.",
|
|
78
|
-
inputSchema: {
|
|
79
|
-
type: "object",
|
|
80
|
-
properties: {
|
|
81
|
-
tabId: { type: "number", description: "Optional target tab ID." },
|
|
82
|
-
fullPage: { type: "boolean", description: "Capture the full page when supported." }
|
|
83
|
-
},
|
|
84
|
-
required: []
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: TOOL_NAMES.READ_PAGE,
|
|
89
|
-
description: "Extract visible page structure and interactive elements.",
|
|
90
|
-
inputSchema: {
|
|
91
|
-
type: "object",
|
|
92
|
-
properties: {
|
|
93
|
-
tabId: { type: "number", description: "Optional target tab ID." },
|
|
94
|
-
interactiveOnly: {
|
|
95
|
-
type: "boolean",
|
|
96
|
-
description: "Return only interactive elements."
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
required: []
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: TOOL_NAMES.CLICK,
|
|
104
|
-
description: "Click a page element by selector.",
|
|
105
|
-
inputSchema: {
|
|
106
|
-
type: "object",
|
|
107
|
-
properties: {
|
|
108
|
-
selector: { type: "string", description: "CSS selector to click." },
|
|
109
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
110
|
-
},
|
|
111
|
-
required: ["selector"]
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
name: TOOL_NAMES.FILL,
|
|
116
|
-
description: "Fill an input or textarea by selector.",
|
|
117
|
-
inputSchema: {
|
|
118
|
-
type: "object",
|
|
119
|
-
properties: {
|
|
120
|
-
selector: { type: "string", description: "CSS selector to fill." },
|
|
121
|
-
value: { type: "string", description: "Text to insert." },
|
|
122
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
123
|
-
},
|
|
124
|
-
required: ["selector", "value"]
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: TOOL_NAMES.KEYBOARD,
|
|
129
|
-
description: "Send keyboard input to the active page.",
|
|
130
|
-
inputSchema: {
|
|
131
|
-
type: "object",
|
|
132
|
-
properties: {
|
|
133
|
-
keys: {
|
|
134
|
-
type: "string",
|
|
135
|
-
description: "Literal key text or chord, for example Enter or Meta+L."
|
|
136
|
-
},
|
|
137
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
138
|
-
},
|
|
139
|
-
required: ["keys"]
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
name: TOOL_NAMES.JAVASCRIPT,
|
|
144
|
-
description: "Evaluate JavaScript in the page context.",
|
|
145
|
-
inputSchema: {
|
|
146
|
-
type: "object",
|
|
147
|
-
properties: {
|
|
148
|
-
expression: { type: "string", description: "JavaScript expression to run." },
|
|
149
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
150
|
-
},
|
|
151
|
-
required: ["expression"]
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
];
|
|
155
|
-
|
|
156
|
-
// src/mcp/server.ts
|
|
157
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
158
|
-
import {
|
|
159
|
-
CallToolRequestSchema,
|
|
160
|
-
ListPromptsRequestSchema,
|
|
161
|
-
ListResourcesRequestSchema,
|
|
162
|
-
ListToolsRequestSchema
|
|
163
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
164
|
-
function toCallToolResult(data) {
|
|
165
|
-
return {
|
|
166
|
-
content: [
|
|
167
|
-
{
|
|
168
|
-
type: "text",
|
|
169
|
-
text: typeof data === "string" ? data : JSON.stringify(data)
|
|
170
|
-
}
|
|
171
|
-
],
|
|
172
|
-
isError: false
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
function createMcpServer(bridge2) {
|
|
176
|
-
const server2 = new Server(
|
|
177
|
-
{ name: "chrome-relay", version: "0.1.0" },
|
|
178
|
-
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
|
|
179
|
-
);
|
|
180
|
-
server2.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
|
|
181
|
-
server2.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
|
|
182
|
-
server2.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
|
|
183
|
-
server2.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
184
|
-
try {
|
|
185
|
-
const result = await bridge2.callTool(
|
|
186
|
-
request.params.name,
|
|
187
|
-
request.params.arguments ?? {}
|
|
188
|
-
);
|
|
189
|
-
return toCallToolResult(result);
|
|
190
|
-
} catch (error) {
|
|
191
|
-
return {
|
|
192
|
-
content: [
|
|
193
|
-
{
|
|
194
|
-
type: "text",
|
|
195
|
-
text: error instanceof Error ? error.message : String(error)
|
|
196
|
-
}
|
|
197
|
-
],
|
|
198
|
-
isError: true
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
return server2;
|
|
203
|
-
}
|
|
204
11
|
|
|
205
12
|
// src/http/server.ts
|
|
206
13
|
var RelayHttpServer = class {
|
|
@@ -211,61 +18,30 @@ var RelayHttpServer = class {
|
|
|
211
18
|
bridge;
|
|
212
19
|
port;
|
|
213
20
|
app = Fastify({ logger: false });
|
|
214
|
-
transports = /* @__PURE__ */ new Map();
|
|
215
21
|
async start() {
|
|
216
|
-
await this.app.register(cors, { origin: true });
|
|
217
22
|
this.app.get("/ping", async () => ({ ok: true, port: this.port }));
|
|
218
|
-
this.app.post("/
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!transport) {
|
|
222
|
-
if (sessionId || !isInitializeRequest(request.body)) {
|
|
223
|
-
reply.code(400).send({ error: "Invalid MCP session." });
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
transport = new StreamableHTTPServerTransport({
|
|
227
|
-
sessionIdGenerator: () => randomUUID(),
|
|
228
|
-
onsessioninitialized: (nextSessionId) => {
|
|
229
|
-
if (transport) {
|
|
230
|
-
this.transports.set(nextSessionId, transport);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
transport.onclose = () => {
|
|
235
|
-
if (transport?.sessionId) {
|
|
236
|
-
this.transports.delete(transport.sessionId);
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
const mcpServer = createMcpServer(this.bridge);
|
|
240
|
-
await mcpServer.connect(transport);
|
|
241
|
-
}
|
|
242
|
-
await transport.handleRequest(request.raw, reply.raw, request.body);
|
|
243
|
-
});
|
|
244
|
-
this.app.get("/mcp", async (request, reply) => {
|
|
245
|
-
const sessionId = request.headers["mcp-session-id"];
|
|
246
|
-
if (!sessionId) {
|
|
247
|
-
reply.code(400).send({ error: "Missing MCP session ID." });
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
const transport = this.transports.get(sessionId);
|
|
251
|
-
if (!transport) {
|
|
252
|
-
reply.code(404).send({ error: "Unknown MCP session." });
|
|
23
|
+
this.app.post("/call", async (request, reply) => {
|
|
24
|
+
if (request.headers.origin) {
|
|
25
|
+
reply.code(403).send({ error: "Browser-origin bridge requests are not accepted." });
|
|
253
26
|
return;
|
|
254
27
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const sessionId = request.headers["mcp-session-id"];
|
|
259
|
-
if (!sessionId) {
|
|
260
|
-
reply.code(400).send({ error: "Missing MCP session ID." });
|
|
28
|
+
const body = request.body ?? {};
|
|
29
|
+
if (typeof body.name !== "string") {
|
|
30
|
+
reply.code(400).send({ ok: false, error: "Missing tool name." });
|
|
261
31
|
return;
|
|
262
32
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
33
|
+
try {
|
|
34
|
+
const data = await this.bridge.callTool(
|
|
35
|
+
body.name,
|
|
36
|
+
body.args ?? {}
|
|
37
|
+
);
|
|
38
|
+
reply.send({ ok: true, data });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
reply.code(500).send({
|
|
41
|
+
ok: false,
|
|
42
|
+
error: error instanceof Error ? error.message : String(error)
|
|
43
|
+
});
|
|
267
44
|
}
|
|
268
|
-
await transport.handleRequest(request.raw, reply.raw);
|
|
269
45
|
});
|
|
270
46
|
await this.app.listen({ port: this.port, host: "127.0.0.1" });
|
|
271
47
|
}
|
|
@@ -275,7 +51,7 @@ var RelayHttpServer = class {
|
|
|
275
51
|
};
|
|
276
52
|
|
|
277
53
|
// src/native/bridge.ts
|
|
278
|
-
import { randomUUID
|
|
54
|
+
import { randomUUID } from "crypto";
|
|
279
55
|
var ExtensionBridge = class {
|
|
280
56
|
constructor(send) {
|
|
281
57
|
this.send = send;
|
|
@@ -340,7 +116,7 @@ var ExtensionBridge = class {
|
|
|
340
116
|
});
|
|
341
117
|
}
|
|
342
118
|
async ping(timeoutMs = 2e3) {
|
|
343
|
-
const id =
|
|
119
|
+
const id = randomUUID();
|
|
344
120
|
const message = { type: "bridge.ping", id };
|
|
345
121
|
return new Promise((resolve) => {
|
|
346
122
|
const timer = setTimeout(() => {
|
|
@@ -357,7 +133,7 @@ var ExtensionBridge = class {
|
|
|
357
133
|
}
|
|
358
134
|
async callTool(name, args, timeoutMs = 3e4) {
|
|
359
135
|
await this.waitUntilReady();
|
|
360
|
-
const id =
|
|
136
|
+
const id = randomUUID();
|
|
361
137
|
return new Promise((resolve, reject) => {
|
|
362
138
|
const timer = setTimeout(() => {
|
|
363
139
|
this.pending.delete(id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-relay",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,12 +15,10 @@
|
|
|
15
15
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
16
16
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
17
17
|
},
|
|
18
|
-
"description": "Connect your local Chrome browser to coding agents through
|
|
19
|
-
"keywords": ["
|
|
18
|
+
"description": "Connect your local Chrome browser to coding agents through a local bridge.",
|
|
19
|
+
"keywords": ["chrome", "browser", "automation", "agents", "native-messaging"],
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@fastify/cors": "^11.0.1",
|
|
23
|
-
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
24
22
|
"chalk": "^5.4.1",
|
|
25
23
|
"commander": "^13.1.0",
|
|
26
24
|
"fastify": "^5.3.2"
|
package/dist/stdio.d.ts
DELETED
package/dist/stdio.js
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/stdio.ts
|
|
4
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
7
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
-
import {
|
|
9
|
-
CallToolRequestSchema,
|
|
10
|
-
ListPromptsRequestSchema,
|
|
11
|
-
ListResourcesRequestSchema,
|
|
12
|
-
ListToolsRequestSchema
|
|
13
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
-
|
|
15
|
-
// ../protocol/dist/index.js
|
|
16
|
-
var DEFAULT_HTTP_PORT = 12122;
|
|
17
|
-
var TOOL_NAMES = {
|
|
18
|
-
GET_WINDOWS_AND_TABS: "get_windows_and_tabs",
|
|
19
|
-
NAVIGATE: "chrome_navigate",
|
|
20
|
-
SWITCH_TAB: "chrome_switch_tab",
|
|
21
|
-
CLOSE_TABS: "chrome_close_tabs",
|
|
22
|
-
SCREENSHOT: "chrome_screenshot",
|
|
23
|
-
READ_PAGE: "chrome_read_page",
|
|
24
|
-
CLICK: "chrome_click_element",
|
|
25
|
-
FILL: "chrome_fill_or_select",
|
|
26
|
-
KEYBOARD: "chrome_keyboard",
|
|
27
|
-
JAVASCRIPT: "chrome_javascript"
|
|
28
|
-
};
|
|
29
|
-
var TOOL_SCHEMAS = [
|
|
30
|
-
{
|
|
31
|
-
name: TOOL_NAMES.GET_WINDOWS_AND_TABS,
|
|
32
|
-
description: "List open Chrome windows and tabs.",
|
|
33
|
-
inputSchema: { type: "object", properties: {}, required: [] }
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
name: TOOL_NAMES.NAVIGATE,
|
|
37
|
-
description: "Navigate the current tab or a specific tab to a URL.",
|
|
38
|
-
inputSchema: {
|
|
39
|
-
type: "object",
|
|
40
|
-
properties: {
|
|
41
|
-
url: { type: "string", description: "Destination URL." },
|
|
42
|
-
tabId: { type: "number", description: "Optional target tab ID." },
|
|
43
|
-
newTab: { type: "boolean", description: "Open the URL in a new tab." },
|
|
44
|
-
active: { type: "boolean", description: "Whether the navigated tab should be active." }
|
|
45
|
-
},
|
|
46
|
-
required: ["url"]
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
name: TOOL_NAMES.SWITCH_TAB,
|
|
51
|
-
description: "Switch the active browser tab.",
|
|
52
|
-
inputSchema: {
|
|
53
|
-
type: "object",
|
|
54
|
-
properties: {
|
|
55
|
-
tabId: { type: "number", description: "Tab ID to activate." }
|
|
56
|
-
},
|
|
57
|
-
required: ["tabId"]
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: TOOL_NAMES.CLOSE_TABS,
|
|
62
|
-
description: "Close one or more tabs.",
|
|
63
|
-
inputSchema: {
|
|
64
|
-
type: "object",
|
|
65
|
-
properties: {
|
|
66
|
-
tabIds: {
|
|
67
|
-
type: "array",
|
|
68
|
-
description: "Tab IDs to close.",
|
|
69
|
-
items: { type: "number" }
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
required: ["tabIds"]
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: TOOL_NAMES.SCREENSHOT,
|
|
77
|
-
description: "Capture a screenshot of the current page.",
|
|
78
|
-
inputSchema: {
|
|
79
|
-
type: "object",
|
|
80
|
-
properties: {
|
|
81
|
-
tabId: { type: "number", description: "Optional target tab ID." },
|
|
82
|
-
fullPage: { type: "boolean", description: "Capture the full page when supported." }
|
|
83
|
-
},
|
|
84
|
-
required: []
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: TOOL_NAMES.READ_PAGE,
|
|
89
|
-
description: "Extract visible page structure and interactive elements.",
|
|
90
|
-
inputSchema: {
|
|
91
|
-
type: "object",
|
|
92
|
-
properties: {
|
|
93
|
-
tabId: { type: "number", description: "Optional target tab ID." },
|
|
94
|
-
interactiveOnly: {
|
|
95
|
-
type: "boolean",
|
|
96
|
-
description: "Return only interactive elements."
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
required: []
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: TOOL_NAMES.CLICK,
|
|
104
|
-
description: "Click a page element by selector.",
|
|
105
|
-
inputSchema: {
|
|
106
|
-
type: "object",
|
|
107
|
-
properties: {
|
|
108
|
-
selector: { type: "string", description: "CSS selector to click." },
|
|
109
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
110
|
-
},
|
|
111
|
-
required: ["selector"]
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
name: TOOL_NAMES.FILL,
|
|
116
|
-
description: "Fill an input or textarea by selector.",
|
|
117
|
-
inputSchema: {
|
|
118
|
-
type: "object",
|
|
119
|
-
properties: {
|
|
120
|
-
selector: { type: "string", description: "CSS selector to fill." },
|
|
121
|
-
value: { type: "string", description: "Text to insert." },
|
|
122
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
123
|
-
},
|
|
124
|
-
required: ["selector", "value"]
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
name: TOOL_NAMES.KEYBOARD,
|
|
129
|
-
description: "Send keyboard input to the active page.",
|
|
130
|
-
inputSchema: {
|
|
131
|
-
type: "object",
|
|
132
|
-
properties: {
|
|
133
|
-
keys: {
|
|
134
|
-
type: "string",
|
|
135
|
-
description: "Literal key text or chord, for example Enter or Meta+L."
|
|
136
|
-
},
|
|
137
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
138
|
-
},
|
|
139
|
-
required: ["keys"]
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
name: TOOL_NAMES.JAVASCRIPT,
|
|
144
|
-
description: "Evaluate JavaScript in the page context.",
|
|
145
|
-
inputSchema: {
|
|
146
|
-
type: "object",
|
|
147
|
-
properties: {
|
|
148
|
-
expression: { type: "string", description: "JavaScript expression to run." },
|
|
149
|
-
tabId: { type: "number", description: "Optional target tab ID." }
|
|
150
|
-
},
|
|
151
|
-
required: ["expression"]
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
];
|
|
155
|
-
|
|
156
|
-
// src/stdio.ts
|
|
157
|
-
async function runStdioProxy() {
|
|
158
|
-
const client = new Client({ name: "chrome-relay-stdio", version: "0.1.0" }, { capabilities: {} });
|
|
159
|
-
const transport = new StreamableHTTPClientTransport(
|
|
160
|
-
new URL(`http://127.0.0.1:${DEFAULT_HTTP_PORT}/mcp`)
|
|
161
|
-
);
|
|
162
|
-
await client.connect(transport);
|
|
163
|
-
const server = new Server(
|
|
164
|
-
{ name: "chrome-relay-stdio", version: "0.1.0" },
|
|
165
|
-
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
|
|
166
|
-
);
|
|
167
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
|
|
168
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
|
|
169
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
|
|
170
|
-
server.setRequestHandler(
|
|
171
|
-
CallToolRequestSchema,
|
|
172
|
-
async (request) => client.callTool({ name: request.params.name, arguments: request.params.arguments ?? {} })
|
|
173
|
-
);
|
|
174
|
-
await server.connect(new StdioServerTransport());
|
|
175
|
-
}
|
|
176
|
-
export {
|
|
177
|
-
runStdioProxy
|
|
178
|
-
};
|