@burtson-labs/agent-core 1.6.13
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/LICENSE +201 -0
- package/README.md +88 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/activation.d.ts +60 -0
- package/dist/mcp/activation.d.ts.map +1 -0
- package/dist/mcp/activation.js +139 -0
- package/dist/mcp/activation.js.map +1 -0
- package/dist/mcp/clientPool.d.ts +202 -0
- package/dist/mcp/clientPool.d.ts.map +1 -0
- package/dist/mcp/clientPool.js +469 -0
- package/dist/mcp/clientPool.js.map +1 -0
- package/dist/mcp/index.d.ts +18 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +28 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +43 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +130 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/toolAdapter.d.ts +57 -0
- package/dist/mcp/toolAdapter.d.ts.map +1 -0
- package/dist/mcp/toolAdapter.js +223 -0
- package/dist/mcp/toolAdapter.js.map +1 -0
- package/dist/mcp/types.d.ts +122 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +15 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/providers/deterministic-provider.d.ts +21 -0
- package/dist/providers/deterministic-provider.d.ts.map +1 -0
- package/dist/providers/deterministic-provider.js +80 -0
- package/dist/providers/deterministic-provider.js.map +1 -0
- package/dist/providers/provider-client.d.ts +12 -0
- package/dist/providers/provider-client.d.ts.map +1 -0
- package/dist/providers/provider-client.js +11 -0
- package/dist/providers/provider-client.js.map +1 -0
- package/dist/runtime/AgentRuntime.d.ts +67 -0
- package/dist/runtime/AgentRuntime.d.ts.map +1 -0
- package/dist/runtime/AgentRuntime.js +382 -0
- package/dist/runtime/AgentRuntime.js.map +1 -0
- package/dist/security/secretPatterns.d.ts +76 -0
- package/dist/security/secretPatterns.d.ts.map +1 -0
- package/dist/security/secretPatterns.js +290 -0
- package/dist/security/secretPatterns.js.map +1 -0
- package/dist/tools/ask-user-tool.d.ts +19 -0
- package/dist/tools/ask-user-tool.d.ts.map +1 -0
- package/dist/tools/ask-user-tool.js +148 -0
- package/dist/tools/ask-user-tool.js.map +1 -0
- package/dist/tools/compactMessages.d.ts +52 -0
- package/dist/tools/compactMessages.d.ts.map +1 -0
- package/dist/tools/compactMessages.js +158 -0
- package/dist/tools/compactMessages.js.map +1 -0
- package/dist/tools/core-tools.d.ts +29 -0
- package/dist/tools/core-tools.d.ts.map +1 -0
- package/dist/tools/core-tools.js +2214 -0
- package/dist/tools/core-tools.js.map +1 -0
- package/dist/tools/git-tools.d.ts +32 -0
- package/dist/tools/git-tools.d.ts.map +1 -0
- package/dist/tools/git-tools.js +330 -0
- package/dist/tools/git-tools.js.map +1 -0
- package/dist/tools/index.d.ts +15 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +31 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/language-adapters.d.ts +48 -0
- package/dist/tools/language-adapters.d.ts.map +1 -0
- package/dist/tools/language-adapters.js +299 -0
- package/dist/tools/language-adapters.js.map +1 -0
- package/dist/tools/loop/compactionTrigger.d.ts +47 -0
- package/dist/tools/loop/compactionTrigger.d.ts.map +1 -0
- package/dist/tools/loop/compactionTrigger.js +32 -0
- package/dist/tools/loop/compactionTrigger.js.map +1 -0
- package/dist/tools/loop/finalAnswerNudges.d.ts +68 -0
- package/dist/tools/loop/finalAnswerNudges.d.ts.map +1 -0
- package/dist/tools/loop/finalAnswerNudges.js +87 -0
- package/dist/tools/loop/finalAnswerNudges.js.map +1 -0
- package/dist/tools/loop/goalAnchor.d.ts +72 -0
- package/dist/tools/loop/goalAnchor.d.ts.map +1 -0
- package/dist/tools/loop/goalAnchor.js +76 -0
- package/dist/tools/loop/goalAnchor.js.map +1 -0
- package/dist/tools/loop/llmStream.d.ts +70 -0
- package/dist/tools/loop/llmStream.d.ts.map +1 -0
- package/dist/tools/loop/llmStream.js +181 -0
- package/dist/tools/loop/llmStream.js.map +1 -0
- package/dist/tools/loop/parallelExecute.d.ts +57 -0
- package/dist/tools/loop/parallelExecute.d.ts.map +1 -0
- package/dist/tools/loop/parallelExecute.js +54 -0
- package/dist/tools/loop/parallelExecute.js.map +1 -0
- package/dist/tools/loop/singleToolExecute.d.ts +71 -0
- package/dist/tools/loop/singleToolExecute.d.ts.map +1 -0
- package/dist/tools/loop/singleToolExecute.js +139 -0
- package/dist/tools/loop/singleToolExecute.js.map +1 -0
- package/dist/tools/loop/toolCallNormalize.d.ts +57 -0
- package/dist/tools/loop/toolCallNormalize.d.ts.map +1 -0
- package/dist/tools/loop/toolCallNormalize.js +99 -0
- package/dist/tools/loop/toolCallNormalize.js.map +1 -0
- package/dist/tools/loop/turnSetup.d.ts +43 -0
- package/dist/tools/loop/turnSetup.d.ts.map +1 -0
- package/dist/tools/loop/turnSetup.js +48 -0
- package/dist/tools/loop/turnSetup.js.map +1 -0
- package/dist/tools/ocr.d.ts +52 -0
- package/dist/tools/ocr.d.ts.map +1 -0
- package/dist/tools/ocr.js +238 -0
- package/dist/tools/ocr.js.map +1 -0
- package/dist/tools/post-edit-checks.d.ts +46 -0
- package/dist/tools/post-edit-checks.d.ts.map +1 -0
- package/dist/tools/post-edit-checks.js +236 -0
- package/dist/tools/post-edit-checks.js.map +1 -0
- package/dist/tools/skill-loader.d.ts +94 -0
- package/dist/tools/skill-loader.d.ts.map +1 -0
- package/dist/tools/skill-loader.js +422 -0
- package/dist/tools/skill-loader.js.map +1 -0
- package/dist/tools/skill-registry.d.ts +44 -0
- package/dist/tools/skill-registry.d.ts.map +1 -0
- package/dist/tools/skill-registry.js +118 -0
- package/dist/tools/skill-registry.js.map +1 -0
- package/dist/tools/skill-types.d.ts +38 -0
- package/dist/tools/skill-types.d.ts.map +1 -0
- package/dist/tools/skill-types.js +10 -0
- package/dist/tools/skill-types.js.map +1 -0
- package/dist/tools/skills/code-review-skill.d.ts +9 -0
- package/dist/tools/skills/code-review-skill.d.ts.map +1 -0
- package/dist/tools/skills/code-review-skill.js +66 -0
- package/dist/tools/skills/code-review-skill.js.map +1 -0
- package/dist/tools/skills/core-skill.d.ts +13 -0
- package/dist/tools/skills/core-skill.d.ts.map +1 -0
- package/dist/tools/skills/core-skill.js +23 -0
- package/dist/tools/skills/core-skill.js.map +1 -0
- package/dist/tools/skills/git-skill.d.ts +10 -0
- package/dist/tools/skills/git-skill.d.ts.map +1 -0
- package/dist/tools/skills/git-skill.js +30 -0
- package/dist/tools/skills/git-skill.js.map +1 -0
- package/dist/tools/skills/index.d.ts +17 -0
- package/dist/tools/skills/index.d.ts.map +1 -0
- package/dist/tools/skills/index.js +49 -0
- package/dist/tools/skills/index.js.map +1 -0
- package/dist/tools/skills/interaction-skill.d.ts +14 -0
- package/dist/tools/skills/interaction-skill.d.ts.map +1 -0
- package/dist/tools/skills/interaction-skill.js +24 -0
- package/dist/tools/skills/interaction-skill.js.map +1 -0
- package/dist/tools/skills/mail-search-skill.d.ts +25 -0
- package/dist/tools/skills/mail-search-skill.d.ts.map +1 -0
- package/dist/tools/skills/mail-search-skill.js +343 -0
- package/dist/tools/skills/mail-search-skill.js.map +1 -0
- package/dist/tools/skills/plan-skill.d.ts +10 -0
- package/dist/tools/skills/plan-skill.d.ts.map +1 -0
- package/dist/tools/skills/plan-skill.js +126 -0
- package/dist/tools/skills/plan-skill.js.map +1 -0
- package/dist/tools/skills/semantic-search-skill.d.ts +22 -0
- package/dist/tools/skills/semantic-search-skill.d.ts.map +1 -0
- package/dist/tools/skills/semantic-search-skill.js +244 -0
- package/dist/tools/skills/semantic-search-skill.js.map +1 -0
- package/dist/tools/skills/test-gen-skill.d.ts +9 -0
- package/dist/tools/skills/test-gen-skill.d.ts.map +1 -0
- package/dist/tools/skills/test-gen-skill.js +123 -0
- package/dist/tools/skills/test-gen-skill.js.map +1 -0
- package/dist/tools/tool-registry.d.ts +60 -0
- package/dist/tools/tool-registry.d.ts.map +1 -0
- package/dist/tools/tool-registry.js +200 -0
- package/dist/tools/tool-registry.js.map +1 -0
- package/dist/tools/tool-types.d.ts +281 -0
- package/dist/tools/tool-types.d.ts.map +1 -0
- package/dist/tools/tool-types.js +10 -0
- package/dist/tools/tool-types.js.map +1 -0
- package/dist/tools/tool-use-loop.d.ts +231 -0
- package/dist/tools/tool-use-loop.d.ts.map +1 -0
- package/dist/tools/tool-use-loop.js +2057 -0
- package/dist/tools/tool-use-loop.js.map +1 -0
- package/dist/tools/tool-use-parser.d.ts +78 -0
- package/dist/tools/tool-use-parser.d.ts.map +1 -0
- package/dist/tools/tool-use-parser.js +427 -0
- package/dist/tools/tool-use-parser.js.map +1 -0
- package/dist/tools/toolAvailabilityDetector.d.ts +48 -0
- package/dist/tools/toolAvailabilityDetector.d.ts.map +1 -0
- package/dist/tools/toolAvailabilityDetector.js +156 -0
- package/dist/tools/toolAvailabilityDetector.js.map +1 -0
- package/dist/tools/unified-patch.d.ts +87 -0
- package/dist/tools/unified-patch.d.ts.map +1 -0
- package/dist/tools/unified-patch.js +217 -0
- package/dist/tools/unified-patch.js.map +1 -0
- package/dist/types/agent.d.ts +69 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/agent.js +54 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/tasks.d.ts +22 -0
- package/dist/types/tasks.d.ts.map +1 -0
- package/dist/types/tasks.js +3 -0
- package/dist/types/tasks.js.map +1 -0
- package/dist/utils/event-emitter.d.ts +13 -0
- package/dist/utils/event-emitter.d.ts.map +1 -0
- package/dist/utils/event-emitter.js +54 -0
- package/dist/utils/event-emitter.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP client pool — manages spawn / handshake / lifecycle for N
|
|
3
|
+
* configured MCP servers. Each entry corresponds to one mcp-servers.json
|
|
4
|
+
* stanza. The pool is intentionally small surface:
|
|
5
|
+
*
|
|
6
|
+
* register(name, config) — store config (no spawn yet)
|
|
7
|
+
* ensureConnected(name) — spawn + handshake + cache. Idempotent.
|
|
8
|
+
* discoverTools(name) — list tools from a connected server (cached
|
|
9
|
+
* after first call until disconnect)
|
|
10
|
+
* callTool(name, tool, args) — proxy through to server.callTool()
|
|
11
|
+
* snapshot() — status view for /mcp and the IDE Connections tab
|
|
12
|
+
* dispose() — close every spawned process. Idempotent.
|
|
13
|
+
*
|
|
14
|
+
* Lazy spawn: nothing happens at register-time. The first ensureConnected
|
|
15
|
+
* triggers the actual child_process. Avoids paying spawn cost for servers
|
|
16
|
+
* the user configures but never invokes in this session.
|
|
17
|
+
*
|
|
18
|
+
* Failure isolation: a spawn/handshake error is recorded as the server's
|
|
19
|
+
* status and never thrown to the caller. discoverTools returns [] for
|
|
20
|
+
* failed servers. The agent loop continues with native tools only —
|
|
21
|
+
* one bad server doesn't kill the session.
|
|
22
|
+
*/
|
|
23
|
+
import type { McpServerConfig, McpServerSnapshot } from './types';
|
|
24
|
+
/**
|
|
25
|
+
* Trust gate. Spawning an MCP server is unconstrained code execution
|
|
26
|
+
* via child_process. The host (CLI / extension) supplies a callback
|
|
27
|
+
* that decides whether a never-seen-before server config is allowed
|
|
28
|
+
* to spawn. Decision is made on `(name, command, args, env-keys)` —
|
|
29
|
+
* env VALUES are intentionally NOT part of the fingerprint so a token
|
|
30
|
+
* rotation doesn't re-trigger the prompt.
|
|
31
|
+
*
|
|
32
|
+
* Returning `true` allows the spawn for this session. Persisting the
|
|
33
|
+
* "always allow" decision to disk is the host's responsibility — the
|
|
34
|
+
* pool only sees the boolean answer.
|
|
35
|
+
*
|
|
36
|
+
* When no gate is wired (default), every config is allowed — backwards
|
|
37
|
+
* compatible with hosts that don't yet implement trust prompts.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Trust gate input — one shape for stdio servers (existing) and one for
|
|
41
|
+
* URL-based remote servers (v1.7.333+). The discriminator is the
|
|
42
|
+
* `kind` field so existing handlers that destructure
|
|
43
|
+
* `{name, command, args, envKeys}` keep working on stdio entries — they
|
|
44
|
+
* just need to check `kind === 'url'` and surface a URL-shaped prompt
|
|
45
|
+
* when one arrives.
|
|
46
|
+
*/
|
|
47
|
+
export type McpTrustGate = (params: {
|
|
48
|
+
kind: 'stdio';
|
|
49
|
+
name: string;
|
|
50
|
+
command: string;
|
|
51
|
+
args: string[];
|
|
52
|
+
envKeys: string[];
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'url';
|
|
55
|
+
name: string;
|
|
56
|
+
url: string;
|
|
57
|
+
/** Short label for the auth strategy, e.g. "bandit-api-key" / "bearer" /
|
|
58
|
+
* "header(X-Foo)" / "none". The gate decides whether to show / how to
|
|
59
|
+
* phrase the auth in the trust prompt. */
|
|
60
|
+
authKind: string;
|
|
61
|
+
}) => Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Stable fingerprint of a server config. Used by hosts to remember
|
|
64
|
+
* "the user already approved this exact shape" across sessions.
|
|
65
|
+
* env VALUES are excluded — only env KEYS count — so rotating a token
|
|
66
|
+
* doesn't re-trigger the trust prompt.
|
|
67
|
+
*/
|
|
68
|
+
export declare function fingerprintServerConfig(name: string, config: McpServerConfig): string;
|
|
69
|
+
/** Tool definition as advertised by an MCP server's listTools response.
|
|
70
|
+
* Exposed so hosts that persist the tool list (see McpClientPoolOptions.
|
|
71
|
+
* onToolsDiscovered) can round-trip the right shape into
|
|
72
|
+
* `primeDiscoveryCache` on the next session. */
|
|
73
|
+
export interface RemoteToolDef {
|
|
74
|
+
name: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
inputSchema?: {
|
|
77
|
+
type?: string;
|
|
78
|
+
properties?: Record<string, {
|
|
79
|
+
type?: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
}>;
|
|
82
|
+
required?: string[];
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Fired once after the pool successfully fetches a server's tool list
|
|
86
|
+
* from a live spawn — so the host can persist the result and prime
|
|
87
|
+
* future sessions without paying the spawn cost just to enumerate. */
|
|
88
|
+
export type McpToolsDiscoveredCallback = (name: string, fingerprint: string, tools: RemoteToolDef[]) => void;
|
|
89
|
+
export interface McpClientPoolOptions {
|
|
90
|
+
/** Optional trust gate — see McpTrustGate. When omitted, every
|
|
91
|
+
* server config is allowed to spawn (current behavior). When
|
|
92
|
+
* provided, the gate is consulted before each first-spawn and the
|
|
93
|
+
* spawn is rejected with a "trust_denied" status if the gate
|
|
94
|
+
* returns false. */
|
|
95
|
+
trustGate?: McpTrustGate;
|
|
96
|
+
/** Optional disk-cache hook. Fired once per (name, fingerprint) after
|
|
97
|
+
* the first successful listTools — hosts persist the result here so
|
|
98
|
+
* subsequent sessions can `primeDiscoveryCache` and skip the
|
|
99
|
+
* enumeration spawn entirely (which is what fires the trust gate
|
|
100
|
+
* even on prompts that never use any MCP tool). */
|
|
101
|
+
onToolsDiscovered?: McpToolsDiscoveredCallback;
|
|
102
|
+
/** Resolve an opaque auth token by kind, for URL-based remote MCP
|
|
103
|
+
* servers (v1.7.333+). Today only `'bandit-api-key'` is asked for —
|
|
104
|
+
* the host should return the configured Bandit Cloud API key (env
|
|
105
|
+
* BANDIT_API_KEY → ~/.bandit/config.json `bandit.apiKey`). Returns
|
|
106
|
+
* undefined when no key is configured; the pool then connects to
|
|
107
|
+
* the server without an Authorization header and surfaces whatever
|
|
108
|
+
* 401/403 the server returns. Future kinds (`oauth-bandit`, etc.)
|
|
109
|
+
* slot in here without a breaking change. */
|
|
110
|
+
resolveAuthToken?: (kind: string) => string | undefined;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Pool managing the lifetime of every configured MCP server.
|
|
114
|
+
* Single instance per Bandit session (extension or CLI process).
|
|
115
|
+
*/
|
|
116
|
+
export declare class McpClientPool {
|
|
117
|
+
private readonly entries;
|
|
118
|
+
private readonly trustGate?;
|
|
119
|
+
private readonly trustedFingerprints;
|
|
120
|
+
private readonly onToolsDiscovered?;
|
|
121
|
+
private readonly resolveAuthToken?;
|
|
122
|
+
constructor(options?: McpClientPoolOptions);
|
|
123
|
+
/** Pre-populate the in-memory tool cache for a named server from a
|
|
124
|
+
* prior session's disk cache, keyed by config fingerprint. When the
|
|
125
|
+
* fingerprint matches the currently-registered server's config,
|
|
126
|
+
* `discoverTools(name)` short-circuits to these tools WITHOUT
|
|
127
|
+
* spawning — which is the whole point: no spawn means no trust-gate
|
|
128
|
+
* prompt on prompts that never use MCP. When the fingerprint doesn't
|
|
129
|
+
* match, the prime is silently dropped (config changed; we have to
|
|
130
|
+
* re-spawn to learn the new tool list). */
|
|
131
|
+
primeDiscoveryCache(name: string, fingerprint: string, tools: RemoteToolDef[]): void;
|
|
132
|
+
/** Mark a server fingerprint as trusted for this session — bypasses
|
|
133
|
+
* the gate on subsequent spawns. Hosts call this after the user
|
|
134
|
+
* approves "always allow" so re-prompting doesn't happen mid-session. */
|
|
135
|
+
trustFingerprint(fingerprint: string): void;
|
|
136
|
+
/** Register a server's config without spawning it. Idempotent — a
|
|
137
|
+
* second register for the same name updates the config and forces a
|
|
138
|
+
* reconnect on next ensureConnected. */
|
|
139
|
+
register(name: string, config: McpServerConfig): void;
|
|
140
|
+
/** Server names currently registered (not necessarily connected). */
|
|
141
|
+
list(): string[];
|
|
142
|
+
/** True when this server has a tool list cached in memory — either
|
|
143
|
+
* from a prior live `discoverTools` this session or from
|
|
144
|
+
* `primeDiscoveryCache` on boot. Callers use this to decide whether
|
|
145
|
+
* enumerating the server would require a spawn (and therefore fire
|
|
146
|
+
* the trust gate). Returns false for unknown or disabled servers. */
|
|
147
|
+
hasCachedTools(name: string): boolean;
|
|
148
|
+
/** Snapshot of every registered server for status views. */
|
|
149
|
+
snapshot(): McpServerSnapshot[];
|
|
150
|
+
/**
|
|
151
|
+
* Spawn + handshake the named server if it isn't already connected.
|
|
152
|
+
* Returns successfully when the server's `initialize` handshake has
|
|
153
|
+
* completed. Returns false when the server is disabled, missing, or
|
|
154
|
+
* a previous spawn failed and we don't retry on every call.
|
|
155
|
+
*/
|
|
156
|
+
ensureConnected(name: string): Promise<boolean>;
|
|
157
|
+
/**
|
|
158
|
+
* Force a reconnect for a previously errored or disconnected server.
|
|
159
|
+
* Used by the `/mcp connect <name>` slash command after the user
|
|
160
|
+
* fixes config / installs the server / sets the right env var.
|
|
161
|
+
*/
|
|
162
|
+
reconnect(name: string): Promise<boolean>;
|
|
163
|
+
/** Tools advertised by a connected server. Returns [] for unknown,
|
|
164
|
+
* disabled, errored, or never-connected servers.
|
|
165
|
+
*
|
|
166
|
+
* Short-circuits to the in-memory cache (populated either by a prior
|
|
167
|
+
* live listTools or by `primeDiscoveryCache`) without spawning the
|
|
168
|
+
* child process. The trust gate sits inside `spawnAndHandshake`, so
|
|
169
|
+
* bypassing the spawn here means the gate doesn't fire just because
|
|
170
|
+
* the host wanted to enumerate the registry — it now only fires
|
|
171
|
+
* when the agent actually invokes a tool via `callTool`. */
|
|
172
|
+
discoverTools(name: string): Promise<RemoteToolDef[]>;
|
|
173
|
+
/**
|
|
174
|
+
* Invoke a tool on a connected server. The pool ensures the server
|
|
175
|
+
* is up before the call. Returns the structured result; throws on
|
|
176
|
+
* RPC error so the caller's try/catch surfaces the failure to the
|
|
177
|
+
* agent's loop with a clear message instead of a silent empty
|
|
178
|
+
* result.
|
|
179
|
+
*/
|
|
180
|
+
callTool(name: string, toolName: string, args: Record<string, unknown>): Promise<{
|
|
181
|
+
content?: Array<{
|
|
182
|
+
type: string;
|
|
183
|
+
text?: string;
|
|
184
|
+
}>;
|
|
185
|
+
isError?: boolean;
|
|
186
|
+
}>;
|
|
187
|
+
/** Close every spawned process. Idempotent. */
|
|
188
|
+
dispose(): Promise<void>;
|
|
189
|
+
/** Close one server's process. Removes the entry. Idempotent. */
|
|
190
|
+
private disposeOne;
|
|
191
|
+
private spawnAndHandshake;
|
|
192
|
+
/**
|
|
193
|
+
* Build the HTTP headers for a remote MCP request based on the server's
|
|
194
|
+
* auth config. `bandit-api-key` resolves through the host-provided
|
|
195
|
+
* resolveAuthToken callback (env BANDIT_API_KEY → ~/.bandit/config.json
|
|
196
|
+
* `bandit.apiKey`). Returns an empty object when no auth is configured
|
|
197
|
+
* — the server will respond 401 if it needs auth, which surfaces as a
|
|
198
|
+
* normal MCP error the user can act on.
|
|
199
|
+
*/
|
|
200
|
+
private buildAuthHeaders;
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=clientPool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientPool.d.ts","sourceRoot":"","sources":["../../src/mcp/clientPool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAmB,MAAM,SAAS,CAAC;AAEnF;;;;;;;;;;;;;;GAcG;AACH;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAC9B;IACE,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,GACD;IACE,IAAI,EAAE,KAAK,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ;;+CAE2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;CAClB,KACA,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CAsBrF;AAkED;;;iDAGiD;AACjD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACrE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACH;AAkBD;;uEAEuE;AACvE,MAAM,MAAM,0BAA0B,GAAG,CACvC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,aAAa,EAAE,KACnB,IAAI,CAAC;AAEV,MAAM,WAAW,oBAAoB;IACnC;;;;yBAIqB;IACrB,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB;;;;wDAIoD;IACpD,iBAAiB,CAAC,EAAE,0BAA0B,CAAC;IAC/C;;;;;;;kDAO8C;IAC9C,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CACzD;AAED;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAe;IAC1C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAA6B;IAChE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAuC;gBAE7D,OAAO,GAAE,oBAAyB;IAM9C;;;;;;;gDAO4C;IAC5C,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,IAAI;IAWpF;;8EAE0E;IAC1E,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAI3C;;6CAEyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI;IAerD,qEAAqE;IACrE,IAAI,IAAI,MAAM,EAAE;IAIhB;;;;0EAIsE;IACtE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAKrC,4DAA4D;IAC5D,QAAQ,IAAI,iBAAiB,EAAE;IAQ/B;;;;;OAKG;IACG,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA8BrD;;;;OAIG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa/C;;;;;;;;iEAQ6D;IACvD,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IA8B3D;;;;;;OAMG;IACG,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QACrF,OAAO,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACjD,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;IAeF,+CAA+C;IACzC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAM9B,iEAAiE;IACjE,OAAO,CAAC,UAAU;YAQJ,iBAAiB;IAuF/B;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;CAmBzB"}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MCP client pool — manages spawn / handshake / lifecycle for N
|
|
4
|
+
* configured MCP servers. Each entry corresponds to one mcp-servers.json
|
|
5
|
+
* stanza. The pool is intentionally small surface:
|
|
6
|
+
*
|
|
7
|
+
* register(name, config) — store config (no spawn yet)
|
|
8
|
+
* ensureConnected(name) — spawn + handshake + cache. Idempotent.
|
|
9
|
+
* discoverTools(name) — list tools from a connected server (cached
|
|
10
|
+
* after first call until disconnect)
|
|
11
|
+
* callTool(name, tool, args) — proxy through to server.callTool()
|
|
12
|
+
* snapshot() — status view for /mcp and the IDE Connections tab
|
|
13
|
+
* dispose() — close every spawned process. Idempotent.
|
|
14
|
+
*
|
|
15
|
+
* Lazy spawn: nothing happens at register-time. The first ensureConnected
|
|
16
|
+
* triggers the actual child_process. Avoids paying spawn cost for servers
|
|
17
|
+
* the user configures but never invokes in this session.
|
|
18
|
+
*
|
|
19
|
+
* Failure isolation: a spawn/handshake error is recorded as the server's
|
|
20
|
+
* status and never thrown to the caller. discoverTools returns [] for
|
|
21
|
+
* failed servers. The agent loop continues with native tools only —
|
|
22
|
+
* one bad server doesn't kill the session.
|
|
23
|
+
*/
|
|
24
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
27
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
28
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
29
|
+
}
|
|
30
|
+
Object.defineProperty(o, k2, desc);
|
|
31
|
+
}) : (function(o, m, k, k2) {
|
|
32
|
+
if (k2 === undefined) k2 = k;
|
|
33
|
+
o[k2] = m[k];
|
|
34
|
+
}));
|
|
35
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
36
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
37
|
+
}) : function(o, v) {
|
|
38
|
+
o["default"] = v;
|
|
39
|
+
});
|
|
40
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
41
|
+
var ownKeys = function(o) {
|
|
42
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
43
|
+
var ar = [];
|
|
44
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
45
|
+
return ar;
|
|
46
|
+
};
|
|
47
|
+
return ownKeys(o);
|
|
48
|
+
};
|
|
49
|
+
return function (mod) {
|
|
50
|
+
if (mod && mod.__esModule) return mod;
|
|
51
|
+
var result = {};
|
|
52
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
53
|
+
__setModuleDefault(result, mod);
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
})();
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.McpClientPool = void 0;
|
|
59
|
+
exports.fingerprintServerConfig = fingerprintServerConfig;
|
|
60
|
+
const crypto = __importStar(require("crypto"));
|
|
61
|
+
/**
|
|
62
|
+
* Stable fingerprint of a server config. Used by hosts to remember
|
|
63
|
+
* "the user already approved this exact shape" across sessions.
|
|
64
|
+
* env VALUES are excluded — only env KEYS count — so rotating a token
|
|
65
|
+
* doesn't re-trigger the trust prompt.
|
|
66
|
+
*/
|
|
67
|
+
function fingerprintServerConfig(name, config) {
|
|
68
|
+
// URL-based remote servers fingerprint on (name, url, authKind). The
|
|
69
|
+
// token VALUE isn't mixed in so rotating a bearer or the Bandit API
|
|
70
|
+
// key doesn't re-trigger the trust prompt — same shape rule as the
|
|
71
|
+
// stdio envKeys-not-envValues policy.
|
|
72
|
+
if (config.url) {
|
|
73
|
+
const payload = {
|
|
74
|
+
name,
|
|
75
|
+
kind: 'url',
|
|
76
|
+
url: config.url,
|
|
77
|
+
authKind: describeAuth(config.auth)
|
|
78
|
+
};
|
|
79
|
+
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 16);
|
|
80
|
+
}
|
|
81
|
+
const payload = {
|
|
82
|
+
name,
|
|
83
|
+
kind: 'stdio',
|
|
84
|
+
command: config.command ?? '',
|
|
85
|
+
args: config.args ?? [],
|
|
86
|
+
envKeys: Object.keys(config.env ?? {}).sort()
|
|
87
|
+
};
|
|
88
|
+
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 16);
|
|
89
|
+
}
|
|
90
|
+
// SDK imports are deferred to spawn time (see loadMcpSdk below) so a
|
|
91
|
+
// dependency-resolution failure inside @modelcontextprotocol/sdk never
|
|
92
|
+
// runs at module-load time. This matters for VS Code extensions: the
|
|
93
|
+
// installed VSIX's node_modules can have broken transitive symlinks
|
|
94
|
+
// (pnpm's symlinked layout doesn't always survive packaging), and a
|
|
95
|
+
// failing require at module top would block extension activation
|
|
96
|
+
// entirely. Deferring means a misconfigured SDK only takes down MCP
|
|
97
|
+
// itself — the rest of the extension keeps working.
|
|
98
|
+
// Cached references to the SDK's classes after the first successful
|
|
99
|
+
// load. Reused on subsequent spawns so we don't pay the require cost
|
|
100
|
+
// repeatedly.
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
|
+
let cachedClient = null;
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
let cachedStdioTransport = null;
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
let cachedHttpTransport = null;
|
|
107
|
+
function loadMcpSdk() {
|
|
108
|
+
if (!cachedClient) {
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
110
|
+
const clientMod = require('@modelcontextprotocol/sdk/client/index.js');
|
|
111
|
+
cachedClient = clientMod.Client;
|
|
112
|
+
}
|
|
113
|
+
if (!cachedStdioTransport) {
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
115
|
+
const stdioMod = require('@modelcontextprotocol/sdk/client/stdio.js');
|
|
116
|
+
cachedStdioTransport = stdioMod.StdioClientTransport;
|
|
117
|
+
}
|
|
118
|
+
if (!cachedHttpTransport) {
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
120
|
+
const httpMod = require('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
121
|
+
cachedHttpTransport = httpMod.StreamableHTTPClientTransport;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
Client: cachedClient,
|
|
125
|
+
StdioClientTransport: cachedStdioTransport,
|
|
126
|
+
StreamableHTTPClientTransport: cachedHttpTransport
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Short label describing the auth strategy for trust-gate display +
|
|
131
|
+
* fingerprinting. Stable across a config's lifetime — env values aren't
|
|
132
|
+
* mixed in so rotating a bearer token doesn't re-trigger the trust
|
|
133
|
+
* prompt (mirrors the stdio fingerprint's "envKeys not envValues" rule).
|
|
134
|
+
*/
|
|
135
|
+
function describeAuth(auth) {
|
|
136
|
+
if (!auth)
|
|
137
|
+
return 'none';
|
|
138
|
+
if (auth === 'bandit')
|
|
139
|
+
return 'bandit-api-key';
|
|
140
|
+
if (auth.type === 'bandit-api-key')
|
|
141
|
+
return 'bandit-api-key';
|
|
142
|
+
if (auth.type === 'bearer')
|
|
143
|
+
return 'bearer';
|
|
144
|
+
if (auth.type === 'header')
|
|
145
|
+
return `header(${auth.name})`;
|
|
146
|
+
return 'unknown';
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Pool managing the lifetime of every configured MCP server.
|
|
150
|
+
* Single instance per Bandit session (extension or CLI process).
|
|
151
|
+
*/
|
|
152
|
+
class McpClientPool {
|
|
153
|
+
constructor(options = {}) {
|
|
154
|
+
this.entries = new Map();
|
|
155
|
+
this.trustedFingerprints = new Set();
|
|
156
|
+
this.trustGate = options.trustGate;
|
|
157
|
+
this.onToolsDiscovered = options.onToolsDiscovered;
|
|
158
|
+
this.resolveAuthToken = options.resolveAuthToken;
|
|
159
|
+
}
|
|
160
|
+
/** Pre-populate the in-memory tool cache for a named server from a
|
|
161
|
+
* prior session's disk cache, keyed by config fingerprint. When the
|
|
162
|
+
* fingerprint matches the currently-registered server's config,
|
|
163
|
+
* `discoverTools(name)` short-circuits to these tools WITHOUT
|
|
164
|
+
* spawning — which is the whole point: no spawn means no trust-gate
|
|
165
|
+
* prompt on prompts that never use MCP. When the fingerprint doesn't
|
|
166
|
+
* match, the prime is silently dropped (config changed; we have to
|
|
167
|
+
* re-spawn to learn the new tool list). */
|
|
168
|
+
primeDiscoveryCache(name, fingerprint, tools) {
|
|
169
|
+
const entry = this.entries.get(name);
|
|
170
|
+
if (!entry)
|
|
171
|
+
return;
|
|
172
|
+
const current = fingerprintServerConfig(name, entry.config);
|
|
173
|
+
if (current !== fingerprint)
|
|
174
|
+
return;
|
|
175
|
+
entry.cachedTools = tools;
|
|
176
|
+
// Status stays `idle` — we haven't actually spawned the server.
|
|
177
|
+
// The cache exists purely so the enumeration path can answer
|
|
178
|
+
// "what tools does this server expose?" without a child process.
|
|
179
|
+
}
|
|
180
|
+
/** Mark a server fingerprint as trusted for this session — bypasses
|
|
181
|
+
* the gate on subsequent spawns. Hosts call this after the user
|
|
182
|
+
* approves "always allow" so re-prompting doesn't happen mid-session. */
|
|
183
|
+
trustFingerprint(fingerprint) {
|
|
184
|
+
this.trustedFingerprints.add(fingerprint);
|
|
185
|
+
}
|
|
186
|
+
/** Register a server's config without spawning it. Idempotent — a
|
|
187
|
+
* second register for the same name updates the config and forces a
|
|
188
|
+
* reconnect on next ensureConnected. */
|
|
189
|
+
register(name, config) {
|
|
190
|
+
const existing = this.entries.get(name);
|
|
191
|
+
if (existing) {
|
|
192
|
+
// Config changed — close any open process and treat the entry
|
|
193
|
+
// as fresh. The caller is responsible for re-invoking
|
|
194
|
+
// ensureConnected after the config change if they need it
|
|
195
|
+
// immediately.
|
|
196
|
+
this.disposeOne(name);
|
|
197
|
+
}
|
|
198
|
+
const status = config.disabled
|
|
199
|
+
? { state: 'disabled' }
|
|
200
|
+
: { state: 'idle' };
|
|
201
|
+
this.entries.set(name, { config, status });
|
|
202
|
+
}
|
|
203
|
+
/** Server names currently registered (not necessarily connected). */
|
|
204
|
+
list() {
|
|
205
|
+
return [...this.entries.keys()];
|
|
206
|
+
}
|
|
207
|
+
/** True when this server has a tool list cached in memory — either
|
|
208
|
+
* from a prior live `discoverTools` this session or from
|
|
209
|
+
* `primeDiscoveryCache` on boot. Callers use this to decide whether
|
|
210
|
+
* enumerating the server would require a spawn (and therefore fire
|
|
211
|
+
* the trust gate). Returns false for unknown or disabled servers. */
|
|
212
|
+
hasCachedTools(name) {
|
|
213
|
+
const entry = this.entries.get(name);
|
|
214
|
+
return !!entry?.cachedTools && entry.cachedTools.length > 0;
|
|
215
|
+
}
|
|
216
|
+
/** Snapshot of every registered server for status views. */
|
|
217
|
+
snapshot() {
|
|
218
|
+
return [...this.entries.entries()].map(([name, entry]) => ({
|
|
219
|
+
name,
|
|
220
|
+
config: entry.config,
|
|
221
|
+
status: entry.status
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Spawn + handshake the named server if it isn't already connected.
|
|
226
|
+
* Returns successfully when the server's `initialize` handshake has
|
|
227
|
+
* completed. Returns false when the server is disabled, missing, or
|
|
228
|
+
* a previous spawn failed and we don't retry on every call.
|
|
229
|
+
*/
|
|
230
|
+
async ensureConnected(name) {
|
|
231
|
+
const entry = this.entries.get(name);
|
|
232
|
+
if (!entry)
|
|
233
|
+
return false;
|
|
234
|
+
if (entry.status.state === 'disabled')
|
|
235
|
+
return false;
|
|
236
|
+
if (entry.status.state === 'connected')
|
|
237
|
+
return true;
|
|
238
|
+
if (entry.pendingConnect) {
|
|
239
|
+
await entry.pendingConnect;
|
|
240
|
+
// Cast widens the narrowed type — entry.status mutates inside the
|
|
241
|
+
// pending promise's catch handler but TypeScript's narrowing
|
|
242
|
+
// doesn't follow that.
|
|
243
|
+
return entry.status.state === 'connected';
|
|
244
|
+
}
|
|
245
|
+
if (entry.status.state === 'error') {
|
|
246
|
+
// Don't auto-retry errored servers — the user has to explicitly
|
|
247
|
+
// reconnect after fixing whatever was wrong (token rotated,
|
|
248
|
+
// server not installed, etc). Saves us from a thundering-herd
|
|
249
|
+
// of failed spawns on every tool invocation.
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
entry.status = { state: 'connecting' };
|
|
253
|
+
entry.pendingConnect = this.spawnAndHandshake(name, entry).catch((err) => {
|
|
254
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
255
|
+
entry.status = { state: 'error', message };
|
|
256
|
+
}).finally(() => {
|
|
257
|
+
entry.pendingConnect = undefined;
|
|
258
|
+
});
|
|
259
|
+
await entry.pendingConnect;
|
|
260
|
+
return entry.status.state === 'connected';
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Force a reconnect for a previously errored or disconnected server.
|
|
264
|
+
* Used by the `/mcp connect <name>` slash command after the user
|
|
265
|
+
* fixes config / installs the server / sets the right env var.
|
|
266
|
+
*/
|
|
267
|
+
async reconnect(name) {
|
|
268
|
+
const entry = this.entries.get(name);
|
|
269
|
+
if (!entry)
|
|
270
|
+
return false;
|
|
271
|
+
if (entry.status.state === 'connected' || entry.status.state === 'connecting') {
|
|
272
|
+
this.disposeOne(name);
|
|
273
|
+
// Re-create the entry shell after dispose (which removed it).
|
|
274
|
+
this.register(name, entry.config);
|
|
275
|
+
}
|
|
276
|
+
else if (entry.status.state === 'error') {
|
|
277
|
+
entry.status = { state: 'idle' };
|
|
278
|
+
}
|
|
279
|
+
return this.ensureConnected(name);
|
|
280
|
+
}
|
|
281
|
+
/** Tools advertised by a connected server. Returns [] for unknown,
|
|
282
|
+
* disabled, errored, or never-connected servers.
|
|
283
|
+
*
|
|
284
|
+
* Short-circuits to the in-memory cache (populated either by a prior
|
|
285
|
+
* live listTools or by `primeDiscoveryCache`) without spawning the
|
|
286
|
+
* child process. The trust gate sits inside `spawnAndHandshake`, so
|
|
287
|
+
* bypassing the spawn here means the gate doesn't fire just because
|
|
288
|
+
* the host wanted to enumerate the registry — it now only fires
|
|
289
|
+
* when the agent actually invokes a tool via `callTool`. */
|
|
290
|
+
async discoverTools(name) {
|
|
291
|
+
const entry = this.entries.get(name);
|
|
292
|
+
if (!entry)
|
|
293
|
+
return [];
|
|
294
|
+
// Cache hit: hand back the cached list without spawning.
|
|
295
|
+
if (entry.cachedTools)
|
|
296
|
+
return entry.cachedTools;
|
|
297
|
+
const ok = await this.ensureConnected(name);
|
|
298
|
+
if (!ok)
|
|
299
|
+
return [];
|
|
300
|
+
try {
|
|
301
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
302
|
+
const result = await entry.client.listTools();
|
|
303
|
+
const tools = Array.isArray(result?.tools) ? result.tools : [];
|
|
304
|
+
entry.cachedTools = tools;
|
|
305
|
+
entry.status = { state: 'connected', toolCount: tools.length };
|
|
306
|
+
// Notify the host so it can persist this tool list — next session
|
|
307
|
+
// primes from disk and never spawns just to enumerate.
|
|
308
|
+
if (this.onToolsDiscovered) {
|
|
309
|
+
try {
|
|
310
|
+
this.onToolsDiscovered(name, fingerprintServerConfig(name, entry.config), tools);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Host cache write must never break the agent loop.
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return tools;
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
320
|
+
entry.status = { state: 'error', message };
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Invoke a tool on a connected server. The pool ensures the server
|
|
326
|
+
* is up before the call. Returns the structured result; throws on
|
|
327
|
+
* RPC error so the caller's try/catch surfaces the failure to the
|
|
328
|
+
* agent's loop with a clear message instead of a silent empty
|
|
329
|
+
* result.
|
|
330
|
+
*/
|
|
331
|
+
async callTool(name, toolName, args) {
|
|
332
|
+
const entry = this.entries.get(name);
|
|
333
|
+
if (!entry)
|
|
334
|
+
throw new Error(`MCP server "${name}" is not registered.`);
|
|
335
|
+
const ok = await this.ensureConnected(name);
|
|
336
|
+
if (!ok) {
|
|
337
|
+
const reason = entry.status.state === 'error'
|
|
338
|
+
? entry.status.message
|
|
339
|
+
: `state=${entry.status.state}`;
|
|
340
|
+
throw new Error(`MCP server "${name}" is not connected (${reason}).`);
|
|
341
|
+
}
|
|
342
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
343
|
+
const client = entry.client;
|
|
344
|
+
return await client.callTool({ name: toolName, arguments: args });
|
|
345
|
+
}
|
|
346
|
+
/** Close every spawned process. Idempotent. */
|
|
347
|
+
async dispose() {
|
|
348
|
+
for (const name of [...this.entries.keys()]) {
|
|
349
|
+
this.disposeOne(name);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/** Close one server's process. Removes the entry. Idempotent. */
|
|
353
|
+
disposeOne(name) {
|
|
354
|
+
const entry = this.entries.get(name);
|
|
355
|
+
if (!entry)
|
|
356
|
+
return;
|
|
357
|
+
try {
|
|
358
|
+
entry.transport?.close?.();
|
|
359
|
+
}
|
|
360
|
+
catch { /* ignore */ }
|
|
361
|
+
try {
|
|
362
|
+
entry.client?.close?.();
|
|
363
|
+
}
|
|
364
|
+
catch { /* ignore */ }
|
|
365
|
+
this.entries.delete(name);
|
|
366
|
+
}
|
|
367
|
+
async spawnAndHandshake(name, entry) {
|
|
368
|
+
// Lazy-load the SDK only when we actually need to spawn. Any
|
|
369
|
+
// dependency-resolution failure inside @modelcontextprotocol/sdk
|
|
370
|
+
// surfaces here as a normal error (caught by the pool's status-
|
|
371
|
+
// tracking wrapper) instead of crashing module load — a hard
|
|
372
|
+
// requirement for the VS Code extension whose host can't tolerate
|
|
373
|
+
// a throw at top-level require time.
|
|
374
|
+
const { Client, StdioClientTransport, StreamableHTTPClientTransport } = loadMcpSdk();
|
|
375
|
+
// URL-based remote server — Streamable HTTP transport. This is the
|
|
376
|
+
// path Bandit Cloud-hosted MCP servers (mcp.burtson.ai and friends)
|
|
377
|
+
// use; the SDK speaks the JSON-RPC-over-HTTP envelope and we just
|
|
378
|
+
// attach the right auth header so the server can identify the user.
|
|
379
|
+
if (entry.config.url) {
|
|
380
|
+
if (this.trustGate) {
|
|
381
|
+
const fingerprint = fingerprintServerConfig(name, entry.config);
|
|
382
|
+
if (!this.trustedFingerprints.has(fingerprint)) {
|
|
383
|
+
const allowed = await this.trustGate({
|
|
384
|
+
kind: 'url',
|
|
385
|
+
name,
|
|
386
|
+
url: entry.config.url,
|
|
387
|
+
authKind: describeAuth(entry.config.auth)
|
|
388
|
+
});
|
|
389
|
+
if (!allowed) {
|
|
390
|
+
throw new Error(`Trust denied: connecting to remote MCP "${entry.config.url}" requires user approval. Approve in the Connections panel (extension) or via /mcp trust ${name} (CLI).`);
|
|
391
|
+
}
|
|
392
|
+
this.trustedFingerprints.add(fingerprint);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const headers = this.buildAuthHeaders(entry.config.auth);
|
|
396
|
+
const transport = new StreamableHTTPClientTransport(new URL(entry.config.url), { requestInit: { headers } });
|
|
397
|
+
const client = new Client({ name: 'bandit', version: '1.0.0' }, { capabilities: {} });
|
|
398
|
+
await client.connect(transport);
|
|
399
|
+
entry.client = client;
|
|
400
|
+
entry.transport = transport;
|
|
401
|
+
entry.status = { state: 'connected', toolCount: 0 };
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
// Stdio path — original behavior, unchanged.
|
|
405
|
+
if (this.trustGate) {
|
|
406
|
+
const fingerprint = fingerprintServerConfig(name, entry.config);
|
|
407
|
+
if (!this.trustedFingerprints.has(fingerprint)) {
|
|
408
|
+
const allowed = await this.trustGate({
|
|
409
|
+
kind: 'stdio',
|
|
410
|
+
name,
|
|
411
|
+
command: entry.config.command ?? '',
|
|
412
|
+
args: entry.config.args ?? [],
|
|
413
|
+
envKeys: Object.keys(entry.config.env ?? {})
|
|
414
|
+
});
|
|
415
|
+
if (!allowed) {
|
|
416
|
+
throw new Error(`Trust denied: spawning "${entry.config.command} ${(entry.config.args ?? []).join(' ')}" requires user approval. Approve in the Connections panel (extension) or via /mcp trust ${name} (CLI).`);
|
|
417
|
+
}
|
|
418
|
+
this.trustedFingerprints.add(fingerprint);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (!entry.config.command) {
|
|
422
|
+
throw new Error(`MCP server "${name}" config is missing both \`command\` (stdio) and \`url\` (remote) — one of the two is required.`);
|
|
423
|
+
}
|
|
424
|
+
const transport = new StdioClientTransport({
|
|
425
|
+
command: entry.config.command,
|
|
426
|
+
args: entry.config.args ?? [],
|
|
427
|
+
env: entry.config.env ?? undefined,
|
|
428
|
+
// pipe stderr so a misbehaving server doesn't dump bytes into
|
|
429
|
+
// the user's terminal — surfaces only when we explicitly read it.
|
|
430
|
+
stderr: 'pipe'
|
|
431
|
+
});
|
|
432
|
+
const client = new Client({ name: 'bandit', version: '1.0.0' }, { capabilities: {} });
|
|
433
|
+
await client.connect(transport);
|
|
434
|
+
entry.client = client;
|
|
435
|
+
entry.transport = transport;
|
|
436
|
+
entry.status = { state: 'connected', toolCount: 0 };
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Build the HTTP headers for a remote MCP request based on the server's
|
|
440
|
+
* auth config. `bandit-api-key` resolves through the host-provided
|
|
441
|
+
* resolveAuthToken callback (env BANDIT_API_KEY → ~/.bandit/config.json
|
|
442
|
+
* `bandit.apiKey`). Returns an empty object when no auth is configured
|
|
443
|
+
* — the server will respond 401 if it needs auth, which surfaces as a
|
|
444
|
+
* normal MCP error the user can act on.
|
|
445
|
+
*/
|
|
446
|
+
buildAuthHeaders(auth) {
|
|
447
|
+
if (!auth)
|
|
448
|
+
return {};
|
|
449
|
+
const normalized = typeof auth === 'string' ? { type: 'bandit-api-key' } : auth;
|
|
450
|
+
if (normalized.type === 'bandit-api-key') {
|
|
451
|
+
const token = this.resolveAuthToken?.('bandit-api-key');
|
|
452
|
+
if (!token)
|
|
453
|
+
return {};
|
|
454
|
+
// mcp.burtson.ai accepts both `X-API-Key: <key>` and
|
|
455
|
+
// `Authorization: Bearer <jwt>`. We send X-API-Key because the
|
|
456
|
+
// Bandit Cloud key is a raw API key, not a JWT.
|
|
457
|
+
return { 'X-API-Key': token };
|
|
458
|
+
}
|
|
459
|
+
if (normalized.type === 'bearer') {
|
|
460
|
+
return { Authorization: `Bearer ${normalized.token}` };
|
|
461
|
+
}
|
|
462
|
+
if (normalized.type === 'header') {
|
|
463
|
+
return { [normalized.name]: normalized.value };
|
|
464
|
+
}
|
|
465
|
+
return {};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
exports.McpClientPool = McpClientPool;
|
|
469
|
+
//# sourceMappingURL=clientPool.js.map
|