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.
- package/package.json +1 -1
- package/src/bridge.js +87 -2
- package/src/mcp-catalog.json +896 -0
- package/src/mcp-registry.js +397 -0
|
@@ -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
|
+
};
|