bare-agent 0.11.0 → 0.12.1
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/bareagent.context.md +1149 -0
- package/bin/cli.d.ts +4 -0
- package/bin/cli.js +40 -10
- package/bin/test-provider.d.ts +2 -0
- package/bin/test-provider.js +5 -1
- package/index.d.ts +20 -0
- package/package.json +46 -10
- package/src/bareguard-adapter.d.ts +118 -0
- package/src/bareguard-adapter.js +75 -3
- package/src/checkpoint.d.ts +61 -0
- package/src/checkpoint.js +17 -8
- package/src/circuit-breaker.d.ts +70 -0
- package/src/circuit-breaker.js +20 -4
- package/src/errors.d.ts +106 -0
- package/src/errors.js +50 -1
- package/src/loop.d.ts +135 -0
- package/src/loop.js +73 -17
- package/src/mcp-bridge.d.ts +133 -0
- package/src/mcp-bridge.js +179 -27
- package/src/mcp.d.ts +4 -0
- package/src/memory.d.ts +50 -0
- package/src/memory.js +22 -2
- package/src/planner.d.ts +62 -0
- package/src/planner.js +26 -7
- package/src/provider-anthropic.d.ts +55 -0
- package/src/provider-anthropic.js +32 -11
- package/src/provider-clipipe.d.ts +86 -0
- package/src/provider-clipipe.js +28 -18
- package/src/provider-fallback.d.ts +44 -0
- package/src/provider-fallback.js +18 -8
- package/src/provider-ollama.d.ts +41 -0
- package/src/provider-ollama.js +27 -7
- package/src/provider-openai.d.ts +57 -0
- package/src/provider-openai.js +31 -16
- package/src/providers.d.ts +6 -0
- package/src/providers.js +8 -0
- package/src/retry.d.ts +44 -0
- package/src/retry.js +15 -1
- package/src/run-plan.d.ts +126 -0
- package/src/run-plan.js +46 -13
- package/src/scheduler.d.ts +102 -0
- package/src/scheduler.js +32 -4
- package/src/state.d.ts +45 -0
- package/src/state.js +18 -2
- package/src/store-jsonfile.d.ts +85 -0
- package/src/store-jsonfile.js +33 -8
- package/src/store-sqlite.d.ts +90 -0
- package/src/store-sqlite.js +31 -7
- package/src/stores.d.ts +3 -0
- package/src/stream.d.ts +79 -0
- package/src/stream.js +32 -0
- package/src/tools.d.ts +8 -0
- package/src/transport-jsonl.d.ts +30 -0
- package/src/transport-jsonl.js +13 -0
- package/src/transports.d.ts +2 -0
- package/tools/browse.d.ts +10 -0
- package/tools/browse.js +2 -0
- package/tools/defer.d.ts +33 -0
- package/tools/defer.js +12 -3
- package/tools/mobile.d.ts +34 -0
- package/tools/mobile.js +28 -15
- package/tools/shell.d.ts +31 -0
- package/tools/shell.js +55 -6
- package/tools/spawn.d.ts +107 -0
- package/tools/spawn.js +24 -5
- package/types/index.d.ts +66 -0
- package/types/shims.d.ts +16 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export type ToolDef = import("../types").ToolDef;
|
|
2
|
+
/**
|
|
3
|
+
* A server definition as found in an IDE/MCP config file.
|
|
4
|
+
*/
|
|
5
|
+
export type ServerDef = {
|
|
6
|
+
command: string;
|
|
7
|
+
args?: string[] | undefined;
|
|
8
|
+
env?: Record<string, string> | undefined;
|
|
9
|
+
cwd?: string | undefined;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Raw tool descriptor as returned by an MCP server's tools/list.
|
|
13
|
+
*/
|
|
14
|
+
export type McpTool = {
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string | undefined;
|
|
17
|
+
inputSchema?: Record<string, any> | undefined;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Per-server entry persisted in .mcp-bridge.json.
|
|
21
|
+
*/
|
|
22
|
+
export type BridgeServerEntry = {
|
|
23
|
+
command: string;
|
|
24
|
+
args: string[];
|
|
25
|
+
env?: Record<string, string> | undefined;
|
|
26
|
+
cwd?: string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* - tool name -> "allow" | "deny"
|
|
29
|
+
*/
|
|
30
|
+
tools: Record<string, string>;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Persisted bridge config (.mcp-bridge.json).
|
|
34
|
+
*/
|
|
35
|
+
export type BridgeConfig = {
|
|
36
|
+
/**
|
|
37
|
+
* - ISO timestamp
|
|
38
|
+
*/
|
|
39
|
+
discovered: string;
|
|
40
|
+
ttl: string;
|
|
41
|
+
servers: Record<string, BridgeServerEntry>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* A denied-tool descriptor surfaced to the LLM.
|
|
45
|
+
*/
|
|
46
|
+
export type DeniedTool = {
|
|
47
|
+
server: string;
|
|
48
|
+
tool: string;
|
|
49
|
+
description: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* JSON-RPC stdio client over a spawned MCP server.
|
|
53
|
+
*/
|
|
54
|
+
export type RpcClient = {
|
|
55
|
+
rpc: (method: string, params?: object) => Promise<any>;
|
|
56
|
+
notify: (method: string, params?: object) => void;
|
|
57
|
+
child: import("node:child_process").ChildProcessWithoutNullStreams;
|
|
58
|
+
stderr: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Create an MCP bridge. On first run, discovers MCP servers from IDE configs,
|
|
62
|
+
* connects, lists tools, and writes .mcp-bridge.json with all tools set to "allow".
|
|
63
|
+
* On subsequent runs, reads .mcp-bridge.json and respects allow/deny per tool.
|
|
64
|
+
* Re-discovers when TTL expires (default: 24h).
|
|
65
|
+
*
|
|
66
|
+
* Returns BOTH surfaces (v0.9+):
|
|
67
|
+
* - `tools` — bulk-loaded array of name-prefixed tools (small catalogs;
|
|
68
|
+
* LLM sees them upfront).
|
|
69
|
+
* - `metaTools` — [mcp_discover, mcp_invoke] LLM-callable pair (large catalogs;
|
|
70
|
+
* LLM picks tools dynamically). Shares the same RPC connections.
|
|
71
|
+
*
|
|
72
|
+
* Wire one or the other into Loop's tool array; never both (the LLM would see
|
|
73
|
+
* the same MCP tool twice). Pick by catalog size and token budget.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} [opts]
|
|
76
|
+
* @param {string} [opts.bridgePath] - Path to .mcp-bridge.json. Default: .mcp-bridge.json in cwd.
|
|
77
|
+
* @param {string[]} [opts.configPaths] - IDE config paths for discovery.
|
|
78
|
+
* @param {string[]} [opts.servers] - Limit to these server names.
|
|
79
|
+
* @param {number} [opts.timeout=15000] - Per-server init timeout in ms.
|
|
80
|
+
* @param {boolean} [opts.refresh=false] - Force re-discovery regardless of TTL.
|
|
81
|
+
* @param {(name: string, def: ServerDef) => boolean | Promise<boolean>} [opts.confirmServer]
|
|
82
|
+
* Vet each discovered server BEFORE its `command` is spawned. Connecting to an
|
|
83
|
+
* MCP server runs its command, and discovery reads configs from the cwd (a
|
|
84
|
+
* `.mcp.json` in an untrusted repo) as well as the user's home/IDE configs.
|
|
85
|
+
* Return false to skip a server (its command is never executed). A throw is
|
|
86
|
+
* treated as a deny (fail-closed). Default: every discovered server is trusted
|
|
87
|
+
* (unchanged behavior) — pass this to gate command execution.
|
|
88
|
+
* @returns {Promise<{tools: ToolDef[], metaTools?: ToolDef[], servers: string[], systemContext: string, denied: DeniedTool[], errors?: Array<{server: string, error: string}>, close: Function}>}
|
|
89
|
+
*/
|
|
90
|
+
export function createMCPBridge(opts?: {
|
|
91
|
+
bridgePath?: string | undefined;
|
|
92
|
+
configPaths?: string[] | undefined;
|
|
93
|
+
servers?: string[] | undefined;
|
|
94
|
+
timeout?: number | undefined;
|
|
95
|
+
refresh?: boolean | undefined;
|
|
96
|
+
confirmServer?: ((name: string, def: ServerDef) => boolean | Promise<boolean>) | undefined;
|
|
97
|
+
}): Promise<{
|
|
98
|
+
tools: ToolDef[];
|
|
99
|
+
metaTools?: ToolDef[];
|
|
100
|
+
servers: string[];
|
|
101
|
+
systemContext: string;
|
|
102
|
+
denied: DeniedTool[];
|
|
103
|
+
errors?: Array<{
|
|
104
|
+
server: string;
|
|
105
|
+
error: string;
|
|
106
|
+
}>;
|
|
107
|
+
close: Function;
|
|
108
|
+
}>;
|
|
109
|
+
/**
|
|
110
|
+
* @param {string[]} [configPaths]
|
|
111
|
+
* @returns {Map<string, ServerDef>}
|
|
112
|
+
*/
|
|
113
|
+
export function discoverServers(configPaths?: string[]): Map<string, ServerDef>;
|
|
114
|
+
/**
|
|
115
|
+
* Build the LLM-callable meta-tool surface from a fully-connected bridge.
|
|
116
|
+
* Shares the underlying tool array and RPC clients with the bulk surface —
|
|
117
|
+
* one set of connections, one factory, two output forms. The user picks
|
|
118
|
+
* `bridge.tools` (bulk) for small catalogs the LLM should see upfront, or
|
|
119
|
+
* `bridge.metaTools` for large catalogs the LLM should discover on demand.
|
|
120
|
+
*
|
|
121
|
+
* Gov shape: when the LLM calls mcp_invoke, the action sent to gate.check
|
|
122
|
+
* is `{ type: 'mcp_invoke', args: { name, args }, _ctx }` — bareguard sees
|
|
123
|
+
* `mcp_invoke` as the type. To deny specific MCP tools, use bareguard's
|
|
124
|
+
* `tools.denyArgPatterns: { mcp_invoke: [/"name":"linear_admin_.*"/] }`
|
|
125
|
+
* or `content.denyPatterns` over the JSON-serialized form. The inner MCP
|
|
126
|
+
* tool name doesn't travel as `action.type` — that's a deliberate v0.9
|
|
127
|
+
* trade for one consistent gate-check call per LLM tool invocation.
|
|
128
|
+
*
|
|
129
|
+
* @param {ToolDef[]} tools - The bulk-loaded, name-prefixed tools array.
|
|
130
|
+
* @param {string} [discoveredAt] - ISO timestamp from .mcp-bridge.json.
|
|
131
|
+
* @returns {ToolDef[]} [mcp_discover, mcp_invoke]
|
|
132
|
+
*/
|
|
133
|
+
export function buildMetaTools(tools: ToolDef[], discoveredAt?: string): ToolDef[];
|
package/src/mcp-bridge.js
CHANGED
|
@@ -6,6 +6,60 @@ const { join } = require('node:path');
|
|
|
6
6
|
const { homedir } = require('node:os');
|
|
7
7
|
const { ToolError } = require('./errors');
|
|
8
8
|
|
|
9
|
+
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A server definition as found in an IDE/MCP config file.
|
|
13
|
+
* @typedef {object} ServerDef
|
|
14
|
+
* @property {string} command
|
|
15
|
+
* @property {string[]} [args]
|
|
16
|
+
* @property {Record<string, string>} [env]
|
|
17
|
+
* @property {string} [cwd]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Raw tool descriptor as returned by an MCP server's tools/list.
|
|
22
|
+
* @typedef {object} McpTool
|
|
23
|
+
* @property {string} name
|
|
24
|
+
* @property {string} [description]
|
|
25
|
+
* @property {Record<string, any>} [inputSchema]
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Per-server entry persisted in .mcp-bridge.json.
|
|
30
|
+
* @typedef {object} BridgeServerEntry
|
|
31
|
+
* @property {string} command
|
|
32
|
+
* @property {string[]} args
|
|
33
|
+
* @property {Record<string, string>} [env]
|
|
34
|
+
* @property {string} [cwd]
|
|
35
|
+
* @property {Record<string, string>} tools - tool name -> "allow" | "deny"
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Persisted bridge config (.mcp-bridge.json).
|
|
40
|
+
* @typedef {object} BridgeConfig
|
|
41
|
+
* @property {string} discovered - ISO timestamp
|
|
42
|
+
* @property {string} ttl
|
|
43
|
+
* @property {Record<string, BridgeServerEntry>} servers
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A denied-tool descriptor surfaced to the LLM.
|
|
48
|
+
* @typedef {object} DeniedTool
|
|
49
|
+
* @property {string} server
|
|
50
|
+
* @property {string} tool
|
|
51
|
+
* @property {string} description
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* JSON-RPC stdio client over a spawned MCP server.
|
|
56
|
+
* @typedef {object} RpcClient
|
|
57
|
+
* @property {(method: string, params?: object) => Promise<any>} rpc
|
|
58
|
+
* @property {(method: string, params?: object) => void} notify
|
|
59
|
+
* @property {import('node:child_process').ChildProcessWithoutNullStreams} child
|
|
60
|
+
* @property {string} stderr
|
|
61
|
+
*/
|
|
62
|
+
|
|
9
63
|
// --- Config discovery (from IDE configs) ---
|
|
10
64
|
|
|
11
65
|
const DEFAULT_CONFIG_PATHS = [
|
|
@@ -16,8 +70,13 @@ const DEFAULT_CONFIG_PATHS = [
|
|
|
16
70
|
() => join(homedir(), '.cursor', 'mcp.json'), // Cursor
|
|
17
71
|
];
|
|
18
72
|
|
|
73
|
+
/**
|
|
74
|
+
* @param {string[]} [configPaths]
|
|
75
|
+
* @returns {Map<string, ServerDef>}
|
|
76
|
+
*/
|
|
19
77
|
function discoverServers(configPaths) {
|
|
20
78
|
const paths = configPaths || DEFAULT_CONFIG_PATHS.map(fn => fn());
|
|
79
|
+
/** @type {Map<string, ServerDef>} */
|
|
21
80
|
const servers = new Map();
|
|
22
81
|
|
|
23
82
|
for (const p of paths) {
|
|
@@ -40,14 +99,21 @@ function discoverServers(configPaths) {
|
|
|
40
99
|
const DEFAULT_BRIDGE_PATH = () => join(process.cwd(), '.mcp-bridge.json');
|
|
41
100
|
const DEFAULT_TTL = '24h';
|
|
42
101
|
|
|
102
|
+
/** @param {string} [ttl] */
|
|
43
103
|
function parseTTL(ttl) {
|
|
44
104
|
const match = (ttl || DEFAULT_TTL).match(/^(\d+)(s|m|h|d)$/);
|
|
45
105
|
if (!match) return 24 * 60 * 60 * 1000;
|
|
46
106
|
const n = parseInt(match[1]);
|
|
47
|
-
|
|
107
|
+
/** @type {Record<string, number>} */
|
|
108
|
+
const units = { s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
109
|
+
const unit = units[match[2]];
|
|
48
110
|
return n * unit;
|
|
49
111
|
}
|
|
50
112
|
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} filePath
|
|
115
|
+
* @returns {BridgeConfig|null}
|
|
116
|
+
*/
|
|
51
117
|
function readBridgeConfig(filePath) {
|
|
52
118
|
try {
|
|
53
119
|
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
@@ -56,10 +122,15 @@ function readBridgeConfig(filePath) {
|
|
|
56
122
|
}
|
|
57
123
|
}
|
|
58
124
|
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} filePath
|
|
127
|
+
* @param {BridgeConfig} config
|
|
128
|
+
*/
|
|
59
129
|
function writeBridgeConfig(filePath, config) {
|
|
60
130
|
writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
|
|
61
131
|
}
|
|
62
132
|
|
|
133
|
+
/** @param {BridgeConfig|null} config */
|
|
63
134
|
function isExpired(config) {
|
|
64
135
|
if (!config || !config.discovered) return true;
|
|
65
136
|
const ttlMs = parseTTL(config.ttl);
|
|
@@ -73,8 +144,13 @@ function isExpired(config) {
|
|
|
73
144
|
* - New tools on existing server: added as "allow"
|
|
74
145
|
* - Removed tools on existing server: removed from config
|
|
75
146
|
* - Existing tools: user's allow/deny preserved
|
|
147
|
+
* @param {BridgeConfig|null} existing
|
|
148
|
+
* @param {Map<string, ServerDef>} discovered
|
|
149
|
+
* @param {Map<string, McpTool[]>} freshTools
|
|
150
|
+
* @returns {BridgeConfig}
|
|
76
151
|
*/
|
|
77
152
|
function mergeBridgeConfig(existing, discovered, freshTools) {
|
|
153
|
+
/** @type {BridgeConfig} */
|
|
78
154
|
const merged = {
|
|
79
155
|
discovered: new Date().toISOString(),
|
|
80
156
|
ttl: existing?.ttl || DEFAULT_TTL,
|
|
@@ -86,6 +162,7 @@ function mergeBridgeConfig(existing, discovered, freshTools) {
|
|
|
86
162
|
const existingServer = existing?.servers?.[name];
|
|
87
163
|
const existingTools = existingServer?.tools || {};
|
|
88
164
|
|
|
165
|
+
/** @type {Record<string, string>} */
|
|
89
166
|
const tools = {};
|
|
90
167
|
for (const t of serverTools) {
|
|
91
168
|
tools[t.name] = existingTools[t.name] || 'allow';
|
|
@@ -105,8 +182,13 @@ function mergeBridgeConfig(existing, discovered, freshTools) {
|
|
|
105
182
|
|
|
106
183
|
// --- Env resolution ---
|
|
107
184
|
|
|
185
|
+
/**
|
|
186
|
+
* @param {Record<string, string>} [env]
|
|
187
|
+
* @returns {Record<string, string>}
|
|
188
|
+
*/
|
|
108
189
|
function resolveEnv(env) {
|
|
109
190
|
if (!env) return {};
|
|
191
|
+
/** @type {Record<string, string>} */
|
|
110
192
|
const resolved = {};
|
|
111
193
|
for (const [k, v] of Object.entries(env)) {
|
|
112
194
|
resolved[k] = typeof v === 'string'
|
|
@@ -118,6 +200,11 @@ function resolveEnv(env) {
|
|
|
118
200
|
|
|
119
201
|
// --- JSON-RPC stdio client ---
|
|
120
202
|
|
|
203
|
+
/**
|
|
204
|
+
* @param {string} name
|
|
205
|
+
* @param {ServerDef} def
|
|
206
|
+
* @returns {RpcClient}
|
|
207
|
+
*/
|
|
121
208
|
function createRpcClient(name, def) {
|
|
122
209
|
const { command, args = [], env, cwd } = def;
|
|
123
210
|
const mergedEnv = { ...process.env, ...resolveEnv(env) };
|
|
@@ -128,6 +215,7 @@ function createRpcClient(name, def) {
|
|
|
128
215
|
...(cwd && { cwd }),
|
|
129
216
|
});
|
|
130
217
|
|
|
218
|
+
/** @type {Map<number, {resolve: (v: any) => void, reject: (e: any) => void}>} */
|
|
131
219
|
const pending = new Map();
|
|
132
220
|
let nextId = 1;
|
|
133
221
|
let buffer = '';
|
|
@@ -167,6 +255,11 @@ function createRpcClient(name, def) {
|
|
|
167
255
|
pending.clear();
|
|
168
256
|
});
|
|
169
257
|
|
|
258
|
+
/**
|
|
259
|
+
* @param {string} method
|
|
260
|
+
* @param {object} [params]
|
|
261
|
+
* @returns {Promise<any>}
|
|
262
|
+
*/
|
|
170
263
|
function rpc(method, params = {}) {
|
|
171
264
|
const id = nextId++;
|
|
172
265
|
return new Promise((resolve, reject) => {
|
|
@@ -176,6 +269,10 @@ function createRpcClient(name, def) {
|
|
|
176
269
|
});
|
|
177
270
|
}
|
|
178
271
|
|
|
272
|
+
/**
|
|
273
|
+
* @param {string} method
|
|
274
|
+
* @param {object} [params]
|
|
275
|
+
*/
|
|
179
276
|
function notify(method, params = {}) {
|
|
180
277
|
const msg = JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n';
|
|
181
278
|
child.stdin.write(msg);
|
|
@@ -186,6 +283,10 @@ function createRpcClient(name, def) {
|
|
|
186
283
|
|
|
187
284
|
// --- Content unwrapping ---
|
|
188
285
|
|
|
286
|
+
/**
|
|
287
|
+
* @param {Array<{type?: string, text?: string}>|any} content
|
|
288
|
+
* @returns {string|any}
|
|
289
|
+
*/
|
|
189
290
|
function unwrapContent(content) {
|
|
190
291
|
if (!Array.isArray(content) || content.length === 0) return '';
|
|
191
292
|
if (content.length === 1 && content[0].type === 'text') return content[0].text;
|
|
@@ -197,6 +298,12 @@ function unwrapContent(content) {
|
|
|
197
298
|
// Runtime arg-dependent policy has moved to Loop-level (new Loop({ policy })).
|
|
198
299
|
// mcp-bridge retains only the static .mcp-bridge.json allow/deny filter below —
|
|
199
300
|
// that decides which tools are exposed to the Loop in the first place.
|
|
301
|
+
/**
|
|
302
|
+
* @param {string} serverName
|
|
303
|
+
* @param {McpTool[]} mcpTools
|
|
304
|
+
* @param {(method: string, params?: object) => Promise<any>} rpc
|
|
305
|
+
* @returns {ToolDef[]}
|
|
306
|
+
*/
|
|
200
307
|
function wrapTools(serverName, mcpTools, rpc) {
|
|
201
308
|
return mcpTools.map(t => ({
|
|
202
309
|
name: `${serverName}_${t.name}`,
|
|
@@ -216,6 +323,7 @@ function wrapTools(serverName, mcpTools, rpc) {
|
|
|
216
323
|
|
|
217
324
|
// --- Server lifecycle ---
|
|
218
325
|
|
|
326
|
+
/** @param {import('node:child_process').ChildProcess} child */
|
|
219
327
|
async function killServer(child) {
|
|
220
328
|
if (child.exitCode !== null) return;
|
|
221
329
|
|
|
@@ -228,7 +336,9 @@ async function killServer(child) {
|
|
|
228
336
|
// Short grace, then SIGTERM, then SIGKILL. Each wait clears its timer
|
|
229
337
|
// promptly when the child closes so we don't block the event loop after
|
|
230
338
|
// exit (which kept node:test's file-level wrapper hanging).
|
|
339
|
+
/** @param {number} ms @returns {Promise<void>} */
|
|
231
340
|
const waitClose = (ms) => new Promise(resolve => {
|
|
341
|
+
/** @type {NodeJS.Timeout} */
|
|
232
342
|
let timer;
|
|
233
343
|
const onClose = () => { clearTimeout(timer); resolve(); };
|
|
234
344
|
child.once('close', onClose);
|
|
@@ -254,34 +364,57 @@ async function killServer(child) {
|
|
|
254
364
|
|
|
255
365
|
// --- Connect + list tools from a server ---
|
|
256
366
|
|
|
367
|
+
/**
|
|
368
|
+
* @param {string} name
|
|
369
|
+
* @param {ServerDef} def
|
|
370
|
+
* @param {number} [timeout]
|
|
371
|
+
* @returns {Promise<{mcpTools: McpTool[], client: RpcClient}>}
|
|
372
|
+
*/
|
|
257
373
|
async function connectAndListTools(name, def, timeout = 15000) {
|
|
258
374
|
const client = createRpcClient(name, def);
|
|
259
375
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
376
|
+
try {
|
|
377
|
+
const init = client.rpc('initialize', {
|
|
378
|
+
protocolVersion: '2024-11-05',
|
|
379
|
+
capabilities: {},
|
|
380
|
+
clientInfo: { name: 'bare-agent', version: '0.5.0' },
|
|
381
|
+
});
|
|
265
382
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
383
|
+
let timerId;
|
|
384
|
+
const timer = new Promise((_, reject) => {
|
|
385
|
+
timerId = setTimeout(() => reject(new ToolError(`MCP server "${name}" init timed out after ${timeout}ms`)), timeout);
|
|
386
|
+
});
|
|
270
387
|
|
|
271
|
-
|
|
272
|
-
|
|
388
|
+
try { await Promise.race([init, timer]); } finally { clearTimeout(timerId); }
|
|
389
|
+
client.notify('notifications/initialized');
|
|
273
390
|
|
|
274
|
-
|
|
391
|
+
const { tools: mcpTools } = await client.rpc('tools/list');
|
|
275
392
|
|
|
276
|
-
|
|
393
|
+
return { mcpTools, client };
|
|
394
|
+
} catch (err) {
|
|
395
|
+
// Connect failed (init timeout, tools/list error, etc.) — the child was
|
|
396
|
+
// spawned but never returned to the caller, so the caller can't track it
|
|
397
|
+
// for close(). Kill it here, or its open stdin pipe keeps the event loop
|
|
398
|
+
// alive and hangs the process on exit (notably node:test's file wrapper).
|
|
399
|
+
await killServer(client.child);
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
277
402
|
}
|
|
278
403
|
|
|
279
404
|
// --- System context for LLM ---
|
|
280
405
|
|
|
406
|
+
/**
|
|
407
|
+
* @param {string[]} servers
|
|
408
|
+
* @param {ToolDef[]} tools
|
|
409
|
+
* @param {DeniedTool[]} denied
|
|
410
|
+
* @returns {string}
|
|
411
|
+
*/
|
|
281
412
|
function buildSystemContext(servers, tools, denied) {
|
|
413
|
+
/** @type {string[]} */
|
|
282
414
|
const lines = [];
|
|
283
415
|
lines.push(`MCP Bridge: ${tools.length} tools available from ${servers.length} server(s): ${servers.join(', ')}.`);
|
|
284
416
|
|
|
417
|
+
/** @type {Record<string, string[]>} */
|
|
285
418
|
const byServer = {};
|
|
286
419
|
for (const t of tools) {
|
|
287
420
|
const sep = t.name.indexOf('_');
|
|
@@ -328,9 +461,9 @@ function buildSystemContext(servers, tools, denied) {
|
|
|
328
461
|
* tool name doesn't travel as `action.type` — that's a deliberate v0.9
|
|
329
462
|
* trade for one consistent gate-check call per LLM tool invocation.
|
|
330
463
|
*
|
|
331
|
-
* @param {
|
|
332
|
-
* @param {string} discoveredAt - ISO timestamp from .mcp-bridge.json.
|
|
333
|
-
* @returns {
|
|
464
|
+
* @param {ToolDef[]} tools - The bulk-loaded, name-prefixed tools array.
|
|
465
|
+
* @param {string} [discoveredAt] - ISO timestamp from .mcp-bridge.json.
|
|
466
|
+
* @returns {ToolDef[]} [mcp_discover, mcp_invoke]
|
|
334
467
|
*/
|
|
335
468
|
function buildMetaTools(tools, discoveredAt) {
|
|
336
469
|
// Catalog descriptors: same info the LLM would see for bulk-loaded tools,
|
|
@@ -364,7 +497,7 @@ function buildMetaTools(tools, discoveredAt) {
|
|
|
364
497
|
},
|
|
365
498
|
},
|
|
366
499
|
},
|
|
367
|
-
execute: async ({ server } = {}) => {
|
|
500
|
+
execute: async (/** @type {{ server?: string }} */ { server } = {}) => {
|
|
368
501
|
const filtered = server
|
|
369
502
|
? catalog.filter(t => t.server === server)
|
|
370
503
|
: catalog;
|
|
@@ -394,7 +527,7 @@ function buildMetaTools(tools, discoveredAt) {
|
|
|
394
527
|
},
|
|
395
528
|
required: ['name'],
|
|
396
529
|
},
|
|
397
|
-
execute: async ({ name, args }) => {
|
|
530
|
+
execute: async (/** @type {{ name: string, args?: object }} */ { name, args }) => {
|
|
398
531
|
const tool = byName.get(name);
|
|
399
532
|
if (!tool) {
|
|
400
533
|
throw new ToolError(`mcp_invoke: unknown tool "${name}". Call mcp_discover for the current catalog.`, {
|
|
@@ -431,14 +564,14 @@ function buildMetaTools(tools, discoveredAt) {
|
|
|
431
564
|
* @param {string[]} [opts.servers] - Limit to these server names.
|
|
432
565
|
* @param {number} [opts.timeout=15000] - Per-server init timeout in ms.
|
|
433
566
|
* @param {boolean} [opts.refresh=false] - Force re-discovery regardless of TTL.
|
|
434
|
-
* @param {(name: string, def:
|
|
567
|
+
* @param {(name: string, def: ServerDef) => boolean | Promise<boolean>} [opts.confirmServer]
|
|
435
568
|
* Vet each discovered server BEFORE its `command` is spawned. Connecting to an
|
|
436
569
|
* MCP server runs its command, and discovery reads configs from the cwd (a
|
|
437
570
|
* `.mcp.json` in an untrusted repo) as well as the user's home/IDE configs.
|
|
438
571
|
* Return false to skip a server (its command is never executed). A throw is
|
|
439
572
|
* treated as a deny (fail-closed). Default: every discovered server is trusted
|
|
440
573
|
* (unchanged behavior) — pass this to gate command execution.
|
|
441
|
-
* @returns {Promise<{tools:
|
|
574
|
+
* @returns {Promise<{tools: ToolDef[], metaTools?: ToolDef[], servers: string[], systemContext: string, denied: DeniedTool[], errors?: Array<{server: string, error: string}>, close: Function}>}
|
|
442
575
|
*/
|
|
443
576
|
async function createMCPBridge(opts = {}) {
|
|
444
577
|
if ('policy' in opts) {
|
|
@@ -454,6 +587,7 @@ async function createMCPBridge(opts = {}) {
|
|
|
454
587
|
// Vet a server before spawning its command. Fail-closed: an undefined hook
|
|
455
588
|
// trusts all (unchanged behavior); a throw denies.
|
|
456
589
|
const confirmServer = typeof opts.confirmServer === 'function' ? opts.confirmServer : null;
|
|
590
|
+
/** @param {string} name @param {ServerDef} def @returns {Promise<boolean>} */
|
|
457
591
|
const vetServer = async (name, def) => {
|
|
458
592
|
if (!confirmServer) return true;
|
|
459
593
|
try { return (await confirmServer(name, def)) === true; }
|
|
@@ -475,12 +609,16 @@ async function createMCPBridge(opts = {}) {
|
|
|
475
609
|
// If discovered.size === 0 but config exists, fall through and use the existing config
|
|
476
610
|
// rather than wiping it on a transient discovery failure.
|
|
477
611
|
if (discovered.size > 0) {
|
|
612
|
+
/** @type {Map<string, McpTool[]>} */
|
|
478
613
|
const freshTools = new Map();
|
|
614
|
+
/** @type {Map<string, RpcClient>} */
|
|
479
615
|
const connectResults = new Map();
|
|
616
|
+
/** @type {Array<{server: string, error: string}>} */
|
|
480
617
|
const errors = [];
|
|
481
618
|
|
|
482
|
-
const
|
|
483
|
-
|
|
619
|
+
const reqServers = opts.servers;
|
|
620
|
+
const toDiscover = reqServers
|
|
621
|
+
? [...discovered.entries()].filter(([n]) => reqServers.includes(n))
|
|
484
622
|
: [...discovered.entries()];
|
|
485
623
|
|
|
486
624
|
await Promise.all(toDiscover.map(async ([name, def]) => {
|
|
@@ -518,24 +656,38 @@ async function createMCPBridge(opts = {}) {
|
|
|
518
656
|
}
|
|
519
657
|
}
|
|
520
658
|
|
|
659
|
+
// At this point config is guaranteed non-null: it either existed, was just
|
|
660
|
+
// written by the refresh branch, or we returned early above.
|
|
661
|
+
if (!config) {
|
|
662
|
+
return { tools: [], servers: [], systemContext: '', denied: [], close: async () => {} };
|
|
663
|
+
}
|
|
664
|
+
/** @type {BridgeConfig} */
|
|
665
|
+
const cfg = config;
|
|
666
|
+
|
|
521
667
|
// Filter to requested servers
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
668
|
+
const reqServers2 = opts.servers;
|
|
669
|
+
const serverNames = reqServers2
|
|
670
|
+
? reqServers2.filter(n => cfg.servers[n])
|
|
671
|
+
: Object.keys(cfg.servers);
|
|
525
672
|
|
|
526
673
|
if (serverNames.length === 0) {
|
|
527
674
|
return { tools: [], servers: [], systemContext: '', denied: [], close: async () => {} };
|
|
528
675
|
}
|
|
529
676
|
|
|
530
677
|
// Connect to servers and wrap only allowed tools
|
|
678
|
+
/** @type {ToolDef[]} */
|
|
531
679
|
const tools = [];
|
|
680
|
+
/** @type {import('node:child_process').ChildProcess[]} */
|
|
532
681
|
const children = [];
|
|
682
|
+
/** @type {string[]} */
|
|
533
683
|
const connected = [];
|
|
684
|
+
/** @type {DeniedTool[]} */
|
|
534
685
|
const denied = [];
|
|
686
|
+
/** @type {Array<{server: string, error: string}>} */
|
|
535
687
|
const errors = [];
|
|
536
688
|
|
|
537
689
|
await Promise.all(serverNames.map(async (name) => {
|
|
538
|
-
const serverConf =
|
|
690
|
+
const serverConf = cfg.servers[name];
|
|
539
691
|
const allowedToolNames = Object.entries(serverConf.tools)
|
|
540
692
|
.filter(([, perm]) => perm === 'allow')
|
|
541
693
|
.map(([t]) => t);
|
package/src/mcp.d.ts
ADDED
package/src/memory.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type Store = import("../types").Store;
|
|
2
|
+
/**
|
|
3
|
+
* Persistence + search across turns and sessions.
|
|
4
|
+
* Thin wrapper that delegates to a swappable store.
|
|
5
|
+
*
|
|
6
|
+
* Interface:
|
|
7
|
+
* store(content, metadata) → id
|
|
8
|
+
* search(query, options) → [{ id, content, metadata, score }]
|
|
9
|
+
* get(id) → { content, metadata }
|
|
10
|
+
* delete(id) → void
|
|
11
|
+
*
|
|
12
|
+
* Stores (swappable):
|
|
13
|
+
* SQLite FTS5 — store-sqlite.js (peer dep: better-sqlite3)
|
|
14
|
+
* JSON file — store-jsonfile.js (zero deps)
|
|
15
|
+
* Bring your own: implement { store, search, get, delete }
|
|
16
|
+
*/
|
|
17
|
+
/** @typedef {import('../types').Store} Store */
|
|
18
|
+
export class Memory {
|
|
19
|
+
/**
|
|
20
|
+
* @param {{ store?: Store }} [options] - Store backend (must implement store/search/get/delete).
|
|
21
|
+
* @throws {Error} `[Memory] requires options.store` — when options.store is missing.
|
|
22
|
+
*/
|
|
23
|
+
constructor(options?: {
|
|
24
|
+
store?: Store;
|
|
25
|
+
});
|
|
26
|
+
/** @type {Store} */
|
|
27
|
+
_store: Store;
|
|
28
|
+
/**
|
|
29
|
+
* @param {any} content
|
|
30
|
+
* @param {Record<string, any>} [metadata]
|
|
31
|
+
* @returns {any} id
|
|
32
|
+
*/
|
|
33
|
+
store(content: any, metadata?: Record<string, any>): any;
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} query
|
|
36
|
+
* @param {Record<string, any>} [options]
|
|
37
|
+
* @returns {any}
|
|
38
|
+
*/
|
|
39
|
+
search(query: string, options?: Record<string, any>): any;
|
|
40
|
+
/**
|
|
41
|
+
* @param {any} id
|
|
42
|
+
* @returns {any}
|
|
43
|
+
*/
|
|
44
|
+
get(id: any): any;
|
|
45
|
+
/**
|
|
46
|
+
* @param {any} id
|
|
47
|
+
* @returns {any}
|
|
48
|
+
*/
|
|
49
|
+
delete(id: any): any;
|
|
50
|
+
}
|
package/src/memory.js
CHANGED
|
@@ -15,29 +15,49 @@
|
|
|
15
15
|
* JSON file — store-jsonfile.js (zero deps)
|
|
16
16
|
* Bring your own: implement { store, search, get, delete }
|
|
17
17
|
*/
|
|
18
|
+
/** @typedef {import('../types').Store} Store */
|
|
19
|
+
|
|
18
20
|
class Memory {
|
|
19
21
|
/**
|
|
20
|
-
* @param {
|
|
21
|
-
* @param {object} options.store - Store backend (must implement store/search/get/delete).
|
|
22
|
+
* @param {{ store?: Store }} [options] - Store backend (must implement store/search/get/delete).
|
|
22
23
|
* @throws {Error} `[Memory] requires options.store` — when options.store is missing.
|
|
23
24
|
*/
|
|
24
25
|
constructor(options = {}) {
|
|
25
26
|
if (!options.store) throw new Error('[Memory] requires options.store');
|
|
27
|
+
/** @type {Store} */
|
|
26
28
|
this._store = options.store;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @param {any} content
|
|
33
|
+
* @param {Record<string, any>} [metadata]
|
|
34
|
+
* @returns {any} id
|
|
35
|
+
*/
|
|
29
36
|
store(content, metadata = {}) {
|
|
30
37
|
return this._store.store(content, metadata);
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} query
|
|
42
|
+
* @param {Record<string, any>} [options]
|
|
43
|
+
* @returns {any}
|
|
44
|
+
*/
|
|
33
45
|
search(query, options = {}) {
|
|
34
46
|
return this._store.search(query, options);
|
|
35
47
|
}
|
|
36
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @param {any} id
|
|
51
|
+
* @returns {any}
|
|
52
|
+
*/
|
|
37
53
|
get(id) {
|
|
38
54
|
return this._store.get(id);
|
|
39
55
|
}
|
|
40
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @param {any} id
|
|
59
|
+
* @returns {any}
|
|
60
|
+
*/
|
|
41
61
|
delete(id) {
|
|
42
62
|
return this._store.delete(id);
|
|
43
63
|
}
|