@steerable/agent-shell 0.6.52 → 0.6.53

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.
@@ -16,6 +16,7 @@ import { LocalScriptRegistry } from '../local-script-registry.js';
16
16
  import { TerminalManager } from '../terminal-manager.js';
17
17
  import { ToolRouter } from '../tool-router.js';
18
18
  import { McpServerRegistry, } from '../mcp-server-registry.js';
19
+ import { mcpExecutor } from '../mcp-executor.js';
19
20
  import { ProjectRegistry } from '../project-registry.js';
20
21
  import { getProductConfig } from '../product-config.js';
21
22
  import { LocalBackendRouter } from '../local-backend/router.js';
@@ -264,6 +265,7 @@ export async function createHostRuntime(options) {
264
265
  for (const handle of [...packHandles.values()].reverse()) {
265
266
  await handle.dispose?.();
266
267
  }
268
+ await mcpExecutor.shutdownAll();
267
269
  await shutdownHostSidecar();
268
270
  await closeStorage();
269
271
  },
@@ -69,9 +69,14 @@ export interface PortableWebSearchSection {
69
69
  }
70
70
  export interface PortableMcpSection {
71
71
  name: string;
72
- command: string;
73
- args: string[];
72
+ transport: 'stdio' | 'streamable-http';
73
+ command?: string;
74
+ args?: string[];
74
75
  cwd?: string;
76
+ url?: string;
77
+ headers?: Record<string, string>;
78
+ headersFromEnv?: Record<string, string>;
79
+ bearerTokenEnvVar?: string;
75
80
  enabled: boolean;
76
81
  envIncluded: boolean;
77
82
  env?: Record<string, string>;
@@ -178,11 +183,20 @@ export declare function webSearchSectionFromSettings(settings: {
178
183
  }, includeSecrets: boolean): PortableWebSearchSection;
179
184
  export declare function mcpSectionFromServer(server: {
180
185
  name: string;
186
+ transport: 'stdio';
181
187
  command: string;
182
188
  args: string[];
183
189
  cwd?: string;
184
190
  enabled: boolean;
185
191
  env: Record<string, string>;
192
+ } | {
193
+ name: string;
194
+ transport: 'streamable-http';
195
+ url: string;
196
+ headers: Record<string, string>;
197
+ headersFromEnv: Record<string, string>;
198
+ bearerTokenEnvVar?: string;
199
+ enabled: boolean;
186
200
  }, includeSecrets: boolean): PortableMcpSection;
187
201
  /**
188
202
  * 把消息正文和 metadata 里的旧附件绝对路径换成新会话里的路径。
@@ -106,15 +106,27 @@ export function webSearchSectionFromSettings(settings, includeSecrets) {
106
106
  };
107
107
  }
108
108
  export function mcpSectionFromServer(server, includeSecrets) {
109
- return {
110
- name: server.name,
111
- command: server.command,
112
- args: [...server.args],
113
- cwd: server.cwd,
114
- enabled: server.enabled,
115
- envIncluded: includeSecrets,
116
- ...(includeSecrets ? { env: { ...server.env } } : {}),
117
- };
109
+ return server.transport === 'stdio'
110
+ ? {
111
+ name: server.name,
112
+ transport: 'stdio',
113
+ command: server.command,
114
+ args: [...server.args],
115
+ cwd: server.cwd,
116
+ enabled: server.enabled,
117
+ envIncluded: includeSecrets,
118
+ ...(includeSecrets ? { env: { ...server.env } } : {}),
119
+ }
120
+ : {
121
+ name: server.name,
122
+ transport: 'streamable-http',
123
+ url: server.url,
124
+ headers: { ...server.headers },
125
+ headersFromEnv: { ...server.headersFromEnv },
126
+ bearerTokenEnvVar: server.bearerTokenEnvVar,
127
+ enabled: server.enabled,
128
+ envIncluded: false,
129
+ };
118
130
  }
119
131
  /**
120
132
  * 把消息正文和 metadata 里的旧附件绝对路径换成新会话里的路径。
@@ -180,22 +180,36 @@ async function applyMcp(mcp, servers) {
180
180
  return 'MCP 注册表不可用';
181
181
  for (const server of servers) {
182
182
  const name = server.name?.trim();
183
- const command = server.command?.trim();
184
- if (!name || !command)
183
+ if (!name)
185
184
  continue;
186
- const input = {
187
- name,
188
- command,
189
- args: Array.isArray(server.args) ? server.args.map((arg) => String(arg)) : [],
190
- cwd: server.cwd,
191
- enabled: server.enabled !== false,
192
- ...(server.envIncluded && server.env ? { env: server.env } : {}),
193
- };
185
+ const input = server.transport === 'streamable-http'
186
+ ? {
187
+ name,
188
+ transport: 'streamable-http',
189
+ url: server.url?.trim() ?? '',
190
+ headers: server.headers ?? {},
191
+ headersFromEnv: server.headersFromEnv ?? {},
192
+ bearerTokenEnvVar: server.bearerTokenEnvVar,
193
+ enabled: server.enabled !== false,
194
+ }
195
+ : {
196
+ name,
197
+ transport: 'stdio',
198
+ command: server.command?.trim() ?? '',
199
+ args: Array.isArray(server.args) ? server.args.map((arg) => String(arg)) : [],
200
+ cwd: server.cwd,
201
+ enabled: server.enabled !== false,
202
+ ...(server.envIncluded && server.env ? { env: server.env } : {}),
203
+ };
204
+ if ((input.transport === 'stdio' && !input.command) ||
205
+ (input.transport === 'streamable-http' && !input.url)) {
206
+ continue;
207
+ }
194
208
  const existing = mcp.list().find((item) => item.name.toLowerCase() === name.toLowerCase());
195
209
  if (existing)
196
210
  mcp.update(existing.id, input);
197
211
  else
198
- mcp.create({ ...input, env: input.env ?? {} });
212
+ mcp.create(input);
199
213
  }
200
214
  return null;
201
215
  }
@@ -51,6 +51,14 @@ import { matchPackBackendRoute } from './pack-backend-routes.js';
51
51
  import { collectTurnFiles } from './turn-files.js';
52
52
  import { resolveMentionedPaths } from './mentioned-paths.js';
53
53
  import { getAuthProvider } from '../auth/index.js';
54
+ function stringRecord(value) {
55
+ if (!value || typeof value !== 'object' || Array.isArray(value))
56
+ return {};
57
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
58
+ key,
59
+ String(item),
60
+ ]));
61
+ }
54
62
  function parseSourceFolders(value) {
55
63
  if (!Array.isArray(value))
56
64
  return [];
@@ -1140,7 +1148,7 @@ export class LocalBackendRouter {
1140
1148
  const servers = registry.list().map((s) => {
1141
1149
  const cached = registry.getCachedTools(s.id);
1142
1150
  return {
1143
- ...s,
1151
+ ...registry.toPublicEntry(s),
1144
1152
  serverKey: registry.serverKey(s),
1145
1153
  toolCount: cached?.tools.length ?? 0,
1146
1154
  toolsPreview: cached?.tools.slice(0, 8).map((t) => t.name) ?? [],
@@ -1156,19 +1164,35 @@ export class LocalBackendRouter {
1156
1164
  return { status: 503, data: { error: 'MCP 注册表不可用' } };
1157
1165
  try {
1158
1166
  const payload = this.toRecord(request.body);
1159
- const entry = registry.create({
1160
- name: String(payload.name ?? ''),
1161
- command: String(payload.command ?? ''),
1162
- args: Array.isArray(payload.args) ? payload.args.map((a) => String(a)) : [],
1163
- env: payload.env && typeof payload.env === 'object' && !Array.isArray(payload.env)
1164
- ? Object.fromEntries(Object.entries(payload.env).map(([k, v]) => [k, String(v)]))
1165
- : {},
1166
- cwd: typeof payload.cwd === 'string' ? payload.cwd : undefined,
1167
- enabled: payload.enabled !== false,
1168
- });
1167
+ const entry = payload.transport === 'streamable-http'
1168
+ ? registry.create({
1169
+ name: String(payload.name ?? ''),
1170
+ transport: 'streamable-http',
1171
+ url: String(payload.url ?? ''),
1172
+ headers: stringRecord(payload.headers),
1173
+ headersFromEnv: stringRecord(payload.headersFromEnv),
1174
+ bearerTokenEnvVar: typeof payload.bearerTokenEnvVar === 'string'
1175
+ ? payload.bearerTokenEnvVar
1176
+ : undefined,
1177
+ reconnect: payload.reconnect &&
1178
+ typeof payload.reconnect === 'object' &&
1179
+ !Array.isArray(payload.reconnect)
1180
+ ? payload.reconnect
1181
+ : undefined,
1182
+ enabled: payload.enabled !== false,
1183
+ })
1184
+ : registry.create({
1185
+ name: String(payload.name ?? ''),
1186
+ transport: 'stdio',
1187
+ command: String(payload.command ?? ''),
1188
+ args: Array.isArray(payload.args) ? payload.args.map((a) => String(a)) : [],
1189
+ env: stringRecord(payload.env),
1190
+ cwd: typeof payload.cwd === 'string' ? payload.cwd : undefined,
1191
+ enabled: payload.enabled !== false,
1192
+ });
1169
1193
  // 后台拉一次工具列表,下一轮对话即可用;失败不阻塞创建
1170
1194
  void registry.refreshTools(entry.id).catch(() => { });
1171
- return { status: 200, data: { server: entry } };
1195
+ return { status: 200, data: { server: registry.toPublicEntry(entry) } };
1172
1196
  }
1173
1197
  catch (err) {
1174
1198
  return { status: 400, data: { error: err instanceof Error ? err.message : String(err) } };
@@ -1188,7 +1212,7 @@ export class LocalBackendRouter {
1188
1212
  return {
1189
1213
  status: 200,
1190
1214
  data: {
1191
- added: result.added,
1215
+ added: result.added.map((entry) => registry.toPublicEntry(entry)),
1192
1216
  skipped: result.skipped,
1193
1217
  },
1194
1218
  };
@@ -1211,17 +1235,32 @@ export class LocalBackendRouter {
1211
1235
  const payload = this.toRecord(request.body);
1212
1236
  const updated = registry.update(serverId, {
1213
1237
  name: typeof payload.name === 'string' ? payload.name : undefined,
1238
+ transport: payload.transport === 'stdio' || payload.transport === 'streamable-http'
1239
+ ? payload.transport
1240
+ : undefined,
1214
1241
  command: typeof payload.command === 'string' ? payload.command : undefined,
1215
1242
  args: Array.isArray(payload.args) ? payload.args.map((a) => String(a)) : undefined,
1216
1243
  env: payload.env && typeof payload.env === 'object' && !Array.isArray(payload.env)
1217
- ? Object.fromEntries(Object.entries(payload.env).map(([k, v]) => [k, String(v)]))
1244
+ ? stringRecord(payload.env)
1218
1245
  : undefined,
1219
1246
  cwd: typeof payload.cwd === 'string' ? payload.cwd : undefined,
1247
+ url: typeof payload.url === 'string' ? payload.url : undefined,
1248
+ headers: payload.headers && typeof payload.headers === 'object' && !Array.isArray(payload.headers)
1249
+ ? stringRecord(payload.headers)
1250
+ : undefined,
1251
+ headersFromEnv: payload.headersFromEnv &&
1252
+ typeof payload.headersFromEnv === 'object' &&
1253
+ !Array.isArray(payload.headersFromEnv)
1254
+ ? stringRecord(payload.headersFromEnv)
1255
+ : undefined,
1256
+ bearerTokenEnvVar: typeof payload.bearerTokenEnvVar === 'string'
1257
+ ? payload.bearerTokenEnvVar
1258
+ : undefined,
1220
1259
  enabled: typeof payload.enabled === 'boolean' ? payload.enabled : undefined,
1221
1260
  });
1222
1261
  if (updated.enabled)
1223
1262
  void registry.refreshTools(updated.id).catch(() => { });
1224
- return { status: 200, data: { server: updated } };
1263
+ return { status: 200, data: { server: registry.toPublicEntry(updated) } };
1225
1264
  }
1226
1265
  catch (err) {
1227
1266
  return { status: 400, data: { error: err instanceof Error ? err.message : String(err) } };
@@ -1,3 +1,4 @@
1
+ import { type ExecDecisionName, type ExecPolicy, type ShellEndpoint, type ShellProcessBackend } from './shell-exec.js';
1
2
  import { type ApplyEditsResult, type EditOp } from './local-edit.js';
2
3
  export declare function getWineCommand(): string | null;
3
4
  export declare function rewriteExeCommandIfNeeded(command: string): string;
@@ -8,6 +9,10 @@ export interface LocalExecRequest {
8
9
  timeout?: number;
9
10
  env?: Record<string, string>;
10
11
  shell?: ShellType;
12
+ /** 在伪终端里跑。缺省走管道,调用方要终端语义时显式打开。 */
13
+ pty?: boolean;
14
+ /** 用户已经批准一次 prompt 决定。不能放过 forbidden。 */
15
+ execApproval?: 'allow';
11
16
  }
