aipex-mcp-bridge 2.0.0 → 2.0.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/dist/bridge.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  // src/bridge.ts
10
10
  import { randomUUID } from "crypto";
11
- import { createServer as createHttpServer } from "http";
11
+ import { createServer as createHttpServer, request as httpRequest } from "http";
12
12
  import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
13
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
14
  import {
@@ -86,13 +86,13 @@ Usage:
86
86
  aipex-mcp-bridge [options]
87
87
 
88
88
  Options:
89
+ --native-host Run as Chrome Native Messaging host (set automatically by register)
89
90
  --port <port> HTTP port for Streamable HTTP transport (default: ${DEFAULT_HTTP_PORT})
90
- --stdio-only Only serve via stdio, skip HTTP server
91
91
  --help, -h Show this help message
92
92
  --version, -v Show version
93
93
 
94
- The bridge is typically launched automatically by Chrome via Native Messaging,
95
- or by MCP clients (Cursor, Claude Desktop) via stdio.
94
+ When run without --native-host, the bridge serves MCP via stdio (for Cursor, Claude, etc.)
95
+ and forwards tool calls to the native host's HTTP server at localhost:${DEFAULT_HTTP_PORT}.
96
96
  `);
97
97
  process.exit(0);
98
98
  }
@@ -102,7 +102,6 @@ if (cliArgs.includes("--version") || cliArgs.includes("-v")) {
102
102
  }
103
103
  var portIdx = cliArgs.indexOf("--port");
104
104
  var HTTP_PORT = portIdx !== -1 ? parseInt(cliArgs[portIdx + 1], 10) : DEFAULT_HTTP_PORT;
105
- var STDIO_ONLY = cliArgs.includes("--stdio-only");
106
105
  if (isNaN(HTTP_PORT) || HTTP_PORT < 1 || HTTP_PORT > 65535) {
107
106
  process.stderr.write(`Invalid port number. Must be between 1 and 65535.
108
107
  `);
@@ -112,7 +111,7 @@ function log(msg) {
112
111
  process.stderr.write(`[aipex-bridge] ${msg}
113
112
  `);
114
113
  }
115
- var isLaunchedByChrome = !process.stdin.isTTY && !cliArgs.includes("--stdio-only") && !cliArgs.includes("--port");
114
+ var IS_NATIVE_HOST = cliArgs.includes("--native-host");
116
115
  var pendingRequests = /* @__PURE__ */ new Map();
117
116
  function rejectAllPending(reason) {
118
117
  for (const [, entry] of pendingRequests) {
@@ -204,7 +203,7 @@ function handleExtensionMessage(msg) {
204
203
  log(`Unhandled extension message type: ${msg.type}`);
205
204
  }
206
205
  }
207
- async function fetchToolsFromExtension() {
206
+ async function fetchToolsViaExtension() {
208
207
  try {
209
208
  const tools = await sendToExtension(
210
209
  { type: NativeMessageType.LIST_TOOLS, requestId: randomUUID() },
@@ -218,7 +217,7 @@ async function fetchToolsFromExtension() {
218
217
  return cachedTools;
219
218
  }
220
219
  }
221
- async function callToolOnExtension(name, args) {
220
+ async function callToolViaExtension(name, args) {
222
221
  return sendToExtension(
223
222
  {
224
223
  type: NativeMessageType.CALL_TOOL,
@@ -229,14 +228,124 @@ async function callToolOnExtension(name, args) {
229
228
  TOOL_CALL_TIMEOUT_MS
230
229
  );
231
230
  }
232
- function createMcpServerInstance() {
231
+ function httpPost(path, body, timeoutMs) {
232
+ return new Promise((resolve, reject) => {
233
+ const data = JSON.stringify(body);
234
+ const timer = setTimeout(() => {
235
+ reject(new Error(`HTTP request to native host timed out after ${timeoutMs}ms`));
236
+ }, timeoutMs);
237
+ const req = httpRequest(
238
+ {
239
+ hostname: "127.0.0.1",
240
+ port: HTTP_PORT,
241
+ path,
242
+ method: "POST",
243
+ headers: {
244
+ "Content-Type": "application/json",
245
+ "Content-Length": Buffer.byteLength(data)
246
+ }
247
+ },
248
+ (res) => {
249
+ const chunks = [];
250
+ res.on("data", (chunk) => chunks.push(chunk));
251
+ res.on("end", () => {
252
+ clearTimeout(timer);
253
+ const responseBody = Buffer.concat(chunks).toString("utf-8");
254
+ try {
255
+ resolve(JSON.parse(responseBody));
256
+ } catch {
257
+ resolve(responseBody);
258
+ }
259
+ });
260
+ }
261
+ );
262
+ req.on("error", (err) => {
263
+ clearTimeout(timer);
264
+ reject(err);
265
+ });
266
+ req.write(data);
267
+ req.end();
268
+ });
269
+ }
270
+ async function checkNativeHostHealth() {
271
+ try {
272
+ const result = await new Promise((resolve, reject) => {
273
+ const req = httpRequest(
274
+ {
275
+ hostname: "127.0.0.1",
276
+ port: HTTP_PORT,
277
+ path: "/health",
278
+ method: "GET",
279
+ timeout: 3e3
280
+ },
281
+ (res) => {
282
+ const chunks = [];
283
+ res.on("data", (chunk) => chunks.push(chunk));
284
+ res.on("end", () => {
285
+ try {
286
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
287
+ } catch {
288
+ resolve(null);
289
+ }
290
+ });
291
+ }
292
+ );
293
+ req.on("error", reject);
294
+ req.on("timeout", () => {
295
+ req.destroy();
296
+ reject(new Error("timeout"));
297
+ });
298
+ req.end();
299
+ });
300
+ const health = result;
301
+ return health?.status === "ok" && health?.extensionConnected === true;
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+ async function fetchToolsViaHttp() {
307
+ try {
308
+ const jsonRpc = {
309
+ jsonrpc: "2.0",
310
+ id: randomUUID(),
311
+ method: "tools/list",
312
+ params: {}
313
+ };
314
+ const resp = await httpPost("/mcp", jsonRpc, EXTENSION_REQUEST_TIMEOUT_MS);
315
+ if (resp?.result?.tools) {
316
+ cachedTools = resp.result.tools;
317
+ return cachedTools;
318
+ }
319
+ return cachedTools;
320
+ } catch (err) {
321
+ log(`Failed to fetch tools via HTTP: ${err instanceof Error ? err.message : String(err)}`);
322
+ return cachedTools;
323
+ }
324
+ }
325
+ async function callToolViaHttp(name, args) {
326
+ const jsonRpc = {
327
+ jsonrpc: "2.0",
328
+ id: randomUUID(),
329
+ method: "tools/call",
330
+ params: { name, arguments: args }
331
+ };
332
+ return httpPost("/mcp", jsonRpc, TOOL_CALL_TIMEOUT_MS);
333
+ }
334
+ function createMcpServerInstance(useHttp) {
233
335
  const server = new McpServer(
234
336
  { name: "aipex-browser-tools", version: "2.0.0" },
235
337
  { capabilities: { tools: {} } }
236
338
  );
237
339
  server.setRequestHandler(ListToolsRequestSchema, async () => {
238
- if (extensionConnected) {
239
- await fetchToolsFromExtension();
340
+ if (useHttp) {
341
+ const healthy = await checkNativeHostHealth();
342
+ if (healthy) {
343
+ await fetchToolsViaHttp();
344
+ }
345
+ } else {
346
+ if (extensionConnected) {
347
+ await fetchToolsViaExtension();
348
+ }
240
349
  }
241
350
  if (cachedTools.length > 0) {
242
351
  return { tools: cachedTools };
@@ -258,32 +367,68 @@ function createMcpServerInstance() {
258
367
  });
259
368
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
260
369
  const { name, arguments: args } = request.params;
261
- if (!extensionConnected) {
262
- return {
263
- content: [{
264
- type: "text",
265
- text: "Error: AIPex Chrome extension is not connected. Please ensure the extension is running and native messaging is registered."
266
- }],
267
- isError: true
268
- };
269
- }
270
- try {
271
- const result = await callToolOnExtension(name, args || {});
272
- const typedResult = result;
273
- if (typedResult?.content) {
274
- return typedResult;
370
+ if (useHttp) {
371
+ const healthy = await checkNativeHostHealth();
372
+ if (!healthy) {
373
+ return {
374
+ content: [{
375
+ type: "text",
376
+ text: "Error: AIPex native host is not running. Please ensure the Chrome extension is running and native messaging is registered (npm install -g aipex-mcp-bridge && aipex-mcp-bridge register)."
377
+ }],
378
+ isError: true
379
+ };
380
+ }
381
+ try {
382
+ const resp = await callToolViaHttp(name, args || {});
383
+ if (resp?.result) {
384
+ return resp.result;
385
+ }
386
+ if (resp?.error) {
387
+ return {
388
+ content: [{ type: "text", text: `Error: ${resp.error.message || JSON.stringify(resp.error)}` }],
389
+ isError: true
390
+ };
391
+ }
392
+ return {
393
+ content: [{ type: "text", text: JSON.stringify(resp, null, 2) }]
394
+ };
395
+ } catch (error) {
396
+ return {
397
+ content: [{
398
+ type: "text",
399
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
400
+ }],
401
+ isError: true
402
+ };
403
+ }
404
+ } else {
405
+ if (!extensionConnected) {
406
+ return {
407
+ content: [{
408
+ type: "text",
409
+ text: "Error: AIPex Chrome extension is not connected."
410
+ }],
411
+ isError: true
412
+ };
413
+ }
414
+ try {
415
+ const result = await callToolViaExtension(name, args || {});
416
+ const typedResult = result;
417
+ if (typedResult?.content) {
418
+ return typedResult;
419
+ }
420
+ return {
421
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
422
+ };
423
+ } catch (error) {
424
+ return {
425
+ content: [{
426
+ type: "text",
427
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
428
+ }],
429
+ isError: true
430
+ };
275
431
  }
276
- return {
277
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
278
- };
279
- } catch (error) {
280
- return {
281
- content: [{
282
- type: "text",
283
- text: `Error: ${error instanceof Error ? error.message : String(error)}`
284
- }],
285
- isError: true
286
- };
287
432
  }
288
433
  });
289
434
  return server;
@@ -305,19 +450,14 @@ async function startNativeMessagingMode() {
305
450
  }
306
451
  async function startStdioMcpMode() {
307
452
  log("Starting in stdio MCP mode (launched by MCP client)");
308
- const server = createMcpServerInstance();
453
+ const server = createMcpServerInstance(true);
309
454
  const transport = new StdioServerTransport();
310
455
  await server.connect(transport);
311
- if (!STDIO_ONLY) {
312
- await startHttpMcpServer();
313
- log(`stdio MCP server ready, HTTP also available on port ${HTTP_PORT}`);
314
- } else {
315
- log("stdio MCP server ready (HTTP disabled)");
316
- }
456
+ log("stdio MCP server ready (tool calls proxy via HTTP to native host at localhost:" + HTTP_PORT + ")");
317
457
  }
318
458
  async function startHttpMcpServer() {
319
459
  const httpServer = createHttpServer(async (req, res) => {
320
- res.setHeader("Access-Control-Allow-Origin", "chrome-extension://*");
460
+ res.setHeader("Access-Control-Allow-Origin", "*");
321
461
  res.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS");
322
462
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
323
463
  res.setHeader("Access-Control-Expose-Headers", "mcp-session-id");
@@ -348,21 +488,11 @@ async function startHttpMcpServer() {
348
488
  res.end(JSON.stringify({ error: "Invalid JSON" }));
349
489
  return;
350
490
  }
351
- const sessionId = req.headers["mcp-session-id"] || void 0;
352
- let transport;
353
- if (!sessionId) {
354
- transport = new StreamableHTTPServerTransport({
355
- sessionIdGenerator: () => randomUUID()
356
- });
357
- const server = createMcpServerInstance();
358
- await server.connect(transport);
359
- } else {
360
- transport = new StreamableHTTPServerTransport({
361
- sessionIdGenerator: void 0
362
- });
363
- const server = createMcpServerInstance();
364
- await server.connect(transport);
365
- }
491
+ const transport = new StreamableHTTPServerTransport({
492
+ sessionIdGenerator: () => randomUUID()
493
+ });
494
+ const server = createMcpServerInstance(false);
495
+ await server.connect(transport);
366
496
  await transport.handleRequest(req, res, parsed);
367
497
  return;
368
498
  }
@@ -383,11 +513,11 @@ async function startHttpMcpServer() {
383
513
  async function main() {
384
514
  const subcommand = cliArgs[0];
385
515
  if (subcommand === "register" || subcommand === "doctor" || subcommand === "fix-permissions" || subcommand === "unregister") {
386
- const cli = await import("./cli-VTRZIIXB.js");
516
+ const cli = await import("./cli-M2UUOVPA.js");
387
517
  await cli.runCli(cliArgs);
388
518
  return;
389
519
  }
390
- if (isLaunchedByChrome) {
520
+ if (IS_NATIVE_HOST) {
391
521
  await startNativeMessagingMode();
392
522
  } else {
393
523
  await startStdioMcpMode();
@@ -121,11 +121,11 @@ function createWrapperScript(nodePath, bridgePath) {
121
121
  const wrapperPath = join(wrapperDir, wrapperName);
122
122
  if (os === "win32") {
123
123
  writeFileSync(wrapperPath, `@echo off\r
124
- "${nodePath}" "${bridgePath}" %*\r
124
+ "${nodePath}" "${bridgePath}" --native-host %*\r
125
125
  `);
126
126
  } else {
127
127
  writeFileSync(wrapperPath, `#!/bin/bash
128
- exec "${nodePath}" "${bridgePath}" "$@"
128
+ exec "${nodePath}" "${bridgePath}" --native-host "$@"
129
129
  `);
130
130
  chmodSync(wrapperPath, 493);
131
131
  }
@@ -122,11 +122,11 @@ function createWrapperScript(nodePath, bridgePath) {
122
122
  const wrapperPath = join(wrapperDir, wrapperName);
123
123
  if (os === "win32") {
124
124
  writeFileSync(wrapperPath, `@echo off\r
125
- "${nodePath}" "${bridgePath}" %*\r
125
+ "${nodePath}" "${bridgePath}" --native-host %*\r
126
126
  `);
127
127
  } else {
128
128
  writeFileSync(wrapperPath, `#!/bin/bash
129
- exec "${nodePath}" "${bridgePath}" "$@"
129
+ exec "${nodePath}" "${bridgePath}" --native-host "$@"
130
130
  `);
131
131
  chmodSync(wrapperPath, 493);
132
132
  }
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  register,
4
4
  runDiagnostics,
5
5
  unregister
6
- } from "./chunk-KWMK3JEL.js";
6
+ } from "./chunk-3WJ3TSAN.js";
7
7
  import "./chunk-YZDMY4WS.js";
8
8
 
9
9
  // src/cli.ts
package/dist/register.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  register,
4
4
  runDiagnostics,
5
5
  unregister
6
- } from "./chunk-KWMK3JEL.js";
6
+ } from "./chunk-3WJ3TSAN.js";
7
7
  import "./chunk-YZDMY4WS.js";
8
8
  export {
9
9
  fixPermissions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aipex-mcp-bridge",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "MCP bridge that connects AI agents (Cursor, Claude, VS Code Copilot, etc.) to the AIPex browser extension via Native Messaging",
5
5
  "type": "module",
6
6
  "bin": {