chrome-relay 0.1.2 → 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 +39 -0
- package/dist/cli.js +21 -206
- 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,39 @@
|
|
|
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 click "<selector>"
|
|
22
|
+
chrome-relay fill "<selector>" "value"
|
|
23
|
+
chrome-relay keys "Enter"
|
|
24
|
+
chrome-relay screenshot -o page.png
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How it works
|
|
28
|
+
|
|
29
|
+
`chrome-relay` is a CLI-first browser bridge:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
chrome-relay CLI
|
|
33
|
+
-> local bridge on your machine
|
|
34
|
+
-> Chrome native host
|
|
35
|
+
-> Browser Relay extension
|
|
36
|
+
-> Chrome APIs
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
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.
|
|
8
|
+
var CHROME_RELAY_VERSION = "0.2.0";
|
|
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,31 @@ 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);
|
|
301
126
|
program.command("install").description("Install and register the local Chrome Relay host.").action(async () => {
|
|
302
127
|
await runInstall();
|
|
303
128
|
});
|
|
@@ -305,9 +130,6 @@ program.command("doctor").description("Validate the local Chrome Relay installat
|
|
|
305
130
|
const ok = await runDoctor();
|
|
306
131
|
process.exit(ok ? 0 : 1);
|
|
307
132
|
});
|
|
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
133
|
async function run(name, args) {
|
|
312
134
|
try {
|
|
313
135
|
const result = await callTool(name, args);
|
|
@@ -395,13 +217,6 @@ tabOpt(
|
|
|
395
217
|
if (opts.tab !== void 0) args.tabId = opts.tab;
|
|
396
218
|
await run("chrome_keyboard", args);
|
|
397
219
|
});
|
|
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
220
|
program.command("switch <tabId>").description("Activate a tab by ID.").action(async (tabId) => {
|
|
406
221
|
await run("chrome_switch_tab", { tabId: Number(tabId) });
|
|
407
222
|
});
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
-
};
|