promethios-bridge 2.1.2 → 2.1.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promethios-bridge",
3
- "version": "2.1.2",
3
+ "version": "2.1.5",
4
4
  "description": "Run Promethios agent frameworks locally on your computer with full file, terminal, browser access, ambient context capture, and the always-on-top floating chat overlay. Native Framework Mode supports OpenClaw and other frameworks via the bridge.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/bridge.js CHANGED
@@ -21,7 +21,8 @@ const ora = require('ora');
21
21
  const fetch = require('node-fetch');
22
22
  const { executeLocalTool } = require('./executor');
23
23
  const { captureContext } = require('./contextCapture');
24
- const { startMcpServer } = require('./mcp-server');
24
+ const { startMcpServer, FULL_MANIFEST } = require('./mcp-server');
25
+ const mcpRegistry = require('./mcp-registry');
25
26
  const { setPinnedRegion, setPinnedApps, registerBrowserPageAccessor } = require('./tools/desktop');
26
27
 
27
28
  // Wire the browser-dom tools to the shared Playwright context.
@@ -210,6 +211,111 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
210
211
  // Health check
211
212
  app.get('/health', (req, res) => res.json({ ok: true, version: require('../package.json').version }));
212
213
 
214
+ // ── /open-external ─────────────────────────────────────────────────────────
215
+ // POST /open-external?url=<encoded-url>
216
+ // Called by the Promethios web app when the user clicks "Open [Provider] ↗"
217
+ // in a provider thread. Opens the URL in the user's real default browser.
218
+ // No auth required — only accessible from localhost.
219
+ app.post('/open-external', (req, res) => {
220
+ const url = (req.query.url || req.body?.url || '').trim();
221
+ if (!url || !/^https?:\/\//.test(url)) {
222
+ return res.status(400).json({ error: 'Invalid or missing url parameter' });
223
+ }
224
+ const { exec } = require('child_process');
225
+ const platform = process.platform;
226
+ let cmd;
227
+ if (platform === 'win32') {
228
+ cmd = `start "" "${url.replace(/"/g, '')}"`;
229
+ } else if (platform === 'darwin') {
230
+ cmd = `open "${url.replace(/"/g, '')}"`;
231
+ } else {
232
+ cmd = `xdg-open "${url.replace(/"/g, '')}"`;
233
+ }
234
+ exec(cmd, (err) => {
235
+ if (err) console.error('[open-external] Failed to open URL:', err.message);
236
+ });
237
+ res.json({ status: 'ok', url });
238
+ });
239
+
240
+ // ── /mcp/* ── MCP Marketplace endpoints ────────────────────────────────────────────
241
+ // All /mcp/* endpoints are localhost-only (no auth token required).
242
+ // They are called by the Promethios web app marketplace UI.
243
+
244
+ // GET /mcp/catalog — serve the curated marketplace catalog
245
+ app.get('/mcp/catalog', (req, res) => {
246
+ try {
247
+ const catalog = require('./mcp-catalog.json');
248
+ const installed = mcpRegistry.listInstalled();
249
+ const installedIds = new Set(installed.map(e => e.id));
250
+ const enriched = catalog.map(item => ({ ...item, installed: installedIds.has(item.id) }));
251
+ res.json({ ok: true, catalog: enriched });
252
+ } catch (err) {
253
+ res.status(500).json({ error: err.message });
254
+ }
255
+ });
256
+
257
+ // GET /mcp/list — list all installed MCP servers (built-ins + user-installed)
258
+ app.get('/mcp/list', (req, res) => {
259
+ const all = mcpRegistry.listAll();
260
+ res.json({ ok: true, servers: all });
261
+ });
262
+
263
+ // GET /mcp/tools — aggregated tool list from all running MCP servers
264
+ // Also includes the built-in Promethios desktop + android tools.
265
+ app.get('/mcp/tools', (req, res) => {
266
+ const builtinTools = FULL_MANIFEST.map(t => ({ ...t, _mcpServerId: 'promethios-desktop' }));
267
+ const installedTools = mcpRegistry.getAggregatedTools();
268
+ res.json({ ok: true, tools: [...builtinTools, ...installedTools] });
269
+ });
270
+
271
+ // POST /mcp/install — install an MCP server from the marketplace
272
+ // Body: { id, name, package, description, icon, env, args }
273
+ app.post('/mcp/install', async (req, res) => {
274
+ const { id, name, package: pkg, description, icon, env, args } = req.body || {};
275
+ if (!id || !pkg) return res.status(400).json({ error: 'id and package are required' });
276
+ if (mcpRegistry.isInstalled(id)) {
277
+ return res.status(409).json({ error: `${id} is already installed` });
278
+ }
279
+ log(`[mcp-marketplace] Installing ${pkg}...`);
280
+ const result = await mcpRegistry.npmInstall(pkg);
281
+ if (!result.ok) {
282
+ return res.status(500).json({ error: 'npm install failed', detail: result.error });
283
+ }
284
+ const entry = { id, name, package: pkg, version: result.version, description, icon, env: env || {}, args: args || [] };
285
+ mcpRegistry.addEntry(entry);
286
+ // Start the server immediately so tools are available right away
287
+ try {
288
+ const tools = await mcpRegistry.startServer(entry, log);
289
+ return res.json({ ok: true, id, version: result.version, toolCount: tools.length });
290
+ } catch (err) {
291
+ return res.json({ ok: true, id, version: result.version, toolCount: 0, warning: 'Installed but failed to start: ' + err.message });
292
+ }
293
+ });
294
+
295
+ // DELETE /mcp/uninstall — uninstall an MCP server
296
+ // Body: { id }
297
+ app.delete('/mcp/uninstall', async (req, res) => {
298
+ const { id } = req.body || {};
299
+ if (!id) return res.status(400).json({ error: 'id is required' });
300
+ const entry = mcpRegistry.getById(id);
301
+ if (!entry) return res.status(404).json({ error: `${id} not found` });
302
+ mcpRegistry.stopServer(id);
303
+ const uninstallResult = await mcpRegistry.npmUninstall(entry.package);
304
+ mcpRegistry.removeEntry(id);
305
+ res.json({ ok: true, id, npmResult: uninstallResult });
306
+ });
307
+
308
+ // PATCH /mcp/toggle — enable or disable an installed MCP server
309
+ // Body: { id, enabled }
310
+ app.patch('/mcp/toggle', (req, res) => {
311
+ const { id, enabled } = req.body || {};
312
+ if (!id || typeof enabled !== 'boolean') return res.status(400).json({ error: 'id and enabled (boolean) required' });
313
+ if (!mcpRegistry.isInstalled(id)) return res.status(404).json({ error: `${id} not found` });
314
+ mcpRegistry.setEnabled(id, enabled);
315
+ if (!enabled) mcpRegistry.stopServer(id);
316
+ res.json({ ok: true, id, enabled });
317
+ });
318
+
213
319
  // ── /status: used by the Electron overlay to auto-connect without manual token entry ──
