@chatpanel/bridge 0.6.0 → 0.7.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/package.json +1 -1
- package/src/engines/codex.js +10 -1
- package/src/engines/custom.js +21 -2
- package/src/env.js +13 -0
- package/src/mcp-local.js +0 -0
- package/src/server.js +95 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local bridge that exposes the AI coding agents installed on your machine — Claude Code (CLI), Codex (CLI), and Gemini CLI — to the ChatPanel Chrome extension over a localhost SSE endpoint. Bring your own agent.",
|
|
6
6
|
"keywords": [
|
package/src/engines/codex.js
CHANGED
|
@@ -19,7 +19,7 @@ import { readFile, unlink, writeFile } from 'node:fs/promises';
|
|
|
19
19
|
import { existsSync, mkdirSync, symlinkSync, readFileSync } from 'node:fs';
|
|
20
20
|
import os from 'node:os';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
-
import { findAgentBin } from '../env.js';
|
|
22
|
+
import { findAgentBin, selfMcpStdio } from '../env.js';
|
|
23
23
|
|
|
24
24
|
// Idle timeout: re-armed on every stdout/stderr chunk, so a long run that keeps
|
|
25
25
|
// streaming never trips it — only true silence does. Override with
|
|
@@ -146,6 +146,15 @@ export async function chat({ messages, system, options, images }, emit) {
|
|
|
146
146
|
// local-config mode). The sandbox above still bounds what can actually happen.
|
|
147
147
|
args.push('-c', 'approval_policy=never');
|
|
148
148
|
if (REASONING) args.push('-c', `model_reasoning_effort=${REASONING}`);
|
|
149
|
+
// Browser tools: register the bridge's MCP server as a stdio MCP server (the
|
|
150
|
+
// bridge binary in --mcp-stdio mode), so Codex can call our page-action tools.
|
|
151
|
+
// `-c key=value` parses value as TOML; JSON.stringify yields valid TOML here.
|
|
152
|
+
if (options.mcp?.url) {
|
|
153
|
+
const name = options.mcp.serverName || 'chatpanel_browser';
|
|
154
|
+
const { command, args: pargs } = selfMcpStdio(options.mcp.url);
|
|
155
|
+
args.push('-c', `mcp_servers.${name}.command=${JSON.stringify(command)}`);
|
|
156
|
+
args.push('-c', `mcp_servers.${name}.args=${JSON.stringify(pargs)}`);
|
|
157
|
+
}
|
|
149
158
|
if (options.model) args.push('-m', options.model);
|
|
150
159
|
for (const f of imageFiles) args.push('-i', f); // attach images to the initial prompt
|
|
151
160
|
args.push('-');
|
package/src/engines/custom.js
CHANGED
|
@@ -19,7 +19,7 @@ import { spawn } from 'node:child_process';
|
|
|
19
19
|
import { writeFile, unlink } from 'node:fs/promises';
|
|
20
20
|
import os from 'node:os';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
-
import { resolveCommand, buildSpawnSpec } from '../env.js';
|
|
22
|
+
import { resolveCommand, buildSpawnSpec, selfMcpStdio } from '../env.js';
|
|
23
23
|
import { isProEntitled } from '../entitlement.js';
|
|
24
24
|
import { handleMessage } from './claude.js';
|
|
25
25
|
|
|
@@ -187,7 +187,26 @@ export async function runSpec(spec, { messages, system, options = {}, images },
|
|
|
187
187
|
// just before the prompt (arg mode) or get appended (stdin mode).
|
|
188
188
|
const tag = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
189
189
|
const imageFiles = spec.imageArg ? await writeImages(images, tag) : [];
|
|
190
|
-
const
|
|
190
|
+
const mcpFiles = [];
|
|
191
|
+
const cleanup = () => [...imageFiles, ...mcpFiles].forEach((f) => unlink(f).catch(() => {}));
|
|
192
|
+
|
|
193
|
+
// Browser tools: when armed (options.mcp) AND this CLI knows how to take an MCP
|
|
194
|
+
// config file (spec.mcpArg, e.g. "--mcp-config {file}"), write a standard
|
|
195
|
+
// mcpServers JSON pointing at the bridge's stdio MCP proxy and inject the flag.
|
|
196
|
+
// Covers any CLI that reads the de-facto {mcpServers:{name:{command,args}}} shape.
|
|
197
|
+
if (options.mcp?.url && spec.mcpArg) {
|
|
198
|
+
const name = options.mcp.serverName || 'chatpanel_browser';
|
|
199
|
+
const { command, args: pargs } = selfMcpStdio(options.mcp.url);
|
|
200
|
+
const cfgFile = path.join(os.tmpdir(), `chatpanel-mcp-${tag}.json`);
|
|
201
|
+
await writeFile(cfgFile, JSON.stringify({ mcpServers: { [name]: { command, args: pargs } } }));
|
|
202
|
+
mcpFiles.push(cfgFile);
|
|
203
|
+
const tmpl = String(spec.mcpArg);
|
|
204
|
+
const tokens = tmpl.includes('{file}')
|
|
205
|
+
? tmpl.replaceAll('{file}', cfgFile).split(/\s+/).filter(Boolean)
|
|
206
|
+
: [...tmpl.split(/\s+/).filter(Boolean), cfgFile];
|
|
207
|
+
args = [...tokens, ...args];
|
|
208
|
+
}
|
|
209
|
+
|
|
191
210
|
const imageTokens = imageTokensFor(spec.imageArg, imageFiles);
|
|
192
211
|
let placedImages = false;
|
|
193
212
|
if (imageTokens.length) {
|
package/src/env.js
CHANGED
|
@@ -48,6 +48,19 @@ export function isCompiledBinary() {
|
|
|
48
48
|
return !(base.startsWith('node') || base.startsWith('bun'));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// How to re-invoke THIS bridge as a child process running the stdio↔HTTP MCP
|
|
52
|
+
// proxy (`--mcp-stdio <url>`). Lets ANY stdio-capable MCP CLI (Codex, a custom
|
|
53
|
+
// CLI) reach the bridge's HTTP MCP server using the bridge itself as the server
|
|
54
|
+
// command — no extra runtime to install. Compiled binary → the binary; Node →
|
|
55
|
+
// node + this entry script. Returns { command, args }.
|
|
56
|
+
export function selfMcpStdio(url) {
|
|
57
|
+
const args = ['--mcp-stdio', url];
|
|
58
|
+
if (isCompiledBinary()) return { command: process.execPath, args };
|
|
59
|
+
// Node/Bun: re-run this same entry script (server.js) under the same runtime.
|
|
60
|
+
const entry = process.argv[1] || path.join(process.cwd(), 'src', 'server.js');
|
|
61
|
+
return { command: process.execPath, args: [entry, ...args] };
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
// Ask the user's login shell to locate a command — no hardcoded locations, works
|
|
52
65
|
// wherever the user actually installed it.
|
|
53
66
|
function shellWhich(name) {
|
package/src/mcp-local.js
ADDED
|
Binary file
|
package/src/server.js
CHANGED
|
@@ -25,11 +25,12 @@ import * as custom from './engines/custom.js';
|
|
|
25
25
|
import { installService, uninstallService, serviceStatus, restartService } from './service.js';
|
|
26
26
|
import { enrichPath, findAgentBin, resolveCommand } from './env.js';
|
|
27
27
|
import { checkForUpdate, selfUpdate } from './update.js';
|
|
28
|
+
import { callLocalMcp } from './mcp-local.js';
|
|
28
29
|
|
|
29
30
|
// Hardcoded (not read from package.json) so it survives Bun's single-file
|
|
30
31
|
// --compile, where package.json isn't on a readable FS. CI fails the publish if
|
|
31
32
|
// this drifts from package.json, so the two can't silently diverge.
|
|
32
|
-
const VERSION = '0.
|
|
33
|
+
const VERSION = '0.7.0';
|
|
33
34
|
const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
|
|
34
35
|
const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
|
|
35
36
|
|
|
@@ -273,6 +274,36 @@ async function handleMcp(req, res, sessionId) {
|
|
|
273
274
|
return fail(-32601, `Method not found: ${msg.method}`);
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
// POST /mcp-local — proxy one JSON-RPC message to a user-configured STDIO MCP
|
|
278
|
+
// server that the bridge spawns and keeps alive. Body: { server:{id,command,args,
|
|
279
|
+
// env?,cwd?}, message }. Returns the full JSON-RPC response (or 202 for a
|
|
280
|
+
// notification). Lets the extension use local MCP servers it can't spawn itself.
|
|
281
|
+
async function handleMcpLocal(req, res) {
|
|
282
|
+
let body;
|
|
283
|
+
try {
|
|
284
|
+
body = await readBody(req);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return json(res, 400, { error: 'Bad JSON: ' + e.message });
|
|
287
|
+
}
|
|
288
|
+
const server = body.server || {};
|
|
289
|
+
const message = body.message;
|
|
290
|
+
if (!server.command || !message) return json(res, 400, { error: 'need server.command and message' });
|
|
291
|
+
try {
|
|
292
|
+
const result = await callLocalMcp(
|
|
293
|
+
{ key: server.id, command: server.command, args: server.args, env: server.env, cwd: server.cwd },
|
|
294
|
+
message,
|
|
295
|
+
);
|
|
296
|
+
if (message.id == null) { res.writeHead(202); return res.end(); }
|
|
297
|
+
return json(res, 200, result); // the full JSON-RPC response message
|
|
298
|
+
} catch (e) {
|
|
299
|
+
return json(res, 200, {
|
|
300
|
+
jsonrpc: '2.0',
|
|
301
|
+
id: message.id ?? null,
|
|
302
|
+
error: { code: -32000, message: String(e?.message || e) },
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
276
307
|
// POST /tool-result — the extension returns a relayed tool's result.
|
|
277
308
|
async function handleToolResult(req, res) {
|
|
278
309
|
let body;
|
|
@@ -400,6 +431,7 @@ const server = createServer(async (req, res) => {
|
|
|
400
431
|
if (req.method === 'DELETE') { deleteSession(sid); res.writeHead(204); return res.end(); }
|
|
401
432
|
}
|
|
402
433
|
if (req.method === 'POST' && url.pathname === '/tool-result') return handleToolResult(req, res);
|
|
434
|
+
if (req.method === 'POST' && url.pathname === '/mcp-local') return handleMcpLocal(req, res);
|
|
403
435
|
if (req.method === 'POST' && url.pathname === '/complete') return handleComplete(req, res);
|
|
404
436
|
if (req.method === 'POST' && url.pathname === '/list-models') return handleListModels(req, res);
|
|
405
437
|
if (req.method === 'POST' && url.pathname === '/agent-check') return handleAgentCheck(req, res);
|
|
@@ -415,6 +447,59 @@ function log(level, msg) {
|
|
|
415
447
|
fn(`[chatpanel-bridge] ${msg}`);
|
|
416
448
|
}
|
|
417
449
|
|
|
450
|
+
// `--mcp-stdio <url>` — run as a stdio↔HTTP MCP proxy: read newline-delimited
|
|
451
|
+
// JSON-RPC from stdin, forward each message to the bridge's HTTP MCP endpoint
|
|
452
|
+
// (<url> = http://127.0.0.1:PORT/mcp/<session>), and write responses to stdout.
|
|
453
|
+
// This lets ANY stdio-MCP CLI (Codex, a custom CLI) use the browser tools with
|
|
454
|
+
// the bridge binary itself as the MCP server command — no extra runtime needed.
|
|
455
|
+
function runMcpStdioProxy(url) {
|
|
456
|
+
let buf = '';
|
|
457
|
+
const queue = [];
|
|
458
|
+
let draining = false;
|
|
459
|
+
let ended = false;
|
|
460
|
+
const maybeExit = () => { if (ended && !draining && !queue.length) process.exit(0); };
|
|
461
|
+
const drain = async () => {
|
|
462
|
+
if (draining) return;
|
|
463
|
+
draining = true;
|
|
464
|
+
while (queue.length) {
|
|
465
|
+
const line = queue.shift();
|
|
466
|
+
let msg;
|
|
467
|
+
try { msg = JSON.parse(line); } catch { continue; }
|
|
468
|
+
try {
|
|
469
|
+
const res = await fetch(url, {
|
|
470
|
+
method: 'POST',
|
|
471
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
472
|
+
body: line,
|
|
473
|
+
});
|
|
474
|
+
if (msg.id == null) continue; // notification — no response expected
|
|
475
|
+
const text = (await res.text()).trim();
|
|
476
|
+
if (text) process.stdout.write(text + '\n');
|
|
477
|
+
} catch (e) {
|
|
478
|
+
if (msg.id != null) {
|
|
479
|
+
process.stdout.write(
|
|
480
|
+
JSON.stringify({ jsonrpc: '2.0', id: msg.id, error: { code: -32000, message: String(e?.message || e) } }) + '\n',
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
draining = false;
|
|
486
|
+
maybeExit(); // stdin closed mid-flight → exit only after the queue is drained
|
|
487
|
+
};
|
|
488
|
+
process.stdin.setEncoding('utf8');
|
|
489
|
+
process.stdin.on('data', (chunk) => {
|
|
490
|
+
buf += chunk;
|
|
491
|
+
let nl;
|
|
492
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
493
|
+
const line = buf.slice(0, nl).trim();
|
|
494
|
+
buf = buf.slice(nl + 1);
|
|
495
|
+
if (line) queue.push(line);
|
|
496
|
+
}
|
|
497
|
+
drain();
|
|
498
|
+
});
|
|
499
|
+
process.stdin.on('end', () => { ended = true; maybeExit(); });
|
|
500
|
+
process.stdin.resume();
|
|
501
|
+
}
|
|
502
|
+
|
|
418
503
|
function startServer() {
|
|
419
504
|
enrichPath(); // so codex/gemini are found even under a minimal service PATH
|
|
420
505
|
server.listen(PORT, HOST, async () => {
|
|
@@ -488,7 +573,15 @@ function runCli() {
|
|
|
488
573
|
return false;
|
|
489
574
|
}
|
|
490
575
|
|
|
491
|
-
|
|
576
|
+
const mcpStdioIdx = process.argv.indexOf('--mcp-stdio');
|
|
577
|
+
if (mcpStdioIdx >= 0) {
|
|
578
|
+
const url = process.argv[mcpStdioIdx + 1];
|
|
579
|
+
if (!url) {
|
|
580
|
+
console.error('--mcp-stdio requires a URL');
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
runMcpStdioProxy(url);
|
|
584
|
+
} else if (process.argv.includes('--update')) {
|
|
492
585
|
(async () => {
|
|
493
586
|
try {
|
|
494
587
|
const r = await selfUpdate(VERSION);
|