twinclaw 1.0.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 (132) hide show
  1. package/README.md +66 -0
  2. package/bin/npm-twinclaw.js +17 -0
  3. package/bin/run-twinbot-cli.js +36 -0
  4. package/bin/twinbot.js +4 -0
  5. package/bin/twinclaw.js +4 -0
  6. package/dist/api/handlers/browser.js +160 -0
  7. package/dist/api/handlers/callback.js +80 -0
  8. package/dist/api/handlers/config-validate.js +19 -0
  9. package/dist/api/handlers/health.js +117 -0
  10. package/dist/api/handlers/local-state-backup.js +118 -0
  11. package/dist/api/handlers/persona-state.js +59 -0
  12. package/dist/api/handlers/skill-packages.js +94 -0
  13. package/dist/api/router.js +278 -0
  14. package/dist/api/runtime-event-producer.js +99 -0
  15. package/dist/api/shared.js +82 -0
  16. package/dist/api/websocket-hub.js +305 -0
  17. package/dist/config/config-loader.js +2 -0
  18. package/dist/config/env-schema.js +202 -0
  19. package/dist/config/env-validator.js +223 -0
  20. package/dist/config/identity-bootstrap.js +115 -0
  21. package/dist/config/json-config.js +344 -0
  22. package/dist/config/workspace.js +186 -0
  23. package/dist/core/channels-cli.js +77 -0
  24. package/dist/core/cli.js +119 -0
  25. package/dist/core/context-assembly.js +33 -0
  26. package/dist/core/doctor.js +365 -0
  27. package/dist/core/gateway-cli.js +323 -0
  28. package/dist/core/gateway.js +416 -0
  29. package/dist/core/heartbeat.js +54 -0
  30. package/dist/core/install-cli.js +320 -0
  31. package/dist/core/lane-executor.js +134 -0
  32. package/dist/core/logs-cli.js +70 -0
  33. package/dist/core/onboarding.js +760 -0
  34. package/dist/core/pairing-cli.js +78 -0
  35. package/dist/core/secret-vault-cli.js +204 -0
  36. package/dist/core/types.js +1 -0
  37. package/dist/index.js +404 -0
  38. package/dist/interfaces/dispatcher.js +214 -0
  39. package/dist/interfaces/telegram_handler.js +82 -0
  40. package/dist/interfaces/tui-dashboard.js +53 -0
  41. package/dist/interfaces/whatsapp_handler.js +94 -0
  42. package/dist/release/cli.js +97 -0
  43. package/dist/release/mvp-gate-cli.js +118 -0
  44. package/dist/release/twinbot-config-schema.js +162 -0
  45. package/dist/release/twinclaw-config-schema.js +162 -0
  46. package/dist/services/block-chunker.js +174 -0
  47. package/dist/services/browser-service.js +334 -0
  48. package/dist/services/context-lifecycle.js +314 -0
  49. package/dist/services/db.js +1055 -0
  50. package/dist/services/delivery-tracker.js +110 -0
  51. package/dist/services/dm-pairing.js +245 -0
  52. package/dist/services/embedding-service.js +125 -0
  53. package/dist/services/file-watcher.js +125 -0
  54. package/dist/services/inbound-debounce.js +92 -0
  55. package/dist/services/incident-manager.js +516 -0
  56. package/dist/services/job-scheduler.js +176 -0
  57. package/dist/services/local-state-backup.js +682 -0
  58. package/dist/services/mcp-client-adapter.js +291 -0
  59. package/dist/services/mcp-server-manager.js +143 -0
  60. package/dist/services/model-router.js +927 -0
  61. package/dist/services/mvp-gate.js +845 -0
  62. package/dist/services/orchestration-service.js +422 -0
  63. package/dist/services/persona-state.js +256 -0
  64. package/dist/services/policy-engine.js +92 -0
  65. package/dist/services/proactive-notifier.js +94 -0
  66. package/dist/services/queue-service.js +146 -0
  67. package/dist/services/release-pipeline.js +652 -0
  68. package/dist/services/runtime-budget-governor.js +415 -0
  69. package/dist/services/secret-vault.js +704 -0
  70. package/dist/services/semantic-memory.js +249 -0
  71. package/dist/services/skill-package-manager.js +806 -0
  72. package/dist/services/skill-registry.js +122 -0
  73. package/dist/services/streaming-output.js +75 -0
  74. package/dist/services/stt-service.js +39 -0
  75. package/dist/services/tts-service.js +44 -0
  76. package/dist/skills/builtin.js +250 -0
  77. package/dist/skills/shell.js +87 -0
  78. package/dist/skills/types.js +1 -0
  79. package/dist/types/api.js +1 -0
  80. package/dist/types/context-budget.js +1 -0
  81. package/dist/types/doctor.js +1 -0
  82. package/dist/types/file-watcher.js +1 -0
  83. package/dist/types/incident.js +1 -0
  84. package/dist/types/local-state-backup.js +1 -0
  85. package/dist/types/mcp.js +1 -0
  86. package/dist/types/messaging.js +1 -0
  87. package/dist/types/model-routing.js +1 -0
  88. package/dist/types/mvp-gate.js +2 -0
  89. package/dist/types/orchestration.js +1 -0
  90. package/dist/types/persona-state.js +22 -0
  91. package/dist/types/policy.js +1 -0
  92. package/dist/types/reasoning-graph.js +1 -0
  93. package/dist/types/release.js +1 -0
  94. package/dist/types/reliability.js +1 -0
  95. package/dist/types/runtime-budget.js +1 -0
  96. package/dist/types/scheduler.js +1 -0
  97. package/dist/types/secret-vault.js +1 -0
  98. package/dist/types/skill-packages.js +1 -0
  99. package/dist/types/websocket.js +14 -0
  100. package/dist/utils/logger.js +57 -0
  101. package/dist/utils/retry.js +61 -0
  102. package/dist/utils/secret-scan.js +208 -0
  103. package/mcp-servers.json +179 -0
  104. package/package.json +81 -0
  105. package/skill-packages.json +92 -0
  106. package/skill-packages.lock.json +5 -0
  107. package/src/skills/builtin.ts +275 -0
  108. package/src/skills/shell.ts +118 -0
  109. package/src/skills/types.ts +30 -0
  110. package/src/types/api.ts +252 -0
  111. package/src/types/blessed-contrib.d.ts +4 -0
  112. package/src/types/context-budget.ts +76 -0
  113. package/src/types/doctor.ts +29 -0
  114. package/src/types/file-watcher.ts +26 -0
  115. package/src/types/incident.ts +57 -0
  116. package/src/types/local-state-backup.ts +121 -0
  117. package/src/types/mcp.ts +106 -0
  118. package/src/types/messaging.ts +35 -0
  119. package/src/types/model-routing.ts +61 -0
  120. package/src/types/mvp-gate.ts +99 -0
  121. package/src/types/orchestration.ts +65 -0
  122. package/src/types/persona-state.ts +61 -0
  123. package/src/types/policy.ts +27 -0
  124. package/src/types/reasoning-graph.ts +58 -0
  125. package/src/types/release.ts +115 -0
  126. package/src/types/reliability.ts +43 -0
  127. package/src/types/runtime-budget.ts +85 -0
  128. package/src/types/scheduler.ts +47 -0
  129. package/src/types/secret-vault.ts +62 -0
  130. package/src/types/skill-packages.ts +81 -0
  131. package/src/types/sqlite-vec.d.ts +5 -0
  132. package/src/types/websocket.ts +122 -0