12
17
  export interface LocalExecResult {
13
18
  success: boolean;
@@ -25,6 +30,14 @@ export interface LocalExecResult {
25
30
  * "程序已启动并在运行" 对启动类命令就是成功,不能让 LLM 当失败重试。
26
31
  */
27
32
  stillRunning?: boolean;
33
+ /** 前缀策略的决定。forbidden 不启动进程;prompt 在批准前也不启动。 */
34
+ execDecision?: ExecDecisionName;
35
+ /** prompt 时为 true,让模型把决定交给用户而不是改写命令重试。 */
36
+ needsFollowup?: boolean;
37
+ /** 这次命令跑在本机还是 SSH 对端。 */
38
+ transport?: 'local' | 'ssh';
39
+ /** 这次启动是否分配了伪终端。 */
40
+ pty?: boolean;
28
41
  }
29
42
  export interface LocalFileReadRequest {
30
43
  path: string;
@@ -208,7 +221,14 @@ export declare class LocalExecutor {
208
221
  * 重灌此集合(与框架一致:resume 只重建 version 证据,门退化为 CAS)。
209
222
  */
210
223
  private readonly partialReadPaths;
224
+ private execPolicy;
225
+ private shellEndpoint;
226
+ private shellBackend;
211
227
  constructor(maxOutputBytes?: number, applyEditsFn?: ApplyEditsFn);
228
+ setExecPolicy(policy: ExecPolicy): void;
229
+ setShellEndpoint(endpoint: ShellEndpoint): void;
230
+ /** 测试和嵌入方替换真正的进程启动。缺省用本机管道或 PTY。 */
231
+ setShellBackend(backend: ShellProcessBackend | null): void;
212
232
  updateSafetyConfig(config: CommandSafetyConfigPayload): void;
213
233
  /**
214
234
  * P2b:会话恢复时重灌 read-before-write 证据。sidecar 扫描持久记录里的
@@ -227,6 +247,8 @@ export declare class LocalExecutor {
227
247
  osArch: string;
228
248
  };
229
249
  executeShell(request: LocalExecRequest): Promise<LocalExecResult>;
250
+ private holdForExecPolicy;
251
+ private runPlannedShell;
230
252
  readLocalFile(request: LocalFileReadRequest, projectRoot?: string | null, additionalReadRoots?: string[] | null): Promise<LocalFileReadResult>;
231
253
  writeLocalFile(request: LocalFileWriteRequest, projectRoot?: string | null, additionalWriteRoots?: string[] | null): Promise<LocalFileWriteResult>;
232
254
  /**
@@ -7,6 +7,8 @@ import { spawn, execSync } from 'child_process';
7
7
  import log from 'electron-log';
8
8
  import { constants as fsConstants } from 'fs';
9
9
  import { adaptCommandForPowerShell } from './shell-adapt.js';
10
+ import { evaluateExecPolicy, planSshArgv, } from './shell-exec.js';
11
+ import { nodeShellBackend } from './shell-process.js';
10
12
  import { applyEdits, EditError } from './local-edit.js';
11
13
  // `he` ships as CommonJS and does not expose ESM named exports, so we have to
12
14
  // take the default import and destructure at runtime in this ESM module.
@@ -241,6 +243,16 @@ const BUILTIN_DANGEROUS = [
241
243
  { id: 'win_del_force', pattern: '\\bdel\\s+\\/f\\s+\\/s\\s+\\/q\\s+[a-z]:\\\\', platform: 'windows' },
242
244
  { id: 'win_rd_force', pattern: '\\brd\\s+\\/s\\s+\\/q\\s+[a-z]:\\\\', platform: 'windows' },
243
245
  ];
246
+ function stringEnv(extra) {
247
+ const env = {};
248
+ for (const [key, value] of Object.entries(process.env)) {
249
+ if (typeof value === 'string')
250
+ env[key] = value;
251
+ }
252
+ if (extra)
253
+ Object.assign(env, extra);
254
+ return env;
255
+ }
244
256
  function toWslPath(input) {
245
257
  const normalized = input.replace(/\\/g, '/');
246
258
  const winDriveMatch = normalized.match(/^([A-Za-z]):\/(.*)$/);
@@ -273,11 +285,24 @@ export class LocalExecutor {
273
285
  * 重灌此集合(与框架一致:resume 只重建 version 证据,门退化为 CAS)。
274
286
  */
275
287
  partialReadPaths = new Set();
288
+ execPolicy = { rules: [] };
289
+ shellEndpoint = { kind: 'local' };
290
+ shellBackend = null;
276
291
  constructor(maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES, applyEditsFn = applyEdits) {
277
292
  this.maxOutputBytes = maxOutputBytes;
278
293
  this.applyEditsFn = applyEditsFn;
279
294
  this.defaultShell = this.computeDefaultShell();
280
295
  }
296
+ setExecPolicy(policy) {
297
+ this.execPolicy = policy;
298
+ }
299
+ setShellEndpoint(endpoint) {
300
+ this.shellEndpoint = endpoint;
301
+ }
302
+ /** 测试和嵌入方替换真正的进程启动。缺省用本机管道或 PTY。 */
303
+ setShellBackend(backend) {
304
+ this.shellBackend = backend;
305
+ }
281
306
  updateSafetyConfig(config) {
282
307
  const disabled = new Set(config.disabledPatternIds);
283
308
  const isWin = process.platform === 'win32';
@@ -355,8 +380,12 @@ export class LocalExecutor {
355
380
  if (dangerousReason) {
356
381
  return { success: false, error: `Blocked dangerous command: ${dangerousReason}` };
357
382
  }
383
+ const held = this.holdForExecPolicy(command, request);
384
+ if (held)
385
+ return held;
386
+ const useSsh = this.shellEndpoint.kind === 'ssh';
358
387
  const resolvedShell = this.resolveShell(request.shell);
359
- if (resolvedShell.type === 'powershell') {
388
+ if (!useSsh && resolvedShell.type === 'powershell') {
360
389
  // LLM 经常混写 cmd / bash 方言(&&、||、%VAR%);Windows PowerShell 5.1
361
390
  // 不支持这些语法,执行前做确定性转换,避免整条命令直接报解析错误。
362
391
  const adapted = adaptCommandForPowerShell(command);
@@ -382,6 +411,17 @@ export class LocalExecutor {
382
411
  const timeout = Math.max(1000, rawTimeout);
383
412
  const cwd = this.resolveCwd(request.cwd, resolvedShell.type);
384
413
  const env = { ...process.env, ...(request.env || {}) };
414
+ if (this.shellBackend || useSsh || request.pty) {
415
+ return await this.runPlannedShell({
416
+ command,
417
+ request,
418
+ resolvedShell,
419
+ useSsh,
420
+ guiCommand,
421
+ timeout,
422
+ cwd,
423
+ });
424
+ }
385
425
  const stdoutChunks = [];
386
426
  const stderrChunks = [];
387
427
  let stdoutBytes = 0;
@@ -486,6 +526,83 @@ export class LocalExecutor {
486
526
  return { success: false, error: message, platform: process.platform };
487
527
  }
488
528
  }
529
+ holdForExecPolicy(command, request) {
530
+ const evaluation = evaluateExecPolicy(command, this.execPolicy);
531
+ if (evaluation.decision === 'allow')
532
+ return null;
533
+ if (evaluation.decision === 'prompt' && request.execApproval === 'allow')
534
+ return null;
535
+ const shown = evaluation.command.join(' ');
536
+ const why = evaluation.rule?.justification ? ` (${evaluation.rule.justification})` : '';
537
+ if (evaluation.decision === 'forbidden') {
538
+ return {
539
+ success: false,
540
+ error: `exec policy forbids: ${shown}${why}`,
541
+ execDecision: 'forbidden',
542
+ platform: process.platform,
543
+ };
544
+ }
545
+ return {
546
+ success: false,
547
+ error: `exec policy requires approval: ${shown}${why}`,
548
+ execDecision: 'prompt',
549
+ needsFollowup: true,
550
+ platform: process.platform,
551
+ };
552
+ }
553
+ async runPlannedShell(input) {
554
+ const pty = input.request.pty === true;
555
+ const spec = input.useSsh
556
+ ? planSshArgv({
557
+ command: input.command,
558
+ cwd: input.request.cwd ? input.cwd : undefined,
559
+ pty,
560
+ shell: input.resolvedShell.type === 'powershell' ? 'powershell' : undefined,
561
+ endpoint: this.shellEndpoint,
562
+ })
563
+ : {
564
+ file: input.resolvedShell.shell,
565
+ args: [...input.resolvedShell.args, input.command],
566
+ cwd: input.cwd,
567
+ env: stringEnv(input.request.env),
568
+ pty,
569
+ transport: 'local',
570
+ };
571
+ if (input.useSsh) {
572
+ spec.cwd = os.homedir();
573
+ spec.env = stringEnv();
574
+ }
575
+ const backend = this.shellBackend ?? nodeShellBackend;
576
+ const outcome = await backend.run(spec, {
577
+ timeoutMs: input.timeout,
578
+ maxOutputBytes: this.maxOutputBytes,
579
+ gui: input.guiCommand,
580
+ });
581
+ const launched = outcome.stillRunning === true;
582
+ return {
583
+ success: launched || (!outcome.error && outcome.exitCode === 0),
584
+ stdout: launched
585
+ ? `${outcome.stdout}\n[GUI 程序已启动,仍在运行(等待 ${input.timeout}ms 后未退出,进程未被终止)。请在打开的界面中继续操作;不要重新执行同一条启动命令。]`
586
+ : outcome.stdout,
587
+ stderr: outcome.stderr,
588
+ exitCode: outcome.exitCode,
589
+ error: launched
590
+ ? undefined
591
+ : outcome.timedOut
592
+ ? `Command timed out after ${input.timeout}ms (shell terminated; a launched GUI app may still be running). ` +
593
+ 'DO NOT blindly re-run the same launch command — if this launched a GUI/long-running program, ' +
594
+ 'first check whether it is already running, or re-run with a larger `timeout`, ' +
595
+ 'or start it detached (e.g. PowerShell Start-Process) so it does not block.'
596
+ : outcome.error,
597
+ truncated: outcome.truncated,
598
+ timedOut: outcome.timedOut,
599
+ stillRunning: outcome.stillRunning,
600
+ shell: input.resolvedShell.type,
601
+ platform: process.platform,
602
+ transport: spec.transport,
603
+ pty: spec.pty,
604
+ };
605
+ }
489
606
  async readLocalFile(request, projectRoot, additionalReadRoots) {
490
607
  try {
491
608
  const filePath = this.resolvePath(request.path);
@@ -1,13 +1,35 @@
1
+ import { type StreamableHTTPReconnectionOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
2
+ export declare const MAX_MCP_SERVER_TOOLS = 64;
1
3
  /**
2
4
  * 给超时类错误追加可操作的排查提示。独立成纯函数便于单测。
3
5
  */
4
6
  export declare function withMcpTroubleshootHint(message: string): string;
5
- export interface McpServerConfig {
7
+ export interface StdioMcpServerConfig {
8
+ transport: 'stdio';
6
9
  command: string;
7
10
  args?: string[];
8
11
  env?: Record<string, string>;
9
12
  cwd?: string;
10
13
  }
14
+ export interface StreamableHttpMcpServerConfig {
15
+ transport: 'streamable-http';
16
+ url: string;
17
+ /** Non-sensitive literal headers. Authorization must come from an env reference. */
18
+ headers?: Record<string, string>;
19
+ /** Header name -> environment variable name. */
20
+ headersFromEnv?: Record<string, string>;
21
+ bearerTokenEnvVar?: string;
22
+ reconnect?: Partial<StreamableHTTPReconnectionOptions>;
23
+ }
24
+ export type McpServerConfig = StdioMcpServerConfig | StreamableHttpMcpServerConfig;
25
+ export type LegacyMcpServerConfig = Omit<StdioMcpServerConfig, 'transport'>;
26
+ export interface ListedTool {
27
+ name: string;
28
+ description: string;
29
+ inputSchema?: unknown;
30
+ }
31
+ export declare function resolveHttpHeaders(config: StreamableHttpMcpServerConfig, env?: Record<string, string | undefined>): Record<string, string>;
32
+ export declare function mcpConfigKey(rawConfig: McpServerConfig | LegacyMcpServerConfig, env?: Record<string, string | undefined>): string;
11
33
  /**
12
34
  * Pure eviction predicate, split out so it's unit-testable without spinning
13
35
  * up a real MCP child process (`McpExecutor` isn't otherwise exported —
@@ -17,29 +39,39 @@ export declare function shouldEvictMcpConnection(entry: {
17
39
  lastUsed: number;
18
40
  inflight: number;
19
41
  }, now: number, idleTimeoutMs: number): boolean;
20
- declare class McpExecutor {
42
+ export interface McpExecutorLike {
43
+ executeTool(config: McpServerConfig | LegacyMcpServerConfig, toolName: string, toolArgs: Record<string, unknown>): Promise<{
44
+ success: boolean;
45
+ text?: string;
46
+ error?: string;
47
+ }>;
48
+ listTools(config: McpServerConfig | LegacyMcpServerConfig, onChanged?: (tools: ListedTool[]) => void, onUnavailable?: (error: string) => void): Promise<{
49
+ success: boolean;
50
+ tools?: ListedTool[];
51
+ error?: string;
52
+ }>;
53
+ }
54
+ export declare class McpExecutor implements McpExecutorLike {
21
55
  private pool;
22
56
  private cleanupTimer;
23
57
  constructor();
24
- executeTool(config: McpServerConfig, toolName: string, toolArgs: Record<string, unknown>): Promise<{
58
+ executeTool(rawConfig: McpServerConfig | LegacyMcpServerConfig, toolName: string, toolArgs: Record<string, unknown>): Promise<{
25
59
  success: boolean;
26
60
  text?: string;
27
61
  error?: string;
28
62
  }>;
29
- listTools(config: McpServerConfig): Promise<{
63
+ listTools(rawConfig: McpServerConfig | LegacyMcpServerConfig, onChanged?: (tools: ListedTool[]) => void, onUnavailable?: (error: string) => void): Promise<{
30
64
  success: boolean;
31
- tools?: {
32
- name: string;
33
- description: string;
34
- inputSchema?: unknown;
35
- }[];
65
+ tools?: ListedTool[];
36
66
  error?: string;
37
67
  }>;
38
68
  private getOrCreate;
69
+ private createTransport;
70
+ private refreshEntryTools;
71
+ private fetchEntryTools;
39
72
  private closeEntry;
40
73
  /** 关闭连接池中的所有连接(应用退出 / 测试收尾时调用)。 */
41
74
  shutdownAll(): Promise<void>;
42
75
  private evictIdle;
43
76
  }
44
77
  export declare const mcpExecutor: McpExecutor;
45
- export {};