@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.
- package/dist/host/runtime.js +2 -0
- package/dist/local-backend/portable-bundle.d.ts +16 -2
- package/dist/local-backend/portable-bundle.js +21 -9
- package/dist/local-backend/portable-service.js +25 -11
- package/dist/local-backend/router.js +54 -15
- package/dist/local-executor.d.ts +22 -0
- package/dist/local-executor.js +118 -1
- package/dist/mcp-executor.d.ts +42 -10
- package/dist/mcp-executor.js +248 -41
- package/dist/mcp-server-registry.d.ts +56 -8
- package/dist/mcp-server-registry.js +191 -26
- package/dist/product-config.d.ts +14 -0
- package/dist/shell-exec.d.ts +62 -0
- package/dist/shell-exec.js +147 -0
- package/dist/shell-process.d.ts +3 -0
- package/dist/shell-process.js +137 -0
- package/dist/sidecar/boot.js +4 -0
- package/dist/tool-router.js +21 -6
- package/package.json +5 -5
package/dist/mcp-executor.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { execFileSync } from 'child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { createRequire } from 'module';
|
|
3
4
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
5
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
6
|
+
import { StreamableHTTPClientTransport, StreamableHTTPError, } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
7
|
+
import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
8
|
// In ESM there is no `__filename` global; use the module URL to seed a local
|
|
6
9
|
// CommonJS require() so we can `require.resolve()` MCP server packages that
|
|
7
10
|
// ship as CJS (npx -y <pkg> targets, e.g. apple-notes-mcp).
|
|
@@ -14,6 +17,14 @@ const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
|
14
17
|
* timed out"(2026-07-31 用户实测遇到)。放宽到 3 分钟覆盖冷启动下载。
|
|
15
18
|
*/
|
|
16
19
|
const MCP_REQUEST_TIMEOUT_MS = 3 * 60 * 1000;
|
|
20
|
+
export const MAX_MCP_SERVER_TOOLS = 64;
|
|
21
|
+
const CONNECT_RETRY_DELAYS_MS = [0, 250, 1_000];
|
|
22
|
+
const DEFAULT_RECONNECTION = {
|
|
23
|
+
initialReconnectionDelay: 500,
|
|
24
|
+
maxReconnectionDelay: 30_000,
|
|
25
|
+
reconnectionDelayGrowFactor: 2,
|
|
26
|
+
maxRetries: 10,
|
|
27
|
+
};
|
|
17
28
|
/**
|
|
18
29
|
* 给超时类错误追加可操作的排查提示。独立成纯函数便于单测。
|
|
19
30
|
*/
|
|
@@ -54,6 +65,7 @@ function tryResolveBundled(config) {
|
|
|
54
65
|
try {
|
|
55
66
|
const entryPath = localRequire.resolve(pkgName);
|
|
56
67
|
return {
|
|
68
|
+
transport: 'stdio',
|
|
57
69
|
command: process.execPath,
|
|
58
70
|
args: [entryPath, ...extraArgs],
|
|
59
71
|
env: { ...config.env },
|
|
@@ -64,8 +76,43 @@ function tryResolveBundled(config) {
|
|
|
64
76
|
return null;
|
|
65
77
|
}
|
|
66
78
|
}
|
|
67
|
-
function
|
|
68
|
-
|
|
79
|
+
export function resolveHttpHeaders(config, env = process.env) {
|
|
80
|
+
const headers = { ...(config.headers ?? {}) };
|
|
81
|
+
for (const [header, variable] of Object.entries(config.headersFromEnv ?? {})) {
|
|
82
|
+
const value = env[variable];
|
|
83
|
+
if (!value)
|
|
84
|
+
throw new Error(`MCP header environment variable "${variable}" is not set`);
|
|
85
|
+
headers[header] = value;
|
|
86
|
+
}
|
|
87
|
+
if (config.bearerTokenEnvVar) {
|
|
88
|
+
const token = env[config.bearerTokenEnvVar];
|
|
89
|
+
if (!token) {
|
|
90
|
+
throw new Error(`MCP bearer token environment variable "${config.bearerTokenEnvVar}" is not set`);
|
|
91
|
+
}
|
|
92
|
+
headers.Authorization = `Bearer ${token}`;
|
|
93
|
+
}
|
|
94
|
+
return headers;
|
|
95
|
+
}
|
|
96
|
+
export function mcpConfigKey(rawConfig, env = process.env) {
|
|
97
|
+
const config = normalizeExecutorConfig(rawConfig);
|
|
98
|
+
const material = config.transport === 'stdio'
|
|
99
|
+
? {
|
|
100
|
+
transport: config.transport,
|
|
101
|
+
command: config.command,
|
|
102
|
+
args: config.args ?? [],
|
|
103
|
+
env: config.env ?? {},
|
|
104
|
+
cwd: config.cwd ?? '',
|
|
105
|
+
}
|
|
106
|
+
: {
|
|
107
|
+
transport: config.transport,
|
|
108
|
+
url: config.url,
|
|
109
|
+
headers: resolveHttpHeaders(config, env),
|
|
110
|
+
reconnect: config.reconnect ?? {},
|
|
111
|
+
};
|
|
112
|
+
return createHash('sha256').update(JSON.stringify(material)).digest('hex');
|
|
113
|
+
}
|
|
114
|
+
function normalizeExecutorConfig(config) {
|
|
115
|
+
return 'transport' in config ? config : { ...config, transport: 'stdio' };
|
|
69
116
|
}
|
|
70
117
|
/**
|
|
71
118
|
* Pure eviction predicate, split out so it's unit-testable without spinning
|
|
@@ -77,6 +124,83 @@ export function shouldEvictMcpConnection(entry, now, idleTimeoutMs) {
|
|
|
77
124
|
return false;
|
|
78
125
|
return now - entry.lastUsed > idleTimeoutMs;
|
|
79
126
|
}
|
|
127
|
+
function startupError(config, message) {
|
|
128
|
+
const prefix = config.transport === 'stdio' ? 'MCP server startup failed' : 'MCP HTTP connection failed';
|
|
129
|
+
return config.transport === 'stdio'
|
|
130
|
+
? withMcpTroubleshootHint(`${prefix}: ${message}`)
|
|
131
|
+
: `${prefix}: ${message}`;
|
|
132
|
+
}
|
|
133
|
+
function parseHttpUrl(raw) {
|
|
134
|
+
let url;
|
|
135
|
+
try {
|
|
136
|
+
url = new URL(raw);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new Error(`invalid MCP HTTP URL: ${raw}`);
|
|
140
|
+
}
|
|
141
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
142
|
+
throw new Error('MCP HTTP URL must use http or https');
|
|
143
|
+
}
|
|
144
|
+
if (url.username || url.password) {
|
|
145
|
+
throw new Error('MCP HTTP URL must not contain credentials');
|
|
146
|
+
}
|
|
147
|
+
return url;
|
|
148
|
+
}
|
|
149
|
+
function sameOriginFetch(origin) {
|
|
150
|
+
return async (input, init) => {
|
|
151
|
+
let current = new URL(String(input), origin);
|
|
152
|
+
for (let redirects = 0; redirects <= 5; redirects += 1) {
|
|
153
|
+
const response = await fetch(current, { ...init, redirect: 'manual' });
|
|
154
|
+
if (response.status < 300 || response.status >= 400)
|
|
155
|
+
return response;
|
|
156
|
+
const location = response.headers.get('location');
|
|
157
|
+
if (!location)
|
|
158
|
+
return response;
|
|
159
|
+
const next = new URL(location, current);
|
|
160
|
+
if (next.origin !== origin.origin) {
|
|
161
|
+
throw new Error(`MCP HTTP redirect crossed origin: ${origin.origin} -> ${next.origin}`);
|
|
162
|
+
}
|
|
163
|
+
current = next;
|
|
164
|
+
}
|
|
165
|
+
throw new Error('MCP HTTP redirect limit exceeded');
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async function connectWithRetry(create) {
|
|
169
|
+
let lastError;
|
|
170
|
+
for (const delayMs of CONNECT_RETRY_DELAYS_MS) {
|
|
171
|
+
if (delayMs > 0)
|
|
172
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
173
|
+
const generation = create();
|
|
174
|
+
try {
|
|
175
|
+
await generation.client.connect(generation.transport, {
|
|
176
|
+
timeout: MCP_REQUEST_TIMEOUT_MS,
|
|
177
|
+
});
|
|
178
|
+
return generation;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
lastError = error;
|
|
182
|
+
try {
|
|
183
|
+
await generation.transport.close();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Failed negotiation can leave an already-closed transport.
|
|
187
|
+
}
|
|
188
|
+
if (!isRetryableConnectError(error))
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
throw lastError;
|
|
193
|
+
}
|
|
194
|
+
function isRetryableConnectError(error) {
|
|
195
|
+
if (error instanceof StreamableHTTPError) {
|
|
196
|
+
return error.code !== undefined && [408, 429, 500, 502, 503, 504].includes(error.code);
|
|
197
|
+
}
|
|
198
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
199
|
+
if (/401|403|unauthorized|forbidden|invalid|protocol|parse|deserialize/i.test(message)) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return /408|429|500|502|503|504|fetch failed|ECONN|ENOTFOUND|ETIMEDOUT|closed/i.test(message);
|
|
203
|
+
}
|
|
80
204
|
function coerceArgs(args, schema) {
|
|
81
205
|
if (!schema?.inputSchema?.properties)
|
|
82
206
|
return args;
|
|
@@ -100,20 +224,24 @@ function coerceArgs(args, schema) {
|
|
|
100
224
|
}
|
|
101
225
|
return out;
|
|
102
226
|
}
|
|
103
|
-
class McpExecutor {
|
|
227
|
+
export class McpExecutor {
|
|
104
228
|
pool = new Map();
|
|
105
229
|
cleanupTimer = null;
|
|
106
230
|
constructor() {
|
|
107
231
|
this.cleanupTimer = setInterval(() => this.evictIdle(), 60_000);
|
|
108
232
|
}
|
|
109
|
-
async executeTool(
|
|
233
|
+
async executeTool(rawConfig, toolName, toolArgs) {
|
|
234
|
+
const config = normalizeExecutorConfig(rawConfig);
|
|
110
235
|
let entry;
|
|
111
236
|
try {
|
|
112
237
|
entry = await this.getOrCreate(config);
|
|
113
238
|
}
|
|
114
239
|
catch (err) {
|
|
115
240
|
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
error: startupError(config, msg),
|
|
244
|
+
};
|
|
117
245
|
}
|
|
118
246
|
entry.inflight += 1;
|
|
119
247
|
try {
|
|
@@ -136,7 +264,7 @@ class McpExecutor {
|
|
|
136
264
|
}
|
|
137
265
|
catch (err) {
|
|
138
266
|
const msg = err instanceof Error ? err.message : String(err);
|
|
139
|
-
const key =
|
|
267
|
+
const key = mcpConfigKey(config);
|
|
140
268
|
await this.closeEntry(key);
|
|
141
269
|
return { success: false, error: withMcpTroubleshootHint(`Tool execution failed: ${msg}`) };
|
|
142
270
|
}
|
|
@@ -145,26 +273,31 @@ class McpExecutor {
|
|
|
145
273
|
entry.lastUsed = Date.now();
|
|
146
274
|
}
|
|
147
275
|
}
|
|
148
|
-
async listTools(
|
|
276
|
+
async listTools(rawConfig, onChanged, onUnavailable) {
|
|
277
|
+
const config = normalizeExecutorConfig(rawConfig);
|
|
149
278
|
let entry;
|
|
150
279
|
try {
|
|
151
280
|
entry = await this.getOrCreate(config);
|
|
152
281
|
}
|
|
153
282
|
catch (err) {
|
|
154
283
|
const msg = err instanceof Error ? err.message : String(err);
|
|
155
|
-
return { success: false, error:
|
|
284
|
+
return { success: false, error: startupError(config, msg) };
|
|
156
285
|
}
|
|
157
286
|
entry.inflight += 1;
|
|
287
|
+
if (onChanged)
|
|
288
|
+
entry.listeners.add(onChanged);
|
|
289
|
+
if (onUnavailable)
|
|
290
|
+
entry.unavailableListeners.add(onUnavailable);
|
|
158
291
|
try {
|
|
159
|
-
const
|
|
292
|
+
const tools = await this.refreshEntryTools(entry);
|
|
160
293
|
return {
|
|
161
294
|
success: true,
|
|
162
|
-
tools
|
|
295
|
+
tools,
|
|
163
296
|
};
|
|
164
297
|
}
|
|
165
298
|
catch (err) {
|
|
166
299
|
const msg = err instanceof Error ? err.message : String(err);
|
|
167
|
-
const key =
|
|
300
|
+
const key = mcpConfigKey(config);
|
|
168
301
|
await this.closeEntry(key);
|
|
169
302
|
return { success: false, error: withMcpTroubleshootHint(`List tools failed: ${msg}`) };
|
|
170
303
|
}
|
|
@@ -174,54 +307,128 @@ class McpExecutor {
|
|
|
174
307
|
}
|
|
175
308
|
}
|
|
176
309
|
async getOrCreate(config) {
|
|
177
|
-
const key =
|
|
310
|
+
const key = mcpConfigKey(config);
|
|
178
311
|
const existing = this.pool.get(key);
|
|
179
312
|
if (existing)
|
|
180
313
|
return existing;
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
314
|
+
const { client, transport } = await connectWithRetry(() => ({
|
|
315
|
+
transport: this.createTransport(config),
|
|
316
|
+
client: new Client({ name: 'agent-shell', version: '1.0.0' }, { capabilities: {} }),
|
|
317
|
+
}));
|
|
318
|
+
const entry = {
|
|
319
|
+
client,
|
|
320
|
+
transport,
|
|
321
|
+
httpTransport: transport instanceof StreamableHTTPClientTransport ? transport : undefined,
|
|
322
|
+
lastUsed: Date.now(),
|
|
323
|
+
key,
|
|
324
|
+
toolSchemas: new Map(),
|
|
325
|
+
inflight: 0,
|
|
326
|
+
listeners: new Set(),
|
|
327
|
+
unavailableListeners: new Set(),
|
|
328
|
+
syncChain: Promise.resolve(),
|
|
329
|
+
};
|
|
330
|
+
await this.refreshEntryTools(entry);
|
|
331
|
+
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
|
|
332
|
+
try {
|
|
333
|
+
const tools = await this.refreshEntryTools(entry);
|
|
334
|
+
for (const listener of entry.listeners)
|
|
335
|
+
listener(tools);
|
|
203
336
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
const entry = { client, transport, lastUsed: Date.now(), key, toolSchemas, inflight: 0 };
|
|
337
|
+
catch {
|
|
338
|
+
// Preserve the previous generation; a later notification/manual refresh can recover.
|
|
339
|
+
}
|
|
340
|
+
});
|
|
209
341
|
this.pool.set(key, entry);
|
|
210
|
-
|
|
342
|
+
client.onclose = () => {
|
|
343
|
+
if (this.pool.get(key) !== entry)
|
|
344
|
+
return;
|
|
211
345
|
this.pool.delete(key);
|
|
346
|
+
for (const listener of entry.unavailableListeners) {
|
|
347
|
+
listener('MCP connection closed after reconnect attempts were exhausted');
|
|
348
|
+
}
|
|
212
349
|
};
|
|
213
350
|
return entry;
|
|
214
351
|
}
|
|
352
|
+
createTransport(config) {
|
|
353
|
+
if (config.transport === 'stdio') {
|
|
354
|
+
const resolved = tryResolveBundled(config) ?? config;
|
|
355
|
+
if (!commandExists(resolved.command)) {
|
|
356
|
+
const hint = COMMAND_HINTS[config.command] || `请确保 ${config.command} 已安装并在 PATH 中`;
|
|
357
|
+
throw new Error(`命令 "${config.command}" 不可用。${hint}`);
|
|
358
|
+
}
|
|
359
|
+
return new StdioClientTransport({
|
|
360
|
+
command: resolved.command,
|
|
361
|
+
args: resolved.args,
|
|
362
|
+
env: { ...process.env, ...(resolved.env ?? {}) },
|
|
363
|
+
cwd: resolved.cwd ?? undefined,
|
|
364
|
+
stderr: 'pipe',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
const url = parseHttpUrl(config.url);
|
|
368
|
+
const headers = resolveHttpHeaders(config);
|
|
369
|
+
return new StreamableHTTPClientTransport(url, {
|
|
370
|
+
requestInit: { headers },
|
|
371
|
+
fetch: sameOriginFetch(url),
|
|
372
|
+
reconnectionOptions: {
|
|
373
|
+
...DEFAULT_RECONNECTION,
|
|
374
|
+
...(config.reconnect ?? {}),
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async refreshEntryTools(entry) {
|
|
379
|
+
let result;
|
|
380
|
+
const run = entry.syncChain.then(async () => {
|
|
381
|
+
result = await this.fetchEntryTools(entry);
|
|
382
|
+
});
|
|
383
|
+
entry.syncChain = run.catch(() => { });
|
|
384
|
+
await run;
|
|
385
|
+
return result ?? [];
|
|
386
|
+
}
|
|
387
|
+
async fetchEntryTools(entry) {
|
|
388
|
+
const tools = [];
|
|
389
|
+
let cursor;
|
|
390
|
+
do {
|
|
391
|
+
const listing = await entry.client.listTools(cursor ? { cursor } : undefined, { timeout: MCP_REQUEST_TIMEOUT_MS });
|
|
392
|
+
for (const tool of listing.tools) {
|
|
393
|
+
tools.push({
|
|
394
|
+
name: tool.name,
|
|
395
|
+
description: tool.description ?? '',
|
|
396
|
+
inputSchema: tool.inputSchema,
|
|
397
|
+
});
|
|
398
|
+
if (tools.length > MAX_MCP_SERVER_TOOLS) {
|
|
399
|
+
throw new Error(`MCP server advertises more than the per-server cap of ${MAX_MCP_SERVER_TOOLS} tools`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
cursor = listing.nextCursor;
|
|
403
|
+
} while (cursor);
|
|
404
|
+
const nextSchemas = new Map();
|
|
405
|
+
for (const tool of tools) {
|
|
406
|
+
nextSchemas.set(tool.name, {
|
|
407
|
+
name: tool.name,
|
|
408
|
+
inputSchema: tool.inputSchema,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
entry.toolSchemas = nextSchemas;
|
|
412
|
+
return tools;
|
|
413
|
+
}
|
|
215
414
|
async closeEntry(key) {
|
|
216
415
|
const entry = this.pool.get(key);
|
|
217
416
|
if (!entry)
|
|
218
417
|
return;
|
|
219
418
|
this.pool.delete(key);
|
|
419
|
+
try {
|
|
420
|
+
if (entry.httpTransport?.sessionId) {
|
|
421
|
+
await entry.httpTransport.terminateSession();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
// Session DELETE is advisory; transport closure still must happen.
|
|
426
|
+
}
|
|
220
427
|
try {
|
|
221
428
|
await entry.client.close();
|
|
222
429
|
}
|
|
223
430
|
catch {
|
|
224
|
-
//
|
|
431
|
+
// An already-closed transport needs no further cleanup.
|
|
225
432
|
}
|
|
226
433
|
}
|
|
227
434
|
/** 关闭连接池中的所有连接(应用退出 / 测试收尾时调用)。 */
|
|
@@ -8,25 +8,70 @@
|
|
|
8
8
|
* 存储通过 {@link McpServerKvStore} 接口注入:main.ts 用 electron-store
|
|
9
9
|
* 实现,单测用内存实现——本模块不 import electron,保持纯 Node 可测。
|
|
10
10
|
*/
|
|
11
|
-
import { type McpServerConfig } from './mcp-executor.js';
|
|
12
|
-
|
|
11
|
+
import { type McpExecutorLike, type McpServerConfig } from './mcp-executor.js';
|
|
12
|
+
interface McpServerCommon {
|
|
13
13
|
id: string;
|
|
14
14
|
/** 用户可见名称,也用于生成工具前缀(如 "filesystem")。 */
|
|
15
15
|
name: string;
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
}
|
|
20
|
+
export interface StdioMcpServerEntry extends McpServerCommon {
|
|
21
|
+
transport: 'stdio';
|
|
16
22
|
command: string;
|
|
17
23
|
args: string[];
|
|
18
24
|
env: Record<string, string>;
|
|
19
25
|
cwd?: string;
|
|
20
|
-
enabled: boolean;
|
|
21
|
-
createdAt: string;
|
|
22
|
-
updatedAt: string;
|
|
23
26
|
}
|
|
24
|
-
export interface
|
|
27
|
+
export interface StreamableHttpMcpServerEntry extends McpServerCommon {
|
|
28
|
+
transport: 'streamable-http';
|
|
29
|
+
url: string;
|
|
30
|
+
headers: Record<string, string>;
|
|
31
|
+
headersFromEnv: Record<string, string>;
|
|
32
|
+
bearerTokenEnvVar?: string;
|
|
33
|
+
reconnect?: {
|
|
34
|
+
initialReconnectionDelay?: number;
|
|
35
|
+
maxReconnectionDelay?: number;
|
|
36
|
+
reconnectionDelayGrowFactor?: number;
|
|
37
|
+
maxRetries?: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export type McpServerEntry = StdioMcpServerEntry | StreamableHttpMcpServerEntry;
|
|
41
|
+
interface CreateMcpServerCommon {
|
|
25
42
|
name: string;
|
|
43
|
+
enabled?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface CreateStdioMcpServerInput extends CreateMcpServerCommon {
|
|
46
|
+
/** Omitted remains compatible with existing callers and stored product configs. */
|
|
47
|
+
transport?: 'stdio';
|
|
26
48
|
command: string;
|
|
27
49
|
args?: string[];
|
|
28
50
|
env?: Record<string, string>;
|
|
29
51
|
cwd?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface CreateStreamableHttpMcpServerInput extends CreateMcpServerCommon {
|
|
54
|
+
transport: 'streamable-http';
|
|
55
|
+
url: string;
|
|
56
|
+
headers?: Record<string, string>;
|
|
57
|
+
headersFromEnv?: Record<string, string>;
|
|
58
|
+
bearerTokenEnvVar?: string;
|
|
59
|
+
reconnect?: StreamableHttpMcpServerEntry['reconnect'];
|
|
60
|
+
command?: never;
|
|
61
|
+
}
|
|
62
|
+
export type CreateMcpServerInput = CreateStdioMcpServerInput | CreateStreamableHttpMcpServerInput;
|
|
63
|
+
export interface UpdateMcpServerInput {
|
|
64
|
+
name?: string;
|
|
65
|
+
transport?: 'stdio' | 'streamable-http';
|
|
66
|
+
command?: string;
|
|
67
|
+
args?: string[];
|
|
68
|
+
env?: Record<string, string>;
|
|
69
|
+
cwd?: string;
|
|
70
|
+
url?: string;
|
|
71
|
+
headers?: Record<string, string>;
|
|
72
|
+
headersFromEnv?: Record<string, string>;
|
|
73
|
+
bearerTokenEnvVar?: string;
|
|
74
|
+
reconnect?: StreamableHttpMcpServerEntry['reconnect'];
|
|
30
75
|
enabled?: boolean;
|
|
31
76
|
}
|
|
32
77
|
/** 最小 KV 存储接口,避免本模块直接依赖 electron-store。 */
|
|
@@ -65,15 +110,16 @@ export interface McpToolEntry {
|
|
|
65
110
|
export declare function sanitizeServerKey(name: string, id: string): string;
|
|
66
111
|
export declare class McpServerRegistry {
|
|
67
112
|
private readonly store;
|
|
113
|
+
private readonly executor;
|
|
68
114
|
/** serverId → 最近一次 listTools 的结果(内存缓存,重启后由后台刷新重建)。 */
|
|
69
115
|
private readonly toolCache;
|
|
70
|
-
constructor(store: McpServerKvStore);
|
|
116
|
+
constructor(store: McpServerKvStore, executor?: McpExecutorLike);
|
|
71
117
|
list(): McpServerEntry[];
|
|
72
118
|
get(idOrName: string): McpServerEntry | null;
|
|
73
119
|
/** 产品预置 MCP:缺省不引入;已有同名服务不覆盖。 */
|
|
74
120
|
seedProductServers(specs: CreateMcpServerInput[] | undefined): void;
|
|
75
121
|
create(input: CreateMcpServerInput): McpServerEntry;
|
|
76
|
-
update(id: string, updates:
|
|
122
|
+
update(id: string, updates: UpdateMcpServerInput): McpServerEntry;
|
|
77
123
|
delete(id: string): boolean;
|
|
78
124
|
/**
|
|
79
125
|
* 解析 Claude Desktop 风格的配置并批量导入:
|
|
@@ -88,6 +134,7 @@ export declare class McpServerRegistry {
|
|
|
88
134
|
/** 工具名前缀(同一名称多实例撞名时追加 id 后缀保证唯一)。 */
|
|
89
135
|
serverKey(entry: McpServerEntry): string;
|
|
90
136
|
toExecutorConfig(entry: McpServerEntry): McpServerConfig;
|
|
137
|
+
toPublicEntry(entry: McpServerEntry): Record<string, unknown>;
|
|
91
138
|
getCachedTools(serverId: string): CachedToolList | null;
|
|
92
139
|
/**
|
|
93
140
|
* 所有「已启用 + 工具缓存非空」服务的工具扁平清单("/" 选择器数据源)。
|
|
@@ -104,3 +151,4 @@ export declare class McpServerRegistry {
|
|
|
104
151
|
/** 后台刷新所有已启用服务的工具列表(启动时调用,不阻塞、不抛错)。 */
|
|
105
152
|
refreshAllEnabled(): Promise<void>;
|
|
106
153
|
}
|
|
154
|
+
export {};
|