@@ -0,0 +1,291 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
+ import { logThought } from '../utils/logger.js';
4
+ import { saveMcpHealthEvent, saveMcpScopeAuditLog } from './db.js';
5
+ import { randomUUID } from 'node:crypto';
6
+ const FAILURE_THRESHOLD = 5;
7
+ const COOLDOWN_MS = 30_000; // 30 seconds
8
+ const SUCCESS_THRESHOLD = 2; // 2 consecutive successes to close half-open
9
+ // Symbols for testing access
10
+ export const INTERNAL_STATE = Symbol('INTERNAL_STATE');
11
+ export const INTERNAL_METRICS = Symbol('INTERNAL_METRICS');
12
+ export const INTERNAL_CLIENT = Symbol('INTERNAL_CLIENT');
13
+ export const INTERNAL_CONVERT = Symbol('INTERNAL_CONVERT');
14
+ /**
15
+ * Manages the lifecycle of a single MCP server connection.
16
+ *
17
+ * - Launches the server as a subprocess via stdio transport.
18
+ * - Discovers available tools using `list_tools()`.
19
+ * - Converts MCP tools into TwinBot's `Skill` contract.
20
+ * - Registers/unregisters tools in the shared `SkillRegistry`.
21
+ * - Provides `callTool()` for executing MCP tools.
22
+ */
23
+ export class McpClientAdapter {
24
+ #config;
25
+ #registry;
26
+ #client = null;
27
+ #transport = null;
28
+ #state = 'disconnected';
29
+ #lastError = null;
30
+ #toolCount = 0;
31
+ // Health tracking
32
+ #circuitState = 'closed';
33
+ #metrics = {
34
+ failureCount: 0,
35
+ latencySpikes: 0,
36
+ timeoutCount: 0,
37
+ lastFailureTime: null,
38
+ consecutiveSuccesses: 0,
39
+ };
40
+ /** @internal Exposure for tests only */
41
+ get [INTERNAL_STATE]() { return this.#circuitState; }
42
+ set [INTERNAL_STATE](v) { this.#circuitState = v; }
43
+ /** @internal Exposure for tests only */
44
+ get [INTERNAL_METRICS]() { return this.#metrics; }
45
+ /** @internal Exposure for tests only */
46
+ get [INTERNAL_CLIENT]() { return this.#client; }
47
+ /** @internal Exposure for tests only */
48
+ [INTERNAL_CONVERT](name, desc, schema) {
49
+ return this.#convertToSkill(name, desc, schema);
50
+ }
51
+ constructor(config, registry) {
52
+ this.#config = config;
53
+ this.#registry = registry;
54
+ }
55
+ get id() {
56
+ return this.#config.id;
57
+ }
58
+ get state() {
59
+ return this.#state;
60
+ }
61
+ /** Return a read-only snapshot of this adapter's status. */
62
+ snapshot() {
63
+ return {
64
+ id: this.#config.id,
65
+ name: this.#config.name,
66
+ state: this.#state,
67
+ toolCount: this.#toolCount,
68
+ lastError: this.#lastError,
69
+ health: this.#healthSnapshot(),
70
+ };
71
+ }
72
+ #healthSnapshot() {
73
+ let remainingCooldown = 0;
74
+ if (this.#circuitState === 'open' && this.#metrics.lastFailureTime) {
75
+ const lastFail = new Date(this.#metrics.lastFailureTime).getTime();
76
+ remainingCooldown = Math.max(0, COOLDOWN_MS - (Date.now() - lastFail));
77
+ }
78
+ return {
79
+ state: this.#circuitState,
80
+ metrics: { ...this.#metrics },
81
+ remainingCooldownMs: remainingCooldown,
82
+ };
83
+ }
84
+ /** Connect to the MCP server and discover its tools. */
85
+ async connect() {
86
+ if (this.#state === 'connected' || this.#state === 'connecting')
87
+ return;
88
+ this.#state = 'connecting';
89
+ this.#lastError = null;
90
+ try {
91
+ await logThought(`[McpClientAdapter] Connecting to MCP server '${this.#config.id}' (${this.#config.command} ${this.#config.args.join(' ')})`);
92
+ this.#transport = new StdioClientTransport({
93
+ command: this.#config.command,
94
+ args: this.#config.args,
95
+ env: this.#config.env,
96
+ });
97
+ this.#client = new Client({
98
+ name: `twinbot-${this.#config.id}`,
99
+ version: '1.0.0',
100
+ });
101
+ await this.#client.connect(this.#transport);
102
+ // Discover and register tools
103
+ await this.#discoverTools();
104
+ this.#state = 'connected';
105
+ await logThought(`[McpClientAdapter] Connected to '${this.#config.id}' — ${this.#toolCount} tools available.`);
106
+ }
107
+ catch (err) {
108
+ const message = err instanceof Error ? err.message : String(err);
109
+ this.#state = 'error';
110
+ this.#lastError = message;
111
+ console.error(`[McpClientAdapter] Failed to connect to '${this.#config.id}':`, message);
112
+ await logThought(`[McpClientAdapter] Connection to '${this.#config.id}' failed: ${message}`);
113
+ }
114
+ }
115
+ /** Disconnect from the MCP server and unregister its tools. */
116
+ async disconnect() {
117
+ // Unregister all tools from this server
118
+ const removed = this.#registry.unregisterByServer(this.#config.id);
119
+ this.#toolCount = 0;
120
+ try {
121
+ await this.#client?.close();
122
+ }
123
+ catch {
124
+ // Best-effort cleanup
125
+ }
126
+ this.#client = null;
127
+ this.#transport = null;
128
+ this.#state = 'disconnected';
129
+ await logThought(`[McpClientAdapter] Disconnected from '${this.#config.id}' — removed ${removed} tools.`);
130
+ }
131
+ /** Execute a tool call on the MCP server. */
132
+ async callTool(toolName, args) {
133
+ if (!this.#client || this.#state !== 'connected') {
134
+ throw new Error(`MCP server '${this.#config.id}' is not connected (state: ${this.#state}).`);
135
+ }
136
+ await logThought(`[McpClientAdapter] Calling tool '${toolName}' on MCP server '${this.#config.id}'.`);
137
+ // Check circuit state before execution
138
+ this.#checkCircuitTransition();
139
+ if (this.#circuitState === 'open') {
140
+ throw new Error(`MCP server '${this.#config.id}' is unavailable (Circuit OPEN). Failure count: ${this.#metrics.failureCount}`);
141
+ }
142
+ const start = Date.now();
143
+ try {
144
+ const result = await this.#client.callTool({ name: toolName, arguments: args });
145
+ const latency = Date.now() - start;
146
+ this.#recordSuccess(latency);
147
+ // MCP tool results come as an array of content blocks
148
+ const content = result.content;
149
+ if (Array.isArray(content)) {
150
+ return content
151
+ .map((block) => {
152
+ if (typeof block === 'object' && block !== null && 'text' in block) {
153
+ return String(block.text);
154
+ }
155
+ return JSON.stringify(block);
156
+ })
157
+ .join('\n');
158
+ }
159
+ const output = typeof content === 'string' ? content : JSON.stringify(content);
160
+ return output;
161
+ }
162
+ catch (err) {
163
+ this.#recordFailure(err);
164
+ throw err;
165
+ }
166
+ }
167
+ // ── Private Helpers ────────────────────────────────────────────────────────
168
+ /** Discover tools from the MCP server and register them in the skill registry. */
169
+ async #discoverTools() {
170
+ if (!this.#client)
171
+ return;
172
+ const response = await this.#client.listTools();
173
+ const tools = response.tools;
174
+ for (const tool of tools) {
175
+ const skill = this.#convertToSkill(tool.name, tool.description ?? '', tool.inputSchema);
176
+ this.#registry.register(skill);
177
+ }
178
+ this.#toolCount = tools.length;
179
+ }
180
+ /** Convert an MCP tool's metadata into TwinBot's Skill interface. */
181
+ #convertToSkill(name, description, inputSchema) {
182
+ const serverId = this.#config.id;
183
+ const adapter = this; // Capture reference for the closure
184
+ const defaultScope = this.#config.capabilities?.defaultScope ?? 'unclassified';
185
+ const mcpScope = this.#config.capabilities?.tools?.[name] ?? defaultScope;
186
+ return {
187
+ name,
188
+ description: `[MCP:${serverId}] ${description}`,
189
+ group: 'group:mcp',
190
+ parameters: inputSchema ?? undefined,
191
+ source: 'mcp',
192
+ serverId,
193
+ mcpScope,
194
+ adapter,
195
+ async execute(input) {
196
+ try {
197
+ const output = await adapter.callTool(name, input);
198
+ return { ok: true, output };
199
+ }
200
+ catch (err) {
201
+ const message = err instanceof Error ? err.message : String(err);
202
+ return { ok: false, output: `MCP tool error: ${message}` };
203
+ }
204
+ },
205
+ };
206
+ }
207
+ /** Audit a scope-blocked action. */
208
+ auditScopeBlock(sessionId, toolName, scope, reason) {
209
+ saveMcpScopeAuditLog({
210
+ id: randomUUID(),
211
+ sessionId,
212
+ serverId: this.#config.id,
213
+ toolName,
214
+ scope,
215
+ outcome: 'denied',
216
+ reason,
217
+ });
218
+ }
219
+ /** Audit a scope-allowed action. */
220
+ auditScopeAllow(sessionId, toolName, scope) {
221
+ saveMcpScopeAuditLog({
222
+ id: randomUUID(),
223
+ sessionId,
224
+ serverId: this.#config.id,
225
+ toolName,
226
+ scope,
227
+ outcome: 'allowed',
228
+ });
229
+ }
230
+ #checkCircuitTransition() {
231
+ if (this.#circuitState === 'open' && this.#metrics.lastFailureTime) {
232
+ const lastFail = new Date(this.#metrics.lastFailureTime).getTime();
233
+ if (Date.now() - lastFail > COOLDOWN_MS) {
234
+ const prevState = this.#circuitState;
235
+ this.#circuitState = 'half-open';
236
+ saveMcpHealthEvent({
237
+ id: randomUUID(),
238
+ serverId: this.#config.id,
239
+ prevState,
240
+ newState: this.#circuitState,
241
+ reason: 'Cooldown expired',
242
+ metrics: { ...this.#metrics },
243
+ });
244
+ logThought(`[McpClientAdapter] Circuit for '${this.#config.id}' transitioned to HALF-OPEN (cooldown expired).`).catch(() => { });
245
+ }
246
+ }
247
+ }
248
+ #recordSuccess(latencyMs) {
249
+ this.#metrics.consecutiveSuccesses++;
250
+ // Latency spikes (> 10s)
251
+ if (latencyMs > 10_000) {
252
+ this.#metrics.latencySpikes++;
253
+ }
254
+ if (this.#circuitState === 'half-open' && this.#metrics.consecutiveSuccesses >= SUCCESS_THRESHOLD) {
255
+ const prevState = this.#circuitState;
256
+ this.#circuitState = 'closed';
257
+ this.#metrics.failureCount = 0; // Reset failures on full recovery
258
+ saveMcpHealthEvent({
259
+ id: randomUUID(),
260
+ serverId: this.#config.id,
261
+ prevState,
262
+ newState: this.#circuitState,
263
+ reason: 'Success threshold reached in half-open state',
264
+ metrics: { ...this.#metrics },
265
+ });
266
+ logThought(`[McpClientAdapter] Circuit for '${this.#config.id}' transitioned to CLOSED (full recovery achieved).`).catch(() => { });
267
+ }
268
+ }
269
+ #recordFailure(err) {
270
+ this.#metrics.failureCount++;
271
+ this.#metrics.lastFailureTime = new Date().toISOString();
272
+ this.#metrics.consecutiveSuccesses = 0;
273
+ const message = err instanceof Error ? err.message : String(err);
274
+ if (message.toLowerCase().includes('timeout')) {
275
+ this.#metrics.timeoutCount++;
276
+ }
277
+ if (this.#circuitState !== 'open' && this.#metrics.failureCount >= FAILURE_THRESHOLD) {
278
+ const prevState = this.#circuitState;
279
+ this.#circuitState = 'open';
280
+ saveMcpHealthEvent({
281
+ id: randomUUID(),
282
+ serverId: this.#config.id,
283
+ prevState,
284
+ newState: this.#circuitState,
285
+ reason: `Failure threshold reached: ${message}`,
286
+ metrics: { ...this.#metrics },
287
+ });
288
+ logThought(`[McpClientAdapter] Circuit for '${this.#config.id}' transitioned to OPEN (failure threshold ${FAILURE_THRESHOLD} reached). Last error: ${message}`).catch(() => { });
289
+ }
290
+ }
291
+ }
@@ -0,0 +1,143 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { McpClientAdapter } from './mcp-client-adapter.js';
4
+ import { SkillPackageManager } from './skill-package-manager.js';
5
+ import { logThought } from '../utils/logger.js';
6
+ const DEFAULT_CONFIG_PATH = path.resolve('mcp-servers.json');
7
+ /**
8
+ * Manages multiple MCP server connections.
9
+ *
10
+ * Reads a configuration file listing MCP servers, creates an adapter
11
+ * for each enabled server, and orchestrates connect/disconnect lifecycle.
12
+ * Failed connections are isolated — one server's failure doesn't affect others.
13
+ *
14
+ * Usage:
15
+ * ```ts
16
+ * const manager = new McpServerManager(registry);
17
+ * await manager.loadConfig();
18
+ * await manager.connectAll();
19
+ *
20
+ * // Later:
21
+ * await manager.disconnectAll();
22
+ * ```
23
+ */
24
+ export class McpServerManager {
25
+ #registry;
26
+ #adapters = new Map();
27
+ #packageManager;
28
+ #configPath;
29
+ constructor(registry, configPath, packageManager) {
30
+ this.#registry = registry;
31
+ this.#configPath = configPath ?? DEFAULT_CONFIG_PATH;
32
+ this.#packageManager = packageManager ?? new SkillPackageManager();
33
+ }
34
+ /** Load MCP server configuration from the config file. */
35
+ async loadConfig() {
36
+ const enabledServers = [];
37
+ try {
38
+ const raw = await readFile(this.#configPath, 'utf8');
39
+ const config = JSON.parse(raw);
40
+ if (!Array.isArray(config.servers)) {
41
+ console.warn('[McpServerManager] Config file has no servers array.');
42
+ }
43
+ else {
44
+ enabledServers.push(...config.servers.filter((s) => s.enabled !== false));
45
+ }
46
+ }
47
+ catch (err) {
48
+ const message = err instanceof Error ? err.message : String(err);
49
+ if (err.code === 'ENOENT') {
50
+ console.warn(`[McpServerManager] Config file not found at ${this.#configPath}. No MCP servers will be loaded.`);
51
+ }
52
+ else {
53
+ console.error('[McpServerManager] Failed to load config:', message);
54
+ await logThought(`[McpServerManager] Config load failed: ${message}`);
55
+ }
56
+ }
57
+ const packagePlan = await this.#packageManager.getActivationPlan();
58
+ for (const packagedServer of packagePlan.servers) {
59
+ const duplicate = enabledServers.some((server) => server.id === packagedServer.id);
60
+ if (duplicate) {
61
+ console.warn(`[McpServerManager] Packaged server '${packagedServer.id}' conflicts with static config and was skipped.`);
62
+ continue;
63
+ }
64
+ enabledServers.push(packagedServer);
65
+ }
66
+ for (const serverConfig of enabledServers) {
67
+ if (this.#adapters.has(serverConfig.id)) {
68
+ console.warn(`[McpServerManager] Duplicate server ID '${serverConfig.id}' — skipping.`);
69
+ continue;
70
+ }
71
+ const adapter = new McpClientAdapter(serverConfig, this.#registry);
72
+ this.#adapters.set(serverConfig.id, adapter);
73
+ }
74
+ if (packagePlan.diagnostics.violations.length > 0) {
75
+ const summary = packagePlan.diagnostics.violations
76
+ .map((violation) => `${violation.packageName}@${violation.version}:${violation.code}`)
77
+ .join(', ');
78
+ console.warn(`[McpServerManager] Skill package activation blocked: ${summary}`);
79
+ }
80
+ await logThought(`[McpServerManager] Loaded ${enabledServers.length} server(s) from config/package lock.`);
81
+ return enabledServers;
82
+ }
83
+ async getSkillPackageDiagnostics() {
84
+ return this.#packageManager.getDiagnostics();
85
+ }
86
+ async installSkillPackage(packageName, versionRange) {
87
+ return this.#packageManager.installPackage(packageName, versionRange);
88
+ }
89
+ async upgradeSkillPackage(packageName, versionRange) {
90
+ return this.#packageManager.upgradePackage(packageName, versionRange);
91
+ }
92
+ async uninstallSkillPackage(packageName) {
93
+ return this.#packageManager.uninstallPackage(packageName);
94
+ }
95
+ /** Connect all configured servers that have autoConnect enabled. */
96
+ async connectAll() {
97
+ const promises = [];
98
+ for (const adapter of this.#adapters.values()) {
99
+ // Connect with error isolation — one failure doesn't block others
100
+ promises.push(adapter.connect().catch((err) => {
101
+ const message = err instanceof Error ? err.message : String(err);
102
+ console.error(`[McpServerManager] Failed to connect '${adapter.id}':`, message);
103
+ }));
104
+ }
105
+ await Promise.allSettled(promises);
106
+ const snapshots = this.listServers();
107
+ const connected = snapshots.filter((s) => s.state === 'connected').length;
108
+ const total = snapshots.length;
109
+ await logThought(`[McpServerManager] Connection complete: ${connected}/${total} servers connected.`);
110
+ }
111
+ /** Connect a specific server by ID. */
112
+ async connect(serverId) {
113
+ const adapter = this.#adapters.get(serverId);
114
+ if (!adapter) {
115
+ throw new Error(`[McpServerManager] Server '${serverId}' is not configured.`);
116
+ }
117
+ await adapter.connect();
118
+ }
119
+ /** Disconnect a specific server by ID. */
120
+ async disconnect(serverId) {
121
+ const adapter = this.#adapters.get(serverId);
122
+ if (!adapter)
123
+ return;
124
+ await adapter.disconnect();
125
+ }
126
+ /** Disconnect all servers gracefully. */
127
+ async disconnectAll() {
128
+ const promises = [];
129
+ for (const adapter of this.#adapters.values()) {
130
+ promises.push(adapter.disconnect());
131
+ }
132
+ await Promise.allSettled(promises);
133
+ await logThought('[McpServerManager] All MCP servers disconnected.');
134
+ }
135
+ /** Return snapshots of all managed MCP servers. */
136
+ listServers() {
137
+ return [...this.#adapters.values()].map((a) => a.snapshot());
138
+ }
139
+ /** Get a specific adapter by ID (for direct tool calls). */
140
+ getAdapter(serverId) {
141
+ return this.#adapters.get(serverId);
142
+ }
143
+ }