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
|
-
|
|
95
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
239
|
-
await
|
|
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 (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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-
|
|
516
|
+
const cli = await import("./cli-M2UUOVPA.js");
|
|
387
517
|
await cli.runCli(cliArgs);
|
|
388
518
|
return;
|
|
389
519
|
}
|
|
390
|
-
if (
|
|
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
package/dist/register.js
CHANGED
package/package.json
CHANGED