214
320
  // Only accessible from localhost (127.0.0.1 or ::1) for security.
215
321
  let bridgeUsername = null; // set after registerBridge resolves
@@ -818,7 +924,12 @@ async function startBridge({ setupToken, apiBase, port, dev }) {
818
924
  process.exit(1);
819
925
  }
820
926
 
821
- // ── Step 3b: Check for updates (non-blocking) ───────────────────────────
927
+ // // ── Step 3b: Start installed MCP servers (non-blocking) ───────────────────
928
+ mcpRegistry.startAllEnabled(log).catch(err => {
929
+ log('[mcp-registry] startAllEnabled error:', err.message);
930
+ });
931
+
932
+ // ── Step 3c: Check for updates (non-blocking) ───────────────────────
822
933
  const currentVersion = require('../package.json').version;
823
934
  checkForUpdates(currentVersion, log).catch(() => {}); // fire-and-forget
824
935
 
@@ -0,0 +1,163 @@
1
+ [
2
+ {
3
+ "id": "github-mcp",
4
+ "name": "GitHub",
5
+ "package": "@modelcontextprotocol/server-github",
6
+ "description": "Read repos, create pull requests, check issues, search code, and manage GitHub Actions — all from any AI provider in Promethios.",
7
+ "icon": "🐙",
8
+ "category": "developer",
9
+ "featured": true,
10
+ "requiresEnv": [
11
+ { "key": "GITHUB_PERSONAL_ACCESS_TOKEN", "label": "GitHub Personal Access Token", "hint": "github.com → Settings → Developer settings → Personal access tokens" }
12
+ ],
13
+ "args": [],
14
+ "links": {
15
+ "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/github",
16
+ "npm": "https://www.npmjs.com/package/@modelcontextprotocol/server-github"
17
+ }
18
+ },
19
+ {
20
+ "id": "filesystem-mcp",
21
+ "name": "File System",
22
+ "package": "@modelcontextprotocol/server-filesystem",
23
+ "description": "Give any AI provider read and write access to files and folders on your computer. Scope it to specific directories for safety.",
24
+ "icon": "📁",
25
+ "category": "productivity",
26
+ "featured": true,
27
+ "requiresEnv": [],
28
+ "args": ["--allowed-directories", "~/Documents", "~/Desktop"],
29
+ "links": {
30
+ "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem",
31
+ "npm": "https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem"
32
+ }
33
+ },
34
+ {
35
+ "id": "brave-search-mcp",
36
+ "name": "Brave Search",
37
+ "package": "@modelcontextprotocol/server-brave-search",
38
+ "description": "Real-time web search for any AI provider. No tracking, no filter bubbles. Requires a free Brave Search API key.",
39
+ "icon": "🦁",
40
+ "category": "search",
41
+ "featured": true,
42
+ "requiresEnv": [
43
+ { "key": "BRAVE_API_KEY", "label": "Brave Search API Key", "hint": "api.search.brave.com → Get API Key (free tier available)" }
44
+ ],
45
+ "args": [],
46
+ "links": {
47
+ "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search",
48
+ "npm": "https://www.npmjs.com/package/@modelcontextprotocol/server-brave-search"
49
+ }
50
+ },
51
+ {
52
+ "id": "postgres-mcp",
53
+ "name": "PostgreSQL",
54
+ "package": "@modelcontextprotocol/server-postgres",
55
+ "description": "Connect any AI provider to your PostgreSQL database. Run queries, inspect schemas, and analyze data without leaving your AI session.",
56
+ "icon": "🐘",
57
+ "category": "database",
58
+ "featured": false,
59
+ "requiresEnv": [
60
+ { "key": "POSTGRES_CONNECTION_STRING", "label": "PostgreSQL Connection String", "hint": "e.g. postgresql://user:password@localhost:5432/mydb" }
61
+ ],
62
+ "args": [],
63
+ "links": {
64
+ "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/postgres",
65
+ "npm": "https://www.npmjs.com/package/@modelcontextprotocol/server-postgres"
66
+ }
67
+ },
68
+ {
69
+ "id": "sqlite-mcp",
70
+ "name": "SQLite",
71
+ "package": "@modelcontextprotocol/server-sqlite",
72
+ "description": "Query and write to local SQLite databases. Perfect for local apps, Electron data stores, and offline-first projects.",
73
+ "icon": "🗃️",
74
+ "category": "database",
75
+ "featured": false,
76
+ "requiresEnv": [],
77
+ "args": [],
78
+ "links": {
79
+ "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite",
80
+ "npm": "https://www.npmjs.com/package/@modelcontextprotocol/server-sqlite"
81
+ }
82
+ },
83
+ {
84
+ "id": "slack-mcp",
85
+ "name": "Slack",
86
+ "package": "@modelcontextprotocol/server-slack",
87
+ "description": "Read channels, send messages, search conversations, and manage your Slack workspace from any AI provider in Promethios.",
88
+ "icon": "💬",
89
+ "category": "communication",
90
+ "featured": false,
91
+ "requiresEnv": [
92
+ { "key": "SLACK_BOT_TOKEN", "label": "Slack Bot Token", "hint": "api.slack.com → Your Apps → OAuth & Permissions → Bot User OAuth Token" },
93
+ { "key": "SLACK_TEAM_ID", "label": "Slack Team ID", "hint": "Found in your Slack workspace URL: app.slack.com/client/TXXXXXXXX" }
94
+ ],
95
+ "args": [],
96
+ "links": {
97
+ "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/slack",
98
+ "npm": "https://www.npmjs.com/package/@modelcontextprotocol/server-slack"
99
+ }
100
+ },
101
+ {
102
+ "id": "google-maps-mcp",
103
+ "name": "Google Maps",
104
+ "package": "@modelcontextprotocol/server-google-maps",
105
+ "description": "Geocoding, directions, place search, and distance calculations. Let any AI provider reason about location and navigation.",
106
+ "icon": "🗺️",
107
+ "category": "productivity",
108
+ "featured": false,
109
+ "requiresEnv": [
110
+ { "key": "GOOGLE_MAPS_API_KEY", "label": "Google Maps API Key", "hint": "console.cloud.google.com → APIs & Services → Credentials" }
111
+ ],
112
+ "args": [],
113
+ "links": {
114
+ "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps",
115
+ "npm": "https://www.npmjs.com/package/@modelcontextprotocol/server-google-maps"
116
+ }
117
+ },
118
+ {
119
+ "id": "memory-mcp",
120
+ "name": "Memory",
121
+ "package": "@modelcontextprotocol/server-memory",
122
+ "description": "Persistent knowledge graph memory for AI providers. Store facts, relationships, and context that survives across sessions.",
123
+ "icon": "🧠",
124
+ "category": "productivity",
125
+ "featured": true,
126
+ "requiresEnv": [],
127
+ "args": [],
128
+ "links": {
129
+ "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/memory",
130
+ "npm": "https://www.npmjs.com/package/@modelcontextprotocol/server-memory"
131
+ }
132
+ },
133
+ {
134
+ "id": "puppeteer-mcp",
135
+ "name": "Puppeteer Browser",
136
+ "package": "@modelcontextprotocol/server-puppeteer",
137
+ "description": "Full browser automation — navigate pages, fill forms, click elements, take screenshots, and scrape content from any website.",
138
+ "icon": "🤖",
139
+ "category": "automation",
140
+ "featured": true,
141
+ "requiresEnv": [],
142
+ "args": [],
143
+ "links": {
144
+ "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer",
145
+ "npm": "https://www.npmjs.com/package/@modelcontextprotocol/server-puppeteer"
146
+ }
147
+ },
148
+ {
149
+ "id": "sequential-thinking-mcp",
150
+ "name": "Sequential Thinking",
151
+ "package": "@modelcontextprotocol/server-sequential-thinking",
152
+ "description": "Structured multi-step reasoning for complex problems. Helps AI providers break down tasks, revise plans, and think through edge cases.",
153
+ "icon": "🔗",
154
+ "category": "reasoning",
155
+ "featured": false,
156
+ "requiresEnv": [],
157
+ "args": [],
158
+ "links": {
159
+ "docs": "https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking",
160
+ "npm": "https://www.npmjs.com/package/@modelcontextprotocol/server-sequential-thinking"
161
+ }
162
+ }
163
+ ]
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Promethios MCP Registry
3
+ *
4
+ * Manages the local registry of user-installed MCP servers.
5
+ * Each entry describes an MCP server package that the bridge
6
+ * installs, starts as a subprocess, and exposes to all connected
7
+ * AI providers (Manus, Claude, ChatGPT, OpenClaw, etc.).
8
+ *
9
+ * Registry file location:
10
+ * %APPDATA%/promethios/mcp-registry.json (Windows)
11
+ * ~/.config/promethios/mcp-registry.json (Linux/macOS)
12
+ *
13
+ * Registry entry schema:
14
+ * {
15
+ * id: string — unique slug, e.g. "github-mcp"
16
+ * name: string — display name, e.g. "GitHub"
17
+ * package: string — npm package name, e.g. "@modelcontextprotocol/server-github"
18
+ * version: string — installed version, e.g. "1.2.0"
19
+ * description: string — short description shown in marketplace
20
+ * icon: string — emoji or URL
21
+ * env: object — env vars required (keys only, values set by user)
22
+ * args: string[] — extra CLI args passed to the server process
23
+ * installedAt: string — ISO timestamp
24
+ * enabled: boolean — whether the server should be started on bridge launch
25
+ * pid: number|null — PID of the running subprocess (runtime only)
26
+ * tools: object[] — cached tool manifest from last successful connection
27
+ * }
28
+ */
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const os = require('os');
33
+
34
+ // ── Registry file path ────────────────────────────────────────────────────────
35
+
36
+ function getRegistryDir() {
37
+ if (process.platform === 'win32') {
38
+ return path.join(process.env.APPDATA || os.homedir(), 'promethios');
39
+ }
40
+ return path.join(os.homedir(), '.config', 'promethios');
41
+ }
42
+
43
+ function getRegistryPath() {
44
+ return path.join(getRegistryDir(), 'mcp-registry.json');
45
+ }
46
+
47
+ // ── Read / write ──────────────────────────────────────────────────────────────
48
+
49
+ function readRegistry() {
50
+ const registryPath = getRegistryPath();
51
+ if (!fs.existsSync(registryPath)) return [];
52
+ try {
53
+ return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
54
+ } catch {
55
+ return [];
56
+ }
57
+ }
58
+
59
+ function writeRegistry(entries) {
60
+ const dir = getRegistryDir();
61
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
62
+ fs.writeFileSync(getRegistryPath(), JSON.stringify(entries, null, 2), 'utf8');
63
+ }
64
+
65
+ // ── CRUD ──────────────────────────────────────────────────────────────────────
66
+
67
+ function listInstalled() {
68
+ return readRegistry().map(e => ({ ...e, pid: null })); // strip runtime pid from persisted data
69
+ }
70
+
71
+ function getById(id) {
72
+ return readRegistry().find(e => e.id === id) || null;
73
+ }
74
+
75
+ function isInstalled(id) {
76
+ return readRegistry().some(e => e.id === id);
77
+ }
78
+
79
+ function addEntry(entry) {
80
+ const entries = readRegistry().filter(e => e.id !== entry.id); // replace if exists
81
+ entries.push({
82
+ ...entry,
83
+ installedAt: entry.installedAt || new Date().toISOString(),
84
+ enabled: entry.enabled !== false,
85
+ tools: entry.tools || [],
86
+ });
87
+ writeRegistry(entries);
88
+ }
89
+
90
+ function removeEntry(id) {
91
+ const entries = readRegistry().filter(e => e.id !== id);
92
+ writeRegistry(entries);
93
+ }
94
+
95
+ function updateEntry(id, patch) {
96
+ const entries = readRegistry().map(e => e.id === id ? { ...e, ...patch } : e);
97
+ writeRegistry(entries);
98
+ }
99
+
100
+ function setEnabled(id, enabled) {
101
+ updateEntry(id, { enabled });
102
+ }
103
+
104
+ function cacheTools(id, tools) {
105
+ updateEntry(id, { tools });
106
+ }
107
+
108
+ // ── Built-in servers (always present, not in registry) ────────────────────────
109
+ // These are the Promethios-native tools already running in the bridge.
110
+ // They are included in the combined tool list but cannot be uninstalled.
111
+
112
+ const BUILTIN_SERVERS = [
113
+ {
114
+ id: 'promethios-desktop',
115
+ name: 'Desktop Bridge',
116
+ description: 'Screen capture, active window, clipboard, file system, and app control on your computer.',
117
+ icon: '🖥️',
118
+ builtin: true,
119
+ enabled: true,
120
+ tools: [], // populated at runtime from FULL_MANIFEST in mcp-server.js
121
+ },
122
+ {
123
+ id: 'promethios-android',
124
+ name: 'Android Bridge',
125
+ description: 'Connect your Android phone — share screen, send messages, control apps remotely.',
126
+ icon: '📱',
127
+ builtin: true,
128
+ enabled: true,
129
+ tools: [],
130
+ },
131
+ ];
132
+
133
+ function listAll() {
134
+ const installed = readRegistry();
135
+ return [...BUILTIN_SERVERS, ...installed];
136
+ }
137
+
138
+ // ── npm install helper ────────────────────────────────────────────────────────
139
+
140
+ /**
141
+ * Install an npm package into the bridge's local node_modules.
142
+ * Returns { ok, version, error }.
143
+ */
144
+ async function npmInstall(packageName) {
145
+ const { execFile } = require('child_process');
146
+ const bridgeDir = path.join(__dirname, '..');
147
+
148
+ return new Promise((resolve) => {
149
+ execFile(
150
+ process.platform === 'win32' ? 'npm.cmd' : 'npm',
151
+ ['install', '--save', packageName],
152
+ { cwd: bridgeDir, timeout: 120_000 },
153
+ (err, stdout, stderr) => {
154
+ if (err) {
155
+ resolve({ ok: false, error: err.message + '\n' + stderr });
156
+ return;
157
+ }
158
+ // Extract installed version from npm output
159
+ const match = (stdout + stderr).match(new RegExp(packageName.replace(/[@/]/g, '.') + '@([\\d.]+)'));
160
+ resolve({ ok: true, version: match ? match[1] : 'unknown' });
161
+ }
162
+ );
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Uninstall an npm package from the bridge's local node_modules.
168
+ */
169
+ async function npmUninstall(packageName) {
170
+ const { execFile } = require('child_process');
171
+ const bridgeDir = path.join(__dirname, '..');
172
+
173
+ return new Promise((resolve) => {
174
+ execFile(
175
+ process.platform === 'win32' ? 'npm.cmd' : 'npm',
176
+ ['uninstall', packageName],
177
+ { cwd: bridgeDir, timeout: 60_000 },
178
+ (err, stdout, stderr) => {
179
+ if (err) resolve({ ok: false, error: err.message });
180
+ else resolve({ ok: true });
181
+ }
182
+ );
183
+ });
184
+ }
185
+
186
+ // ── Subprocess manager ────────────────────────────────────────────────────────
187
+
188
+ // Map of id → { process, tools }
189
+ const _running = new Map();
190
+
191
+ /**
192
+ * Start an installed MCP server as a stdio subprocess.
193
+ * Connects via MCP stdio transport and fetches the tool list.
194
+ * Returns the tool list on success.
195
+ */
196
+ async function startServer(entry, log = () => {}) {
197
+ if (_running.has(entry.id)) {
198
+ log(`[mcp-registry] ${entry.id} already running`);
199
+ return _running.get(entry.id).tools;
200
+ }
201
+
202
+ const { spawn } = require('child_process');
203
+ const env = { ...process.env, ...(entry.env || {}) };
204
+
205
+ // Resolve the binary: try npx first, fall back to node + main file
206
+ const args = ['--yes', entry.package, ...(entry.args || [])];
207
+ const child = spawn(
208
+ process.platform === 'win32' ? 'npx.cmd' : 'npx',
209
+ args,
210
+ { env, stdio: ['pipe', 'pipe', 'pipe'] }
211
+ );
212
+
213
+ child.on('error', (err) => {
214
+ log(`[mcp-registry] ${entry.id} process error: ${err.message}`);
215
+ _running.delete(entry.id);
216
+ });
217
+
218
+ child.on('exit', (code) => {
219
+ log(`[mcp-registry] ${entry.id} exited with code ${code}`);
220
+ _running.delete(entry.id);
221
+ });
222
+
223
+ // Give the process a moment to start, then probe for tools via MCP stdio
224
+ await new Promise(r => setTimeout(r, 1500));
225
+
226
+ let tools = [];
227
+ try {
228
+ tools = await fetchToolsViaStdio(child, entry.id, log);
229
+ cacheTools(entry.id, tools);
230
+ log(`[mcp-registry] ${entry.id} started — ${tools.length} tools available`);
231
+ } catch (err) {
232
+ log(`[mcp-registry] ${entry.id} tool fetch failed: ${err.message}`);
233
+ }
234
+
235
+ _running.set(entry.id, { process: child, tools });
236
+ return tools;
237
+ }
238
+
239
+ /**
240
+ * Stop a running MCP server subprocess.
241
+ */
242
+ function stopServer(id) {
243
+ const running = _running.get(id);
244
+ if (running) {
245
+ try { running.process.kill(); } catch {}
246
+ _running.delete(id);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Send a tools/list request over stdio to an MCP subprocess and return the tools.
252
+ */
253
+ async function fetchToolsViaStdio(child, id, log) {
254
+ return new Promise((resolve, reject) => {
255
+ const timeout = setTimeout(() => {
256
+ reject(new Error(`tools/list timeout for ${id}`));
257
+ }, 8000);
258
+
259
+ let buffer = '';
260
+ const onData = (chunk) => {
261
+ buffer += chunk.toString();
262
+ const lines = buffer.split('\n');
263
+ buffer = lines.pop(); // keep incomplete line
264
+ for (const line of lines) {
265
+ if (!line.trim()) continue;
266
+ try {
267
+ const msg = JSON.parse(line);
268
+ if (msg.id === 1 && msg.result?.tools) {
269
+ clearTimeout(timeout);
270
+ child.stdout.off('data', onData);
271
+ resolve(msg.result.tools);
272
+ }
273
+ } catch {}
274
+ }
275
+ };
276
+
277
+ child.stdout.on('data', onData);
278
+
279
+ // Send tools/list request
280
+ const request = JSON.stringify({
281
+ jsonrpc: '2.0',
282
+ id: 1,
283
+ method: 'tools/list',
284
+ params: {},
285
+ }) + '\n';
286
+
287
+ child.stdin.write(request, (err) => {
288
+ if (err) {
289
+ clearTimeout(timeout);
290
+ reject(err);
291
+ }
292
+ });
293
+ });
294
+ }
295
+
296
+ /**
297
+ * Call a tool on a running MCP server subprocess.
298
+ */
299
+ async function callTool(id, toolName, args) {
300
+ const running = _running.get(id);
301
+ if (!running) throw new Error(`MCP server ${id} is not running`);
302
+
303
+ return new Promise((resolve, reject) => {
304
+ const callId = Date.now();
305
+ const timeout = setTimeout(() => {
306
+ reject(new Error(`Tool call timeout: ${toolName} on ${id}`));
307
+ }, 30_000);
308
+
309
+ let buffer = '';
310
+ const onData = (chunk) => {
311
+ buffer += chunk.toString();
312
+ const lines = buffer.split('\n');
313
+ buffer = lines.pop();
314
+ for (const line of lines) {
315
+ if (!line.trim()) continue;
316
+ try {
317
+ const msg = JSON.parse(line);
318
+ if (msg.id === callId) {
319
+ clearTimeout(timeout);
320
+ running.process.stdout.off('data', onData);
321
+ if (msg.error) reject(new Error(msg.error.message));
322
+ else resolve(msg.result);
323
+ }
324
+ } catch {}
325
+ }
326
+ };
327
+
328
+ running.process.stdout.on('data', onData);
329
+
330
+ const request = JSON.stringify({
331
+ jsonrpc: '2.0',
332
+ id: callId,
333
+ method: 'tools/call',
334
+ params: { name: toolName, arguments: args || {} },
335
+ }) + '\n';
336
+
337
+ running.process.stdin.write(request, (err) => {
338
+ if (err) {
339
+ clearTimeout(timeout);
340
+ reject(err);
341
+ }
342
+ });
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Get the combined tool list from all running MCP servers.
348
+ * Each tool is tagged with its source server id.
349
+ */
350
+ function getAggregatedTools() {
351
+ const tools = [];
352
+ for (const [id, { tools: serverTools }] of _running.entries()) {
353
+ for (const tool of serverTools) {
354
+ tools.push({ ...tool, _mcpServerId: id });
355
+ }
356
+ }
357
+ return tools;
358
+ }
359
+
360
+ /**
361
+ * Start all enabled installed servers on bridge launch.
362
+ */
363
+ async function startAllEnabled(log = () => {}) {
364
+ const entries = readRegistry().filter(e => e.enabled);
365
+ for (const entry of entries) {
366
+ try {
367
+ await startServer(entry, log);
368
+ } catch (err) {
369
+ log(`[mcp-registry] Failed to start ${entry.id}: ${err.message}`);
370
+ }
371
+ }
372
+ }
373
+
374
+ module.exports = {
375
+ // Registry CRUD
376
+ listInstalled,
377
+ listAll,
378
+ getById,
379
+ isInstalled,
380
+ addEntry,
381
+ removeEntry,
382
+ updateEntry,
383
+ setEnabled,
384
+ cacheTools,
385
+ BUILTIN_SERVERS,
386
+ // npm helpers
387
+ npmInstall,
388
+ npmUninstall,
389
+ // Subprocess manager
390
+ startServer,
391
+ stopServer,
392
+ callTool,
393
+ getAggregatedTools,
394
+ startAllEnabled,
395
+ // Paths
396
+ getRegistryPath,
397
+ };