bare-agent 0.4.3 → 0.5.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/README.md +1 -0
- package/package.json +3 -2
- package/src/mcp-bridge.js +450 -0
- package/src/mcp.js +5 -0
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ Every piece works alone — take what you need, ignore the rest.
|
|
|
74
74
|
| **Errors** | Typed hierarchy — `ProviderError`, `ToolError`, `TimeoutError`, `MaxRoundsError`, `CircuitOpenError` |
|
|
75
75
|
| **Browsing** | Web navigation, clicking, typing, reading via `barebrowse` (17 tools). Two modes: library tools (inline snapshots, pass to Loop) or CLI session (disk-based snapshots, token-efficient for multi-step flows). Optional `assess` tool (privacy scan) when `wearehere` is installed |
|
|
76
76
|
| **Mobile** | Android + iOS device control via `baremobile`. Same two modes: library tools (`createMobileTools` — action tools auto-return snapshots) or CLI session (`baremobile` CLI — disk-based snapshots) |
|
|
77
|
+
| **MCP Bridge** | Auto-discover MCP servers from IDE configs (Claude Code, Cursor, etc.), expose as bareagent tools. Allow/deny filtering, policy functions, `systemContext` for LLM awareness. Zero deps |
|
|
77
78
|
|
|
78
79
|
**Providers:** OpenAI-compatible (OpenAI, OpenRouter, Groq, vLLM, LM Studio), Anthropic, Ollama, CLIPipe (any CLI tool via stdin/stdout with real-time streaming), Fallback, or bring your own (one method: `generate`). All return the same shape — swap freely.
|
|
79
80
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bare-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"index.js",
|
|
6
6
|
"src/",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"./providers": "./src/providers.js",
|
|
24
24
|
"./stores": "./src/stores.js",
|
|
25
25
|
"./transports": "./src/transports.js",
|
|
26
|
-
"./tools": "./src/tools.js"
|
|
26
|
+
"./tools": "./src/tools.js",
|
|
27
|
+
"./mcp": "./src/mcp.js"
|
|
27
28
|
},
|
|
28
29
|
"engines": {
|
|
29
30
|
"node": ">=18"
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('node:child_process');
|
|
4
|
+
const { readFileSync, writeFileSync, existsSync } = require('node:fs');
|
|
5
|
+
const { join } = require('node:path');
|
|
6
|
+
const { homedir } = require('node:os');
|
|
7
|
+
const { ToolError } = require('./errors');
|
|
8
|
+
|
|
9
|
+
// --- Config discovery (from IDE configs) ---
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CONFIG_PATHS = [
|
|
12
|
+
() => join(process.cwd(), '.mcp.json'), // project
|
|
13
|
+
() => join(homedir(), '.mcp.json'), // home
|
|
14
|
+
() => join(homedir(), '.claude', 'mcp_servers.json'), // Claude Code
|
|
15
|
+
() => join(homedir(), '.config', 'Claude', 'claude_desktop_config.json'), // Claude Desktop
|
|
16
|
+
() => join(homedir(), '.cursor', 'mcp.json'), // Cursor
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
function discoverServers(configPaths) {
|
|
20
|
+
const paths = configPaths || DEFAULT_CONFIG_PATHS.map(fn => fn());
|
|
21
|
+
const servers = new Map();
|
|
22
|
+
|
|
23
|
+
for (const p of paths) {
|
|
24
|
+
let raw;
|
|
25
|
+
try { raw = readFileSync(p, 'utf8'); } catch { continue; }
|
|
26
|
+
let parsed;
|
|
27
|
+
try { parsed = JSON.parse(raw); } catch { continue; }
|
|
28
|
+
|
|
29
|
+
const entries = parsed.mcpServers || {};
|
|
30
|
+
for (const [name, def] of Object.entries(entries)) {
|
|
31
|
+
if (!servers.has(name)) servers.set(name, def);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return servers;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Bridge config file (.mcp-bridge.json) ---
|
|
39
|
+
|
|
40
|
+
const DEFAULT_BRIDGE_PATH = () => join(process.cwd(), '.mcp-bridge.json');
|
|
41
|
+
const DEFAULT_TTL = '24h';
|
|
42
|
+
|
|
43
|
+
function parseTTL(ttl) {
|
|
44
|
+
const match = (ttl || DEFAULT_TTL).match(/^(\d+)(s|m|h|d)$/);
|
|
45
|
+
if (!match) return 24 * 60 * 60 * 1000;
|
|
46
|
+
const n = parseInt(match[1]);
|
|
47
|
+
const unit = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[match[2]];
|
|
48
|
+
return n * unit;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readBridgeConfig(filePath) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeBridgeConfig(filePath, config) {
|
|
60
|
+
writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isExpired(config) {
|
|
64
|
+
if (!config || !config.discovered) return true;
|
|
65
|
+
const ttlMs = parseTTL(config.ttl);
|
|
66
|
+
return Date.now() - new Date(config.discovered).getTime() > ttlMs;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Merge fresh discovery into existing config.
|
|
71
|
+
* - New servers: added with all tools "allow"
|
|
72
|
+
* - Removed servers: removed from config
|
|
73
|
+
* - New tools on existing server: added as "allow"
|
|
74
|
+
* - Removed tools on existing server: removed from config
|
|
75
|
+
* - Existing tools: user's allow/deny preserved
|
|
76
|
+
*/
|
|
77
|
+
function mergeBridgeConfig(existing, discovered, freshTools) {
|
|
78
|
+
const merged = {
|
|
79
|
+
discovered: new Date().toISOString(),
|
|
80
|
+
ttl: existing?.ttl || DEFAULT_TTL,
|
|
81
|
+
servers: {},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const [name, def] of discovered) {
|
|
85
|
+
const serverTools = freshTools.get(name) || [];
|
|
86
|
+
const existingServer = existing?.servers?.[name];
|
|
87
|
+
const existingTools = existingServer?.tools || {};
|
|
88
|
+
|
|
89
|
+
const tools = {};
|
|
90
|
+
for (const t of serverTools) {
|
|
91
|
+
tools[t.name] = existingTools[t.name] || 'allow';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
merged.servers[name] = {
|
|
95
|
+
command: def.command,
|
|
96
|
+
args: def.args || [],
|
|
97
|
+
...(def.env && { env: def.env }),
|
|
98
|
+
...(def.cwd && { cwd: def.cwd }),
|
|
99
|
+
tools,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return merged;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Env resolution ---
|
|
107
|
+
|
|
108
|
+
function resolveEnv(env) {
|
|
109
|
+
if (!env) return {};
|
|
110
|
+
const resolved = {};
|
|
111
|
+
for (const [k, v] of Object.entries(env)) {
|
|
112
|
+
resolved[k] = typeof v === 'string'
|
|
113
|
+
? v.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] || '')
|
|
114
|
+
: v;
|
|
115
|
+
}
|
|
116
|
+
return resolved;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- JSON-RPC stdio client ---
|
|
120
|
+
|
|
121
|
+
function createRpcClient(name, def) {
|
|
122
|
+
const { command, args = [], env, cwd } = def;
|
|
123
|
+
const mergedEnv = { ...process.env, ...resolveEnv(env) };
|
|
124
|
+
|
|
125
|
+
const child = spawn(command, args, {
|
|
126
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
127
|
+
env: mergedEnv,
|
|
128
|
+
...(cwd && { cwd }),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const pending = new Map();
|
|
132
|
+
let nextId = 1;
|
|
133
|
+
let buffer = '';
|
|
134
|
+
|
|
135
|
+
child.stdout.setEncoding('utf8');
|
|
136
|
+
child.stdout.on('data', (chunk) => {
|
|
137
|
+
buffer += chunk;
|
|
138
|
+
let idx;
|
|
139
|
+
while ((idx = buffer.indexOf('\n')) !== -1) {
|
|
140
|
+
const line = buffer.slice(0, idx).trim();
|
|
141
|
+
buffer = buffer.slice(idx + 1);
|
|
142
|
+
if (!line) continue;
|
|
143
|
+
let msg;
|
|
144
|
+
try { msg = JSON.parse(line); } catch { continue; }
|
|
145
|
+
if (!msg.id) continue;
|
|
146
|
+
const p = pending.get(msg.id);
|
|
147
|
+
if (!p) continue;
|
|
148
|
+
pending.delete(msg.id);
|
|
149
|
+
if (msg.error) {
|
|
150
|
+
p.reject(new ToolError(`MCP server "${name}": ${msg.error.message}`, {
|
|
151
|
+
context: { code: msg.error.code },
|
|
152
|
+
}));
|
|
153
|
+
} else {
|
|
154
|
+
p.resolve(msg.result);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
let stderrBuf = '';
|
|
160
|
+
child.stderr?.setEncoding('utf8');
|
|
161
|
+
child.stderr?.on('data', (chunk) => { stderrBuf += chunk; });
|
|
162
|
+
|
|
163
|
+
child.on('close', (code) => {
|
|
164
|
+
for (const [id, { reject }] of pending) {
|
|
165
|
+
reject(new ToolError(`MCP server "${name}" exited (code ${code}). stderr: ${stderrBuf.slice(-500)}`));
|
|
166
|
+
}
|
|
167
|
+
pending.clear();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
function rpc(method, params = {}) {
|
|
171
|
+
const id = nextId++;
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
pending.set(id, { resolve, reject });
|
|
174
|
+
const msg = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
|
|
175
|
+
child.stdin.write(msg);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function notify(method, params = {}) {
|
|
180
|
+
const msg = JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n';
|
|
181
|
+
child.stdin.write(msg);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { rpc, notify, child, get stderr() { return stderrBuf; } };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --- Content unwrapping ---
|
|
188
|
+
|
|
189
|
+
function unwrapContent(content) {
|
|
190
|
+
if (!Array.isArray(content) || content.length === 0) return '';
|
|
191
|
+
if (content.length === 1 && content[0].type === 'text') return content[0].text;
|
|
192
|
+
return content;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- Tool wrapping ---
|
|
196
|
+
|
|
197
|
+
function wrapTools(serverName, mcpTools, rpc, policy) {
|
|
198
|
+
return mcpTools.map(t => ({
|
|
199
|
+
name: `${serverName}_${t.name}`,
|
|
200
|
+
description: t.description || '',
|
|
201
|
+
parameters: t.inputSchema || { type: 'object', properties: {} },
|
|
202
|
+
execute: async (args) => {
|
|
203
|
+
if (policy) {
|
|
204
|
+
const verdict = await policy(serverName, t.name, args);
|
|
205
|
+
if (verdict === false || typeof verdict === 'string') {
|
|
206
|
+
const reason = typeof verdict === 'string'
|
|
207
|
+
? verdict
|
|
208
|
+
: `[GOVERNANCE] Tool "${serverName}_${t.name}" is not permitted by policy. Do not retry this tool.`;
|
|
209
|
+
throw new ToolError(reason, { context: { server: serverName, tool: t.name } });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const result = await rpc('tools/call', { name: t.name, arguments: args });
|
|
213
|
+
if (result.isError) {
|
|
214
|
+
throw new ToolError(unwrapContent(result.content) || 'MCP tool error', {
|
|
215
|
+
context: { server: serverName, tool: t.name },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return unwrapContent(result.content);
|
|
219
|
+
},
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- Server lifecycle ---
|
|
224
|
+
|
|
225
|
+
async function killServer(child) {
|
|
226
|
+
if (child.exitCode !== null) return;
|
|
227
|
+
|
|
228
|
+
child.stdin?.destroy();
|
|
229
|
+
child.stdout?.destroy();
|
|
230
|
+
child.stderr?.destroy();
|
|
231
|
+
|
|
232
|
+
await new Promise(resolve => {
|
|
233
|
+
const onClose = () => resolve();
|
|
234
|
+
child.once('close', onClose);
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
child.removeListener('close', onClose);
|
|
237
|
+
resolve();
|
|
238
|
+
}, 700);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (child.exitCode === null) {
|
|
242
|
+
child.kill('SIGTERM');
|
|
243
|
+
await new Promise(resolve => {
|
|
244
|
+
const onClose = () => resolve();
|
|
245
|
+
child.once('close', onClose);
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
child.removeListener('close', onClose);
|
|
248
|
+
resolve();
|
|
249
|
+
}, 700);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (child.exitCode === null) {
|
|
254
|
+
child.kill('SIGKILL');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
child.unref();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// --- Connect + list tools from a server ---
|
|
261
|
+
|
|
262
|
+
async function connectAndListTools(name, def, timeout = 15000) {
|
|
263
|
+
const client = createRpcClient(name, def);
|
|
264
|
+
|
|
265
|
+
const init = client.rpc('initialize', {
|
|
266
|
+
protocolVersion: '2024-11-05',
|
|
267
|
+
capabilities: {},
|
|
268
|
+
clientInfo: { name: 'bare-agent', version: '0.5.0' },
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const timer = new Promise((_, reject) =>
|
|
272
|
+
setTimeout(() => reject(new ToolError(`MCP server "${name}" init timed out after ${timeout}ms`)), timeout)
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
await Promise.race([init, timer]);
|
|
276
|
+
client.notify('notifications/initialized');
|
|
277
|
+
|
|
278
|
+
const { tools: mcpTools } = await client.rpc('tools/list');
|
|
279
|
+
|
|
280
|
+
return { mcpTools, client };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --- System context for LLM ---
|
|
284
|
+
|
|
285
|
+
function buildSystemContext(servers, tools, denied) {
|
|
286
|
+
const lines = [];
|
|
287
|
+
lines.push(`MCP Bridge: ${tools.length} tools available from ${servers.length} server(s): ${servers.join(', ')}.`);
|
|
288
|
+
|
|
289
|
+
const byServer = {};
|
|
290
|
+
for (const t of tools) {
|
|
291
|
+
const parts = t.name.split('_');
|
|
292
|
+
const server = parts[0];
|
|
293
|
+
(byServer[server] = byServer[server] || []).push(t.name.replace(`${server}_`, ''));
|
|
294
|
+
}
|
|
295
|
+
for (const [server, toolNames] of Object.entries(byServer)) {
|
|
296
|
+
lines.push(` ${server}: ${toolNames.join(', ')}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (denied.length > 0) {
|
|
300
|
+
lines.push(`Restricted tools (${denied.length}, not available to you):`);
|
|
301
|
+
for (const d of denied) {
|
|
302
|
+
lines.push(` - ${d.server}_${d.tool}: ${d.description.slice(0, 80)} [denied]`);
|
|
303
|
+
}
|
|
304
|
+
lines.push('If you need a restricted tool, explain what you need and why to the user.');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const governance = denied.length > 0 ? 'filtered' : 'open (all tools exposed)';
|
|
308
|
+
lines.push(`Governance: ${governance}.`);
|
|
309
|
+
|
|
310
|
+
if (denied.length === 0) {
|
|
311
|
+
lines.push('To restrict tools, edit .mcp-bridge.json and set tools to "deny".');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return lines.join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- Main entry point ---
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Create an MCP bridge. On first run, discovers MCP servers from IDE configs,
|
|
321
|
+
* connects, lists tools, and writes .mcp-bridge.json with all tools set to "allow".
|
|
322
|
+
* On subsequent runs, reads .mcp-bridge.json and respects allow/deny per tool.
|
|
323
|
+
* Re-discovers when TTL expires (default: 24h).
|
|
324
|
+
*
|
|
325
|
+
* @param {object} [opts]
|
|
326
|
+
* @param {string} [opts.bridgePath] - Path to .mcp-bridge.json. Default: .mcp-bridge.json in cwd.
|
|
327
|
+
* @param {string[]} [opts.configPaths] - IDE config paths for discovery.
|
|
328
|
+
* @param {string[]} [opts.servers] - Limit to these server names.
|
|
329
|
+
* @param {number} [opts.timeout=15000] - Per-server init timeout in ms.
|
|
330
|
+
* @param {Function} [opts.policy] - Async function(serverName, toolName, args) for runtime arg-dependent checks.
|
|
331
|
+
* @param {boolean} [opts.refresh=false] - Force re-discovery regardless of TTL.
|
|
332
|
+
* @returns {Promise<{tools: Array, servers: string[], systemContext: string, denied: Array, close: Function}>}
|
|
333
|
+
*/
|
|
334
|
+
async function createMCPBridge(opts = {}) {
|
|
335
|
+
const bridgePath = opts.bridgePath || DEFAULT_BRIDGE_PATH();
|
|
336
|
+
const timeout = opts.timeout || 15000;
|
|
337
|
+
|
|
338
|
+
let config = readBridgeConfig(bridgePath);
|
|
339
|
+
const needsRefresh = opts.refresh || !config || isExpired(config);
|
|
340
|
+
|
|
341
|
+
if (needsRefresh) {
|
|
342
|
+
// Discover from IDE configs
|
|
343
|
+
const discovered = discoverServers(opts.configPaths);
|
|
344
|
+
if (discovered.size === 0 && !config) {
|
|
345
|
+
return { tools: [], servers: [], systemContext: '', denied: [], close: async () => {} };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Connect to all discovered servers and list their tools
|
|
349
|
+
const freshTools = new Map();
|
|
350
|
+
const connectResults = new Map();
|
|
351
|
+
const errors = [];
|
|
352
|
+
|
|
353
|
+
const toDiscover = opts.servers
|
|
354
|
+
? [...discovered.entries()].filter(([n]) => opts.servers.includes(n))
|
|
355
|
+
: [...discovered.entries()];
|
|
356
|
+
|
|
357
|
+
await Promise.all(toDiscover.map(async ([name, def]) => {
|
|
358
|
+
try {
|
|
359
|
+
const result = await connectAndListTools(name, def, timeout);
|
|
360
|
+
freshTools.set(name, result.mcpTools);
|
|
361
|
+
connectResults.set(name, result.client);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
errors.push({ server: name, error: err.message });
|
|
364
|
+
}
|
|
365
|
+
}));
|
|
366
|
+
|
|
367
|
+
if (errors.length > 0) {
|
|
368
|
+
console.warn('[MCP Bridge] Some servers failed to connect:', errors);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Merge with existing config (preserves user's allow/deny)
|
|
372
|
+
config = mergeBridgeConfig(config, new Map(toDiscover), freshTools);
|
|
373
|
+
|
|
374
|
+
// Write the config file
|
|
375
|
+
writeBridgeConfig(bridgePath, config);
|
|
376
|
+
console.log(`[MCP Bridge] Wrote ${bridgePath}`);
|
|
377
|
+
|
|
378
|
+
// Close the discovery connections — we'll reconnect below using the config
|
|
379
|
+
for (const client of connectResults.values()) {
|
|
380
|
+
await killServer(client.child);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Filter to requested servers
|
|
385
|
+
const serverNames = opts.servers
|
|
386
|
+
? opts.servers.filter(n => config.servers[n])
|
|
387
|
+
: Object.keys(config.servers);
|
|
388
|
+
|
|
389
|
+
if (serverNames.length === 0) {
|
|
390
|
+
return { tools: [], servers: [], systemContext: '', denied: [], close: async () => {} };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Connect to servers and wrap only allowed tools
|
|
394
|
+
const tools = [];
|
|
395
|
+
const children = [];
|
|
396
|
+
const connected = [];
|
|
397
|
+
const denied = [];
|
|
398
|
+
const errors = [];
|
|
399
|
+
|
|
400
|
+
await Promise.all(serverNames.map(async (name) => {
|
|
401
|
+
const serverConf = config.servers[name];
|
|
402
|
+
const allowedToolNames = Object.entries(serverConf.tools)
|
|
403
|
+
.filter(([, perm]) => perm === 'allow')
|
|
404
|
+
.map(([t]) => t);
|
|
405
|
+
const deniedToolNames = Object.entries(serverConf.tools)
|
|
406
|
+
.filter(([, perm]) => perm !== 'allow')
|
|
407
|
+
.map(([t]) => t);
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const { mcpTools, client } = await connectAndListTools(name, serverConf, timeout);
|
|
411
|
+
|
|
412
|
+
// Only wrap tools that are allowed in config
|
|
413
|
+
const allowed = mcpTools.filter(t => allowedToolNames.includes(t.name));
|
|
414
|
+
const wrapped = wrapTools(name, allowed, client.rpc, opts.policy);
|
|
415
|
+
|
|
416
|
+
tools.push(...wrapped);
|
|
417
|
+
children.push(client.child);
|
|
418
|
+
connected.push(name);
|
|
419
|
+
|
|
420
|
+
// Track denied tools with descriptions from the server
|
|
421
|
+
for (const t of mcpTools) {
|
|
422
|
+
if (deniedToolNames.includes(t.name)) {
|
|
423
|
+
denied.push({ server: name, tool: t.name, description: t.description || '' });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
errors.push({ server: name, error: err.message });
|
|
428
|
+
}
|
|
429
|
+
}));
|
|
430
|
+
|
|
431
|
+
if (errors.length > 0) {
|
|
432
|
+
console.warn('[MCP Bridge] Some servers failed to connect:', errors);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const systemContext = buildSystemContext(connected, tools, denied);
|
|
436
|
+
if (connected.length > 0) console.log(systemContext);
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
tools,
|
|
440
|
+
servers: connected,
|
|
441
|
+
denied,
|
|
442
|
+
systemContext,
|
|
443
|
+
errors,
|
|
444
|
+
close: async () => {
|
|
445
|
+
await Promise.all(children.map(killServer));
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
module.exports = { createMCPBridge, discoverServers };
|