chrome-relay 0.1.1 → 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 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
@@ -2,9 +2,10 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
+ import { writeFileSync } from "fs";
5
6
 
6
7
  // src/index.ts
7
- var CHROME_RELAY_VERSION = "0.1.0";
8
+ var CHROME_RELAY_VERSION = "0.2.0";
8
9
 
9
10
  // src/install/install.ts
10
11
  import os from "os";
@@ -16,144 +17,6 @@ import { fileURLToPath } from "url";
16
17
  var NATIVE_HOST_NAME = "dev.chrome_relay.native_host";
17
18
  var DEFAULT_HTTP_PORT = 12122;
18
19
  var DEFAULT_EXTENSION_ID = "cdmmkpadhnpcfjljhgpdnnljhjafmhop";
19
- var TOOL_NAMES = {
20
- GET_WINDOWS_AND_TABS: "get_windows_and_tabs",
21
- NAVIGATE: "chrome_navigate",
22
- SWITCH_TAB: "chrome_switch_tab",
23
- CLOSE_TABS: "chrome_close_tabs",
24
- SCREENSHOT: "chrome_screenshot",
25
- READ_PAGE: "chrome_read_page",
26
- CLICK: "chrome_click_element",
27
- FILL: "chrome_fill_or_select",
28
- KEYBOARD: "chrome_keyboard",
29
- JAVASCRIPT: "chrome_javascript"
30
- };
31
- var TOOL_SCHEMAS = [
32
- {
33
- name: TOOL_NAMES.GET_WINDOWS_AND_TABS,
34
- description: "List open Chrome windows and tabs.",
35
- inputSchema: { type: "object", properties: {}, required: [] }
36
- },
37
- {
38
- name: TOOL_NAMES.NAVIGATE,
39
- description: "Navigate the current tab or a specific tab to a URL.",
40
- inputSchema: {
41
- type: "object",
42
- properties: {
43
- url: { type: "string", description: "Destination URL." },
44
- tabId: { type: "number", description: "Optional target tab ID." },
45
- newTab: { type: "boolean", description: "Open the URL in a new tab." },
46
- active: { type: "boolean", description: "Whether the navigated tab should be active." }
47
- },
48
- required: ["url"]
49
- }
50
- },
51
- {
52
- name: TOOL_NAMES.SWITCH_TAB,
53
- description: "Switch the active browser tab.",
54
- inputSchema: {
55
- type: "object",
56
- properties: {
57
- tabId: { type: "number", description: "Tab ID to activate." }
58
- },
59
- required: ["tabId"]
60
- }
61
- },
62
- {
63
- name: TOOL_NAMES.CLOSE_TABS,
64
- description: "Close one or more tabs.",
65
- inputSchema: {
66
- type: "object",
67
- properties: {
68
- tabIds: {
69
- type: "array",
70
- description: "Tab IDs to close.",
71
- items: { type: "number" }
72
- }
73
- },
74
- required: ["tabIds"]
75
- }
76
- },
77
- {
78
- name: TOOL_NAMES.SCREENSHOT,
79
- description: "Capture a screenshot of the current page.",
80
- inputSchema: {
81
- type: "object",
82
- properties: {
83
- tabId: { type: "number", description: "Optional target tab ID." },
84
- fullPage: { type: "boolean", description: "Capture the full page when supported." }
85
- },
86
- required: []
87
- }
88
- },
89
- {
90
- name: TOOL_NAMES.READ_PAGE,
91
- description: "Extract visible page structure and interactive elements.",
92
- inputSchema: {
93
- type: "object",
94
- properties: {
95
- tabId: { type: "number", description: "Optional target tab ID." },
96
- interactiveOnly: {
97
- type: "boolean",
98
- description: "Return only interactive elements."
99
- }
100
- },
101
- required: []
102
- }
103
- },
104
- {
105
- name: TOOL_NAMES.CLICK,
106
- description: "Click a page element by selector.",
107
- inputSchema: {
108
- type: "object",
109
- properties: {
110
- selector: { type: "string", description: "CSS selector to click." },
111
- tabId: { type: "number", description: "Optional target tab ID." }
112
- },
113
- required: ["selector"]
114
- }
115
- },
116
- {
117
- name: TOOL_NAMES.FILL,
118
- description: "Fill an input or textarea by selector.",
119
- inputSchema: {
120
- type: "object",
121
- properties: {
122
- selector: { type: "string", description: "CSS selector to fill." },
123
- value: { type: "string", description: "Text to insert." },
124
- tabId: { type: "number", description: "Optional target tab ID." }
125
- },
126
- required: ["selector", "value"]
127
- }
128
- },
129
- {
130
- name: TOOL_NAMES.KEYBOARD,
131
- description: "Send keyboard input to the active page.",
132
- inputSchema: {
133
- type: "object",
134
- properties: {
135
- keys: {
136
- type: "string",
137
- description: "Literal key text or chord, for example Enter or Meta+L."
138
- },
139
- tabId: { type: "number", description: "Optional target tab ID." }
140
- },
141
- required: ["keys"]
142
- }
143
- },
144
- {
145
- name: TOOL_NAMES.JAVASCRIPT,
146
- description: "Evaluate JavaScript in the page context.",
147
- inputSchema: {
148
- type: "object",
149
- properties: {
150
- expression: { type: "string", description: "JavaScript expression to run." },
151
- tabId: { type: "number", description: "Optional target tab ID." }
152
- },
153
- required: ["expression"]
154
- }
155
- }
156
- ];
157
20
 
