promethios-bridge 2.1.4 → 2.1.6

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.
@@ -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
+ };