bare-agent 0.11.0 → 0.12.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.
Files changed (65) hide show
  1. package/bin/cli.d.ts +4 -0
  2. package/bin/cli.js +40 -10
  3. package/bin/test-provider.d.ts +2 -0
  4. package/bin/test-provider.js +5 -1
  5. package/index.d.ts +20 -0
  6. package/package.json +44 -10
  7. package/src/bareguard-adapter.d.ts +118 -0
  8. package/src/bareguard-adapter.js +75 -3
  9. package/src/checkpoint.d.ts +61 -0
  10. package/src/checkpoint.js +17 -8
  11. package/src/circuit-breaker.d.ts +70 -0
  12. package/src/circuit-breaker.js +20 -4
  13. package/src/errors.d.ts +106 -0
  14. package/src/errors.js +50 -1
  15. package/src/loop.d.ts +135 -0
  16. package/src/loop.js +73 -17
  17. package/src/mcp-bridge.d.ts +133 -0
  18. package/src/mcp-bridge.js +179 -27
  19. package/src/mcp.d.ts +4 -0
  20. package/src/memory.d.ts +50 -0
  21. package/src/memory.js +22 -2
  22. package/src/planner.d.ts +62 -0
  23. package/src/planner.js +26 -7
  24. package/src/provider-anthropic.d.ts +55 -0
  25. package/src/provider-anthropic.js +32 -11
  26. package/src/provider-clipipe.d.ts +86 -0
  27. package/src/provider-clipipe.js +28 -18
  28. package/src/provider-fallback.d.ts +44 -0
  29. package/src/provider-fallback.js +18 -8
  30. package/src/provider-ollama.d.ts +41 -0
  31. package/src/provider-ollama.js +27 -7
  32. package/src/provider-openai.d.ts +57 -0
  33. package/src/provider-openai.js +31 -16
  34. package/src/providers.d.ts +6 -0
  35. package/src/retry.d.ts +44 -0
  36. package/src/retry.js +15 -1
  37. package/src/run-plan.d.ts +126 -0
  38. package/src/run-plan.js +46 -13
  39. package/src/scheduler.d.ts +102 -0
  40. package/src/scheduler.js +32 -4
  41. package/src/state.d.ts +45 -0
  42. package/src/state.js +18 -2
  43. package/src/store-jsonfile.d.ts +85 -0
  44. package/src/store-jsonfile.js +33 -8
  45. package/src/store-sqlite.d.ts +90 -0
  46. package/src/store-sqlite.js +31 -7
  47. package/src/stores.d.ts +3 -0
  48. package/src/stream.d.ts +79 -0
  49. package/src/stream.js +32 -0
  50. package/src/tools.d.ts +8 -0
  51. package/src/transport-jsonl.d.ts +30 -0
  52. package/src/transport-jsonl.js +13 -0
  53. package/src/transports.d.ts +2 -0
  54. package/tools/browse.d.ts +10 -0
  55. package/tools/browse.js +2 -0
  56. package/tools/defer.d.ts +33 -0
  57. package/tools/defer.js +12 -3
  58. package/tools/mobile.d.ts +34 -0
  59. package/tools/mobile.js +28 -15
  60. package/tools/shell.d.ts +31 -0
  61. package/tools/shell.js +55 -6
  62. package/tools/spawn.d.ts +107 -0
  63. package/tools/spawn.js +24 -5
  64. package/types/index.d.ts +66 -0
  65. 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
- const unit = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[match[2]];
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
- const init = client.rpc('initialize', {
261
- protocolVersion: '2024-11-05',
262
- capabilities: {},
263
- clientInfo: { name: 'bare-agent', version: '0.5.0' },
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
- let timerId;
267
- const timer = new Promise((_, reject) => {
268
- timerId = setTimeout(() => reject(new ToolError(`MCP server "${name}" init timed out after ${timeout}ms`)), timeout);
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
- try { await Promise.race([init, timer]); } finally { clearTimeout(timerId); }
272
- client.notify('notifications/initialized');
388
+ try { await Promise.race([init, timer]); } finally { clearTimeout(timerId); }
389
+ client.notify('notifications/initialized');
273
390
 
274
- const { tools: mcpTools } = await client.rpc('tools/list');
391
+ const { tools: mcpTools } = await client.rpc('tools/list');
275
392
 
276
- return { mcpTools, client };
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 {Array} tools - The bulk-loaded, name-prefixed tools array.
332
- * @param {string} discoveredAt - ISO timestamp from .mcp-bridge.json.
333
- * @returns {Array} [mcp_discover, mcp_invoke]
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: object) => boolean | Promise<boolean>} [opts.confirmServer]
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: Array, metaTools: Array, servers: string[], systemContext: string, denied: Array, close: Function}>}
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 toDiscover = opts.servers
483
- ? [...discovered.entries()].filter(([n]) => opts.servers.includes(n))
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 serverNames = opts.servers
523
- ? opts.servers.filter(n => config.servers[n])
524
- : Object.keys(config.servers);
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 = config.servers[name];
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
@@ -0,0 +1,4 @@
1
+ import { createMCPBridge } from "./mcp-bridge";
2
+ import { discoverServers } from "./mcp-bridge";
3
+ import { buildMetaTools } from "./mcp-bridge";
4
+ export { createMCPBridge, discoverServers, buildMetaTools };
@@ -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 {object} options
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
  }