158
21
  // src/install/install.ts
159
22
  var APP_DIR = path.join(os.homedir(), ".chrome-relay");
@@ -205,7 +68,7 @@ async function runInstall() {
205
68
  console.log(`Installed Chrome Relay native host.`);
206
69
  console.log(`Wrapper: ${wrapperPath}`);
207
70
  console.log(`Manifest: ${manifestPath}`);
208
- console.log(`HTTP MCP endpoint: http://127.0.0.1:${DEFAULT_HTTP_PORT}/mcp`);
71
+ console.log(`Local bridge port: ${DEFAULT_HTTP_PORT}`);
209
72
  }
210
73
  async function runDoctor() {
211
74
  try {
@@ -224,7 +87,7 @@ async function runDoctor() {
224
87
  console.log(`Wrapper present: yes`);
225
88
  console.log(`Manifest present: yes`);
226
89
  console.log(`Allowed origin: ${manifest.allowed_origins?.[0] ?? "missing"}`);
227
- console.log(`HTTP server reachable: ${serverReachable ? "yes" : "no"}`);
90
+ console.log(`Local bridge reachable: ${serverReachable ? "yes" : "no"}`);
228
91
  if (!serverReachable) {
229
92
  console.log(`Tip: load the extension so it can launch the native host.`);
230
93
  }
@@ -235,40 +98,31 @@ async function runDoctor() {
235
98
  }
236
99
  }
237
100
 
238
- // src/stdio.ts
239
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
240
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
241
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
242
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
243
- import {
244
- CallToolRequestSchema,
245
- ListPromptsRequestSchema,
246
- ListResourcesRequestSchema,
247
- ListToolsRequestSchema
248
- } from "@modelcontextprotocol/sdk/types.js";
249
- async function runStdioProxy() {
250
- const client = new Client({ name: "chrome-relay-stdio", version: "0.1.0" }, { capabilities: {} });
251
- const transport = new StreamableHTTPClientTransport(
252
- new URL(`http://127.0.0.1:${DEFAULT_HTTP_PORT}/mcp`)
253
- );
254
- await client.connect(transport);
255
- const server = new Server(
256
- { name: "chrome-relay-stdio", version: "0.1.0" },
257
- { capabilities: { tools: {}, resources: {}, prompts: {} } }
258
- );
259
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
260
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [] }));
261
- server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: [] }));
262
- server.setRequestHandler(
263
- CallToolRequestSchema,
264
- async (request) => client.callTool({ name: request.params.name, arguments: request.params.arguments ?? {} })
265
- );
266
- await server.connect(new StdioServerTransport());
101
+ // src/client/call.ts
102
+ async function callTool(name, args) {
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}`);
116
+ }
117
+ if (!payload?.ok) {
118
+ throw new Error(payload?.error || "Bridge call failed.");
119
+ }
120
+ return payload.data;
267
121
  }
268
122
 
269
123
  // src/cli.ts
270
124
  var program = new Command();
271
- program.name("chrome-relay").description("Connect your local Chrome browser to coding agents through MCP.").version(CHROME_RELAY_VERSION);
125
+ program.name("chrome-relay").description("Connect your local Chrome browser to coding agents through a local bridge.").version(CHROME_RELAY_VERSION);
272
126
  program.command("install").description("Install and register the local Chrome Relay host.").action(async () => {
273
127
  await runInstall();
274
128
  });
@@ -276,7 +130,101 @@ program.command("doctor").description("Validate the local Chrome Relay installat
276
130
  const ok = await runDoctor();
277
131
  process.exit(ok ? 0 : 1);
278
132
  });
279
- program.command("stdio").description("Expose Chrome Relay over stdio for MCP clients that do not support HTTP.").action(async () => {
280
- await runStdioProxy();
133
+ async function run(name, args) {
134
+ try {
135
+ const result = await callTool(name, args);
136
+ if (typeof result === "string") {
137
+ process.stdout.write(result + "\n");
138
+ } else {
139
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
140
+ }
141
+ } catch (error) {
142
+ process.stderr.write(
143
+ (error instanceof Error ? error.message : String(error)) + "\n"
144
+ );
145
+ process.exit(1);
146
+ }
147
+ }
148
+ function tabOpt(cmd) {
149
+ return cmd.option("-t, --tab <id>", "target tab ID", (v) => Number(v));
150
+ }
151
+ tabOpt(
152
+ program.command("tabs").description("List open Chrome windows and tabs.")
153
+ ).action(async () => {
154
+ await run("get_windows_and_tabs", {});
155
+ });
156
+ tabOpt(
157
+ 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")
158
+ ).action(async (url, opts) => {
159
+ const args = { url };
160
+ if (opts.tab !== void 0) args.tabId = opts.tab;
161
+ if (opts.new) args.newTab = true;
162
+ if (opts.inactive) args.active = false;
163
+ await run("chrome_navigate", args);
164
+ });
165
+ tabOpt(
166
+ program.command("screenshot").description("Capture a screenshot of the current page.").option("--full", "capture full page").option("-o, --out <path>", "save image to path (base64 PNG decoded)")
167
+ ).action(async (opts) => {
168
+ const args = {};
169
+ if (opts.tab !== void 0) args.tabId = opts.tab;
170
+ if (opts.full) args.fullPage = true;
171
+ try {
172
+ const result = await callTool("chrome_screenshot", args);
173
+ if (opts.out && result && typeof result === "object") {
174
+ const data = result.dataUrl ?? result.data;
175
+ if (typeof data === "string") {
176
+ const b64 = data.includes(",") ? data.split(",")[1] : data;
177
+ writeFileSync(opts.out, Buffer.from(b64, "base64"));
178
+ process.stdout.write(`Saved screenshot to ${opts.out}
179
+ `);
180
+ return;
181
+ }
182
+ }
183
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
184
+ } catch (error) {
185
+ process.stderr.write(
186
+ (error instanceof Error ? error.message : String(error)) + "\n"
187
+ );
188
+ process.exit(1);
189
+ }
190
+ });
191
+ tabOpt(
192
+ program.command("read").description("Extract page structure and interactive elements.").option("-i, --interactive", "return only interactive elements")
193
+ ).action(async (opts) => {
194
+ const args = {};
195
+ if (opts.tab !== void 0) args.tabId = opts.tab;
196
+ if (opts.interactive) args.interactiveOnly = true;
197
+ await run("chrome_read_page", args);
198
+ });
199
+ tabOpt(
200
+ program.command("click <selector>").description("Click an element by CSS selector.")
201
+ ).action(async (selector, opts) => {
202
+ const args = { selector };
203
+ if (opts.tab !== void 0) args.tabId = opts.tab;
204
+ await run("chrome_click_element", args);
205
+ });
206
+ tabOpt(
207
+ program.command("fill <selector> <value>").description("Fill an input or textarea.")
208
+ ).action(async (selector, value, opts) => {
209
+ const args = { selector, value };
210
+ if (opts.tab !== void 0) args.tabId = opts.tab;
211
+ await run("chrome_fill_or_select", args);
212
+ });
213
+ tabOpt(
214
+ program.command("keys <keys>").description("Send keyboard input (e.g. Enter, Meta+L).")
215
+ ).action(async (keys, opts) => {
216
+ const args = { keys };
217
+ if (opts.tab !== void 0) args.tabId = opts.tab;
218
+ await run("chrome_keyboard", args);
219
+ });
220
+ program.command("switch <tabId>").description("Activate a tab by ID.").action(async (tabId) => {
221
+ await run("chrome_switch_tab", { tabId: Number(tabId) });
222
+ });
223
+ program.command("close <tabIds...>").description("Close one or more tabs by ID.").action(async (tabIds) => {
224
+ await run("chrome_close_tabs", { tabIds: tabIds.map(Number) });
225
+ });
226
+ program.command("call <tool> [json]").description("Call any Chrome Relay tool with raw JSON args.").action(async (tool, json) => {
227
+ const args = json ? JSON.parse(json) : {};
228
+ await run(tool, args);
281
229
  });
282
- program.parse(process.argv);
230
+ program.parseAsync(process.argv);
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- declare const CHROME_RELAY_VERSION = "0.1.0";
1
+ declare const CHROME_RELAY_VERSION = "0.2.0";
2
2
 
3
3
  export { CHROME_RELAY_VERSION };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- var CHROME_RELAY_VERSION = "0.1.0";
2
+ var CHROME_RELAY_VERSION = "0.2.0";
3
3
  export {
4
4
  CHROME_RELAY_VERSION
5
5
  };
@@ -5,266 +5,43 @@ 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 {
207
14
  constructor(bridge2, port = DEFAULT_HTTP_PORT) {
15
+ this.bridge = bridge2;
208
16
  this.port = port;
209
- this.mcpServer = createMcpServer(bridge2);
210
17
  }
18
+ bridge;
211
19
  port;
212
20
  app = Fastify({ logger: false });
213
- transports = /* @__PURE__ */ new Map();
214
- mcpServer;
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("/mcp", async (request, reply) => {
219
- const sessionId = request.headers["mcp-session-id"];
220
- let transport = sessionId ? this.transports.get(sessionId) : void 0;
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
- await this.mcpServer.connect(transport);
240
- }
241
- await transport.handleRequest(request.raw, reply.raw, request.body);
242
- });
243
- this.app.get("/mcp", async (request, reply) => {
244
- const sessionId = request.headers["mcp-session-id"];
245
- if (!sessionId) {
246
- reply.code(400).send({ error: "Missing MCP session ID." });
247
- return;
248
- }
249
- const transport = this.transports.get(sessionId);
250
- if (!transport) {
251
- 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." });
252
26
  return;
253
27
  }
254
- await transport.handleRequest(request.raw, reply.raw);
255
- });
256
- this.app.delete("/mcp", async (request, reply) => {
257
- const sessionId = request.headers["mcp-session-id"];
258
- if (!sessionId) {
259
- 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." });
260
31
  return;
261
32
  }
262
- const transport = this.transports.get(sessionId);
263
- if (!transport) {
264
- reply.code(404).send({ error: "Unknown MCP session." });
265
- return;
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
+ });
266
44
  }
267
- await transport.handleRequest(request.raw, reply.raw);
268
45
  });
269
46
  await this.app.listen({ port: this.port, host: "127.0.0.1" });
270
47
  }
@@ -274,7 +51,7 @@ var RelayHttpServer = class {
274
51
  };
275
52
 
276
53
  // src/native/bridge.ts
277
- import { randomUUID as randomUUID2 } from "crypto";
54
+ import { randomUUID } from "crypto";
278
55
  var ExtensionBridge = class {
279
56
  constructor(send) {
280
57
  this.send = send;
@@ -339,7 +116,7 @@ var ExtensionBridge = class {
339
116
  });
340
117
  }
341
118
  async ping(timeoutMs = 2e3) {
342
- const id = randomUUID2();
119
+ const id = randomUUID();
343
120
  const message = { type: "bridge.ping", id };
344
121
  return new Promise((resolve) => {
345
122
  const timer = setTimeout(() => {
@@ -356,7 +133,7 @@ var ExtensionBridge = class {
356
133
  }
357
134
  async callTool(name, args, timeoutMs = 3e4) {
358
135
  await this.waitUntilReady();
359
- const id = randomUUID2();
136
+ const id = randomUUID();
360
137
  return new Promise((resolve, reject) => {
361
138
  const timer = setTimeout(() => {
362
139
  this.pending.delete(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-relay",
3
- "version": "0.1.1",
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 MCP.",
19
- "keywords": ["mcp", "chrome", "browser", "automation", "agents"],
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
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- declare function runStdioProxy(): Promise<void>;
3
-
4
- export { runStdioProxy };
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
- };