@defai.digital/ax-cli 3.7.1 → 3.8.1

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 (213) hide show
  1. package/README.md +143 -56
  2. package/dist/agent/context-manager.d.ts +15 -1
  3. package/dist/agent/context-manager.js +50 -19
  4. package/dist/agent/context-manager.js.map +1 -1
  5. package/dist/agent/dependency-resolver.js +13 -7
  6. package/dist/agent/dependency-resolver.js.map +1 -1
  7. package/dist/agent/llm-agent.d.ts +35 -0
  8. package/dist/agent/llm-agent.js +137 -4
  9. package/dist/agent/llm-agent.js.map +1 -1
  10. package/dist/agent/status-reporter.d.ts +114 -0
  11. package/dist/agent/status-reporter.js +335 -0
  12. package/dist/agent/status-reporter.js.map +1 -0
  13. package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js +8 -2
  14. package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js.map +1 -1
  15. package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js +3 -1
  16. package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js.map +1 -1
  17. package/dist/analyzers/best-practices/rules/typescript/prefer-const.js +3 -1
  18. package/dist/analyzers/best-practices/rules/typescript/prefer-const.js.map +1 -1
  19. package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js +3 -1
  20. package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js.map +1 -1
  21. package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js +9 -3
  22. package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js.map +1 -1
  23. package/dist/analyzers/git/churn-calculator.d.ts +1 -0
  24. package/dist/analyzers/git/churn-calculator.js +25 -6
  25. package/dist/analyzers/git/churn-calculator.js.map +1 -1
  26. package/dist/analyzers/git/hotspot-detector.js +2 -2
  27. package/dist/analyzers/git/hotspot-detector.js.map +1 -1
  28. package/dist/analyzers/metrics/metrics-analyzer.js +1 -1
  29. package/dist/analyzers/metrics/metrics-analyzer.js.map +1 -1
  30. package/dist/analyzers/security/security-analyzer.js +1 -1
  31. package/dist/analyzers/security/security-analyzer.js.map +1 -1
  32. package/dist/checkpoint/manager.d.ts +1 -0
  33. package/dist/checkpoint/manager.js +49 -9
  34. package/dist/checkpoint/manager.js.map +1 -1
  35. package/dist/checkpoint/storage.js +2 -2
  36. package/dist/checkpoint/storage.js.map +1 -1
  37. package/dist/commands/mcp-migrate.d.ts +9 -0
  38. package/dist/commands/mcp-migrate.js +172 -0
  39. package/dist/commands/mcp-migrate.js.map +1 -0
  40. package/dist/commands/status.d.ts +7 -0
  41. package/dist/commands/status.js +211 -0
  42. package/dist/commands/status.js.map +1 -0
  43. package/dist/commands/vscode.d.ts +7 -0
  44. package/dist/commands/vscode.js +363 -0
  45. package/dist/commands/vscode.js.map +1 -0
  46. package/dist/index.js +79 -30
  47. package/dist/index.js.map +1 -1
  48. package/dist/llm/client.js +22 -4
  49. package/dist/llm/client.js.map +1 -1
  50. package/dist/mcp/automatosx-loader.d.ts +84 -0
  51. package/dist/mcp/automatosx-loader.js +238 -0
  52. package/dist/mcp/automatosx-loader.js.map +1 -0
  53. package/dist/mcp/client-mutex-patch.d.ts +36 -0
  54. package/dist/mcp/client-mutex-patch.js +75 -0
  55. package/dist/mcp/client-mutex-patch.js.map +1 -0
  56. package/dist/mcp/client-v2.d.ts +229 -0
  57. package/dist/mcp/client-v2.js +740 -0
  58. package/dist/mcp/client-v2.js.map +1 -0
  59. package/dist/mcp/client.d.ts +111 -13
  60. package/dist/mcp/client.js +168 -253
  61. package/dist/mcp/client.js.map +1 -1
  62. package/dist/mcp/config-detector-v2.d.ts +83 -0
  63. package/dist/mcp/config-detector-v2.js +328 -0
  64. package/dist/mcp/config-detector-v2.js.map +1 -0
  65. package/dist/mcp/config-detector.d.ts +90 -0
  66. package/dist/mcp/config-detector.js +242 -0
  67. package/dist/mcp/config-detector.js.map +1 -0
  68. package/dist/mcp/config-migrator-v2.d.ts +89 -0
  69. package/dist/mcp/config-migrator-v2.js +288 -0
  70. package/dist/mcp/config-migrator-v2.js.map +1 -0
  71. package/dist/mcp/config-migrator.d.ts +63 -0
  72. package/dist/mcp/config-migrator.js +269 -0
  73. package/dist/mcp/config-migrator.js.map +1 -0
  74. package/dist/mcp/config-v2.d.ts +106 -0
  75. package/dist/mcp/config-v2.js +417 -0
  76. package/dist/mcp/config-v2.js.map +1 -0
  77. package/dist/mcp/config.d.ts +12 -1
  78. package/dist/mcp/config.js +95 -10
  79. package/dist/mcp/config.js.map +1 -1
  80. package/dist/mcp/error-formatter.d.ts +46 -0
  81. package/dist/mcp/error-formatter.js +244 -0
  82. package/dist/mcp/error-formatter.js.map +1 -0
  83. package/dist/mcp/health.d.ts +5 -0
  84. package/dist/mcp/health.js +22 -2
  85. package/dist/mcp/health.js.map +1 -1
  86. package/dist/mcp/invariants.d.ts +141 -0
  87. package/dist/mcp/invariants.js +243 -0
  88. package/dist/mcp/invariants.js.map +1 -0
  89. package/dist/mcp/mutex-safe.d.ts +153 -0
  90. package/dist/mcp/mutex-safe.js +260 -0
  91. package/dist/mcp/mutex-safe.js.map +1 -0
  92. package/dist/mcp/mutex.d.ts +73 -0
  93. package/dist/mcp/mutex.js +130 -0
  94. package/dist/mcp/mutex.js.map +1 -0
  95. package/dist/mcp/reconnection.d.ts +4 -0
  96. package/dist/mcp/reconnection.js +15 -0
  97. package/dist/mcp/reconnection.js.map +1 -1
  98. package/dist/mcp/transports-v2.d.ts +152 -0
  99. package/dist/mcp/transports-v2.js +481 -0
  100. package/dist/mcp/transports-v2.js.map +1 -0
  101. package/dist/mcp/type-safety.d.ts +231 -0
  102. package/dist/mcp/type-safety.js +273 -0
  103. package/dist/mcp/type-safety.js.map +1 -0
  104. package/dist/planner/task-planner.js +13 -0
  105. package/dist/planner/task-planner.js.map +1 -1
  106. package/dist/planner/types.d.ts +6 -6
  107. package/dist/schemas/confirmation-schemas.d.ts +2 -2
  108. package/dist/schemas/settings-schemas.d.ts +196 -0
  109. package/dist/schemas/settings-schemas.js +146 -5
  110. package/dist/schemas/settings-schemas.js.map +1 -1
  111. package/dist/sdk/index.d.ts +118 -2
  112. package/dist/sdk/index.js +146 -4
  113. package/dist/sdk/index.js.map +1 -1
  114. package/dist/sdk/testing.d.ts +182 -0
  115. package/dist/sdk/testing.js +231 -0
  116. package/dist/sdk/testing.js.map +1 -1
  117. package/dist/sdk/version.d.ts +114 -15
  118. package/dist/sdk/version.js +137 -15
  119. package/dist/sdk/version.js.map +1 -1
  120. package/dist/tools/bash.js +54 -9
  121. package/dist/tools/bash.js.map +1 -1
  122. package/dist/tools/registry.d.ts +146 -0
  123. package/dist/tools/registry.js +170 -0
  124. package/dist/tools/registry.js.map +1 -0
  125. package/dist/tools/search.js +12 -2
  126. package/dist/tools/search.js.map +1 -1
  127. package/dist/tools/text-editor.js +84 -26
  128. package/dist/tools/text-editor.js.map +1 -1
  129. package/dist/ui/components/chat-history.js +6 -1
  130. package/dist/ui/components/chat-history.js.map +1 -1
  131. package/dist/ui/components/chat-input.d.ts +2 -1
  132. package/dist/ui/components/chat-input.js +5 -2
  133. package/dist/ui/components/chat-input.js.map +1 -1
  134. package/dist/ui/components/chat-interface.js +187 -5
  135. package/dist/ui/components/chat-interface.js.map +1 -1
  136. package/dist/ui/components/context-breakdown.d.ts +23 -0
  137. package/dist/ui/components/context-breakdown.js +124 -0
  138. package/dist/ui/components/context-breakdown.js.map +1 -0
  139. package/dist/ui/components/keyboard-help.d.ts +17 -0
  140. package/dist/ui/components/keyboard-help.js +116 -0
  141. package/dist/ui/components/keyboard-help.js.map +1 -0
  142. package/dist/ui/components/keyboard-hints.js +2 -2
  143. package/dist/ui/components/keyboard-hints.js.map +1 -1
  144. package/dist/ui/components/quick-actions.js +43 -7
  145. package/dist/ui/components/quick-actions.js.map +1 -1
  146. package/dist/ui/components/status-bar.d.ts +3 -0
  147. package/dist/ui/components/status-bar.js +25 -16
  148. package/dist/ui/components/status-bar.js.map +1 -1
  149. package/dist/ui/components/toast-notification.d.ts +42 -0
  150. package/dist/ui/components/toast-notification.js +30 -2
  151. package/dist/ui/components/toast-notification.js.map +1 -1
  152. package/dist/ui/components/tool-group-display.js +34 -4
  153. package/dist/ui/components/tool-group-display.js.map +1 -1
  154. package/dist/ui/components/welcome-panel.js +2 -2
  155. package/dist/ui/components/welcome-panel.js.map +1 -1
  156. package/dist/ui/hooks/use-enhanced-input.d.ts +9 -1
  157. package/dist/ui/hooks/use-enhanced-input.js +486 -41
  158. package/dist/ui/hooks/use-enhanced-input.js.map +1 -1
  159. package/dist/ui/hooks/use-input-handler.d.ts +11 -1
  160. package/dist/ui/hooks/use-input-handler.js +67 -3
  161. package/dist/ui/hooks/use-input-handler.js.map +1 -1
  162. package/dist/ui/hooks/use-input-history.d.ts +1 -1
  163. package/dist/ui/hooks/use-input-history.js +50 -14
  164. package/dist/ui/hooks/use-input-history.js.map +1 -1
  165. package/dist/ui/utils/bracketed-paste-handler.d.ts +97 -0
  166. package/dist/ui/utils/bracketed-paste-handler.js +322 -0
  167. package/dist/ui/utils/bracketed-paste-handler.js.map +1 -0
  168. package/dist/ui/utils/change-summarizer.js +16 -6
  169. package/dist/ui/utils/change-summarizer.js.map +1 -1
  170. package/dist/ui/utils/tool-grouper.d.ts +10 -1
  171. package/dist/ui/utils/tool-grouper.js +143 -30
  172. package/dist/ui/utils/tool-grouper.js.map +1 -1
  173. package/dist/utils/auto-accept-logger.d.ts +173 -0
  174. package/dist/utils/auto-accept-logger.js +420 -0
  175. package/dist/utils/auto-accept-logger.js.map +1 -0
  176. package/dist/utils/background-task-manager.d.ts +11 -0
  177. package/dist/utils/background-task-manager.js +124 -38
  178. package/dist/utils/background-task-manager.js.map +1 -1
  179. package/dist/utils/confirmation-service.d.ts +1 -0
  180. package/dist/utils/confirmation-service.js +6 -1
  181. package/dist/utils/confirmation-service.js.map +1 -1
  182. package/dist/utils/encryption.d.ts +8 -0
  183. package/dist/utils/encryption.js +44 -27
  184. package/dist/utils/encryption.js.map +1 -1
  185. package/dist/utils/enhanced-error-messages.d.ts +33 -0
  186. package/dist/utils/enhanced-error-messages.js +420 -0
  187. package/dist/utils/enhanced-error-messages.js.map +1 -0
  188. package/dist/utils/error-handler.d.ts +13 -3
  189. package/dist/utils/error-handler.js +16 -4
  190. package/dist/utils/error-handler.js.map +1 -1
  191. package/dist/utils/external-editor.d.ts +47 -0
  192. package/dist/utils/external-editor.js +179 -0
  193. package/dist/utils/external-editor.js.map +1 -0
  194. package/dist/utils/history-migration.d.ts +9 -0
  195. package/dist/utils/history-migration.js +36 -0
  196. package/dist/utils/history-migration.js.map +1 -0
  197. package/dist/utils/paste-utils.js +12 -11
  198. package/dist/utils/paste-utils.js.map +1 -1
  199. package/dist/utils/rate-limiter.js +7 -0
  200. package/dist/utils/rate-limiter.js.map +1 -1
  201. package/dist/utils/safety-rules.d.ts +64 -0
  202. package/dist/utils/safety-rules.js +225 -0
  203. package/dist/utils/safety-rules.js.map +1 -0
  204. package/dist/utils/settings-manager.d.ts +89 -1
  205. package/dist/utils/settings-manager.js +359 -3
  206. package/dist/utils/settings-manager.js.map +1 -1
  207. package/dist/utils/token-counter.d.ts +2 -0
  208. package/dist/utils/token-counter.js +17 -4
  209. package/dist/utils/token-counter.js.map +1 -1
  210. package/dist/utils/version.d.ts +11 -2
  211. package/dist/utils/version.js +54 -21
  212. package/dist/utils/version.js.map +1 -1
  213. package/package.json +2 -1
@@ -0,0 +1,740 @@
1
+ /**
2
+ * Type-Safe MCP Client (Phase 1 Improvements)
3
+ *
4
+ * Improvements applied:
5
+ * 1. SafeMutex with lock tokens (prevents race conditions)
6
+ * 2. Result types for all public APIs (explicit error handling)
7
+ * 3. State machine for connection tracking (type-safe states)
8
+ * 4. Branded types for ServerName/ToolName (prevent confusion)
9
+ * 5. Invariant checks (runtime validation)
10
+ *
11
+ * Coverage: 70% → 85%+ (Phase 1)
12
+ */
13
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
14
+ import { EventEmitter } from "events";
15
+ import { createTransport } from "./transports.js";
16
+ import { MCP_CONFIG, ERROR_MESSAGES } from "../constants.js";
17
+ import { MCPServerConfigSchema } from "../schemas/settings-schemas.js";
18
+ import { getTokenCounter } from "../utils/token-counter.js";
19
+ // Phase 1: Import type safety utilities
20
+ import { SafeKeyedMutex } from "./mutex-safe.js";
21
+ import { Ok, Err } from "./type-safety.js";
22
+ import { createServerName } from "./type-safety.js";
23
+ import { assertValidServerName } from "./invariants.js";
24
+ /**
25
+ * Default reconnection configuration
26
+ */
27
+ export const DEFAULT_RECONNECTION_CONFIG = {
28
+ enabled: true,
29
+ maxRetries: 5,
30
+ initialDelayMs: 1000, // 1 second
31
+ maxDelayMs: 30000, // 30 seconds
32
+ backoffMultiplier: 2 // exponential: 1s, 2s, 4s, 8s, 16s, 30s (max)
33
+ };
34
+ /**
35
+ * Default health check configuration
36
+ */
37
+ export const DEFAULT_HEALTH_CHECK_CONFIG = {
38
+ enabled: true,
39
+ intervalMs: 30000 // 30 seconds
40
+ };
41
+ /**
42
+ * Type-safe MCP Manager with improved safety
43
+ */
44
+ export class MCPManagerV2 extends EventEmitter {
45
+ // Phase 1: Replace Maps with state machine
46
+ connections = new Map();
47
+ tools = new Map();
48
+ // Phase 1: Use SafeMutex instead of pendingConnections Map
49
+ connectionMutex = new SafeKeyedMutex();
50
+ initializationPromise = null;
51
+ tokenCounter = getTokenCounter();
52
+ disposed = false;
53
+ // Phase 2: Reconnection management
54
+ reconnectionConfig;
55
+ healthCheckConfig;
56
+ reconnectionAttempts = new Map();
57
+ reconnectionTimers = new Map();
58
+ serverConfigs = new Map();
59
+ healthCheckTimer = null;
60
+ constructor(reconnectionConfig = {}, healthCheckConfig = {}) {
61
+ super();
62
+ this.reconnectionConfig = {
63
+ ...DEFAULT_RECONNECTION_CONFIG,
64
+ ...reconnectionConfig
65
+ };
66
+ this.healthCheckConfig = {
67
+ ...DEFAULT_HEALTH_CHECK_CONFIG,
68
+ ...healthCheckConfig
69
+ };
70
+ // Start health checks if enabled
71
+ if (this.healthCheckConfig.enabled) {
72
+ this.startHealthChecks();
73
+ }
74
+ }
75
+ /**
76
+ * Add MCP server with type-safe connection management
77
+ *
78
+ * Phase 1 improvements:
79
+ * - Returns Result instead of throwing
80
+ * - Uses SafeMutex for concurrency control
81
+ * - Tracks state machine transitions
82
+ * - Validates inputs with invariants
83
+ */
84
+ async addServer(config) {
85
+ // Phase 1: Check if disposed
86
+ if (this.disposed) {
87
+ return Err(new Error('MCPManager is disposed'));
88
+ }
89
+ // Phase 1: Validate server name (branded type creation)
90
+ const serverName = createServerName(config.name);
91
+ if (!serverName) {
92
+ return Err(new Error(`Invalid server name: "${config.name}"`));
93
+ }
94
+ // Phase 1: Check current state
95
+ const currentState = this.connections.get(serverName);
96
+ if (currentState) {
97
+ switch (currentState.status) {
98
+ case 'connected':
99
+ return Ok(undefined); // Already connected
100
+ case 'connecting':
101
+ // Wait for existing connection attempt
102
+ return await currentState.promise;
103
+ case 'disconnecting':
104
+ return Err(new Error(`Server ${serverName} is disconnecting`));
105
+ case 'failed':
106
+ // Can retry after failure
107
+ break;
108
+ case 'idle':
109
+ // Can connect
110
+ break;
111
+ }
112
+ }
113
+ // Phase 1: Use SafeMutex for concurrency control
114
+ const mutexResult = await this.connectionMutex.runExclusive(serverName, async () => {
115
+ // Double-check state inside mutex
116
+ const state = this.connections.get(serverName);
117
+ if (state?.status === 'connected') {
118
+ return Ok(undefined);
119
+ }
120
+ return await this._addServerInternal(serverName, config);
121
+ });
122
+ // Unwrap nested Result
123
+ if (!mutexResult.success) {
124
+ return mutexResult;
125
+ }
126
+ return mutexResult.value;
127
+ }
128
+ /**
129
+ * Internal connection logic with state transitions
130
+ */
131
+ async _addServerInternal(serverName, config) {
132
+ // Validate config with Zod
133
+ const validationResult = MCPServerConfigSchema.safeParse(config);
134
+ if (!validationResult.success) {
135
+ // Transition to failed state
136
+ this.connections.set(serverName, {
137
+ status: 'failed',
138
+ serverName,
139
+ error: new Error(`Invalid config: ${validationResult.error.message}`),
140
+ failedAt: Date.now()
141
+ });
142
+ return Err(new Error(`Invalid MCP server config: ${validationResult.error.message}`));
143
+ }
144
+ const validatedConfig = validationResult.data;
145
+ // Phase 2: Store server config for reconnection attempts
146
+ this.serverConfigs.set(serverName, validatedConfig);
147
+ // Handle legacy stdio-only configuration
148
+ let transportConfig = validatedConfig.transport;
149
+ if (!transportConfig && validatedConfig.command) {
150
+ transportConfig = {
151
+ type: 'stdio',
152
+ command: validatedConfig.command,
153
+ args: validatedConfig.args,
154
+ env: validatedConfig.env
155
+ };
156
+ }
157
+ if (!transportConfig) {
158
+ const error = new Error(ERROR_MESSAGES.TRANSPORT_CONFIG_REQUIRED);
159
+ this.connections.set(serverName, {
160
+ status: 'failed',
161
+ serverName,
162
+ error,
163
+ failedAt: Date.now()
164
+ });
165
+ return Err(error);
166
+ }
167
+ try {
168
+ // Transition to connecting state
169
+ const startedAt = Date.now();
170
+ const connectingPromise = (async () => {
171
+ try {
172
+ // Create transport
173
+ const transport = createTransport(transportConfig);
174
+ // Create client
175
+ const client = new Client({
176
+ name: MCP_CONFIG.CLIENT_NAME,
177
+ version: MCP_CONFIG.CLIENT_VERSION
178
+ }, {
179
+ capabilities: {
180
+ tools: {}
181
+ }
182
+ });
183
+ // Connect
184
+ const sdkTransport = await transport.connect();
185
+ await client.connect(sdkTransport);
186
+ // List tools
187
+ const toolsResult = await client.listTools();
188
+ // Register tools with branded types
189
+ for (const tool of toolsResult.tools) {
190
+ const toolName = createToolName(`mcp__${serverName}__${tool.name}`);
191
+ if (!toolName) {
192
+ console.warn(`Invalid tool name: ${tool.name}`);
193
+ continue;
194
+ }
195
+ const mcpTool = {
196
+ name: toolName,
197
+ description: tool.description || `Tool from ${serverName} server`,
198
+ inputSchema: tool.inputSchema,
199
+ serverName
200
+ };
201
+ this.tools.set(toolName, mcpTool);
202
+ }
203
+ // Transition to connected state
204
+ this.connections.set(serverName, {
205
+ status: 'connected',
206
+ serverName,
207
+ client,
208
+ transport,
209
+ connectedAt: Date.now()
210
+ });
211
+ this.emit('serverAdded', serverName, toolsResult.tools.length);
212
+ return Ok(undefined);
213
+ }
214
+ catch (error) {
215
+ // Transition to failed state
216
+ const err = error instanceof Error ? error : new Error(String(error));
217
+ this.connections.set(serverName, {
218
+ status: 'failed',
219
+ serverName,
220
+ error: err,
221
+ failedAt: Date.now()
222
+ });
223
+ this.emit('serverError', serverName, err);
224
+ // Phase 2: Schedule reconnection if enabled
225
+ if (this.reconnectionConfig.enabled && !this.disposed) {
226
+ this.scheduleReconnection(serverName, validatedConfig);
227
+ }
228
+ return Err(err);
229
+ }
230
+ })();
231
+ this.connections.set(serverName, {
232
+ status: 'connecting',
233
+ serverName,
234
+ startedAt,
235
+ promise: connectingPromise
236
+ });
237
+ return await connectingPromise;
238
+ }
239
+ catch (error) {
240
+ const err = error instanceof Error ? error : new Error(String(error));
241
+ this.connections.set(serverName, {
242
+ status: 'failed',
243
+ serverName,
244
+ error: err,
245
+ failedAt: Date.now()
246
+ });
247
+ // Phase 2: Schedule reconnection if enabled
248
+ if (this.reconnectionConfig.enabled && !this.disposed) {
249
+ this.scheduleReconnection(serverName, validatedConfig);
250
+ }
251
+ return Err(err);
252
+ }
253
+ }
254
+ /**
255
+ * Remove MCP server with proper state transitions
256
+ */
257
+ async removeServer(serverName) {
258
+ // Phase 1: Check if disposed
259
+ if (this.disposed) {
260
+ return Err(new Error('MCPManager is disposed'));
261
+ }
262
+ // Phase 1: Validate server name
263
+ assertValidServerName(serverName);
264
+ // Phase 2: Cancel any pending reconnection attempts
265
+ this.cancelReconnection(serverName);
266
+ this.reconnectionAttempts.delete(serverName);
267
+ this.serverConfigs.delete(serverName);
268
+ // BUG FIX: Use mutex to prevent TOCTOU race with addServer/callTool
269
+ const mutexResult = await this.connectionMutex.runExclusive(serverName, async () => {
270
+ // Re-check state inside mutex (prevent race conditions)
271
+ const state = this.connections.get(serverName);
272
+ if (!state) {
273
+ return Err(new Error(`Server ${serverName} not found`));
274
+ }
275
+ // Check if we can disconnect from current state
276
+ if (state.status === 'connecting') {
277
+ return Err(new Error(`Server ${serverName} is still connecting`));
278
+ }
279
+ if (state.status === 'disconnecting') {
280
+ return Err(new Error(`Server ${serverName} is already disconnecting`));
281
+ }
282
+ if (state.status !== 'connected') {
283
+ // Remove from map if not connected
284
+ this.connections.delete(serverName);
285
+ return Ok(undefined);
286
+ }
287
+ return await this._removeServerInternal(serverName, state);
288
+ });
289
+ // Unwrap nested Result
290
+ if (!mutexResult.success) {
291
+ return mutexResult;
292
+ }
293
+ return mutexResult.value;
294
+ }
295
+ /**
296
+ * Internal disconnection logic with state transitions
297
+ */
298
+ async _removeServerInternal(serverName, state) {
299
+ // Transition to disconnecting state
300
+ this.connections.set(serverName, {
301
+ status: 'disconnecting',
302
+ serverName,
303
+ client: state.client,
304
+ transport: state.transport
305
+ });
306
+ try {
307
+ // Remove tools
308
+ for (const [toolName, tool] of this.tools.entries()) {
309
+ if (tool.serverName === serverName) {
310
+ this.tools.delete(toolName);
311
+ }
312
+ }
313
+ // Disconnect client
314
+ const clientResult = await this.closeClient(state.client, serverName);
315
+ // Disconnect transport
316
+ const transportResult = await this.disconnectTransport(state.transport, serverName);
317
+ // Aggregate errors
318
+ const errors = [];
319
+ if (!clientResult.success)
320
+ errors.push(clientResult.error);
321
+ if (!transportResult.success)
322
+ errors.push(transportResult.error);
323
+ // Transition to idle state
324
+ this.connections.delete(serverName);
325
+ this.emit('serverRemoved', serverName);
326
+ if (errors.length > 0) {
327
+ return Err(new AggregateError(errors, `Failed to fully disconnect ${serverName}`));
328
+ }
329
+ return Ok(undefined);
330
+ }
331
+ catch (error) {
332
+ // Even if error, remove from state
333
+ this.connections.delete(serverName);
334
+ return Err(error instanceof Error ? error : new Error(String(error)));
335
+ }
336
+ }
337
+ /**
338
+ * Close client with error handling
339
+ */
340
+ async closeClient(client, serverName) {
341
+ try {
342
+ await client.close();
343
+ return Ok(undefined);
344
+ }
345
+ catch (error) {
346
+ const err = error instanceof Error ? error : new Error(String(error));
347
+ console.warn(`Error closing MCP client ${serverName}:`, err);
348
+ return Err(err);
349
+ }
350
+ }
351
+ /**
352
+ * Disconnect transport with error handling
353
+ */
354
+ async disconnectTransport(transport, serverName) {
355
+ try {
356
+ await transport.disconnect();
357
+ return Ok(undefined);
358
+ }
359
+ catch (error) {
360
+ const err = error instanceof Error ? error : new Error(String(error));
361
+ console.warn(`Error disconnecting MCP transport ${serverName}:`, err);
362
+ return Err(err);
363
+ }
364
+ }
365
+ /**
366
+ * Call MCP tool with type safety
367
+ */
368
+ async callTool(toolName, arguments_) {
369
+ if (this.disposed) {
370
+ return Err(new Error('MCPManager is disposed'));
371
+ }
372
+ const tool = this.tools.get(toolName);
373
+ if (!tool) {
374
+ return Err(new Error(`Tool ${toolName} not found`));
375
+ }
376
+ // BUG FIX: Get client reference inside mutex to prevent TOCTOU race with removeServer
377
+ const mutexResult = await this.connectionMutex.runExclusive(tool.serverName, async () => {
378
+ // Re-check state inside mutex
379
+ const state = this.connections.get(tool.serverName);
380
+ if (!state) {
381
+ return Err(new Error(`Server ${tool.serverName} not found`));
382
+ }
383
+ if (state.status !== 'connected') {
384
+ return Err(new Error(`Server ${tool.serverName} not connected (status: ${state.status})`));
385
+ }
386
+ // Return client snapshot (mutex released after this, but client reference is safe to use)
387
+ return Ok(state.client);
388
+ });
389
+ // Unwrap nested Result
390
+ if (!mutexResult.success) {
391
+ return mutexResult;
392
+ }
393
+ const clientResult = mutexResult.value;
394
+ if (!clientResult.success) {
395
+ return clientResult;
396
+ }
397
+ const client = clientResult.value;
398
+ try {
399
+ // Extract original tool name
400
+ const prefix = `mcp__${tool.serverName}__`;
401
+ const originalToolName = toolName.startsWith(prefix)
402
+ ? toolName.substring(prefix.length)
403
+ : toolName;
404
+ // Validate arguments
405
+ const safeArgs = (arguments_ && typeof arguments_ === 'object' && !Array.isArray(arguments_))
406
+ ? arguments_
407
+ : {};
408
+ // Call tool (mutex released, but client reference is still valid)
409
+ const result = await client.callTool({
410
+ name: originalToolName,
411
+ arguments: safeArgs
412
+ });
413
+ // Apply token limiting
414
+ if (MCP_CONFIG.TRUNCATION_ENABLED) {
415
+ const resultText = JSON.stringify(result.content);
416
+ const tokenCount = this.tokenCounter.countTokens(resultText);
417
+ if (tokenCount > MCP_CONFIG.TOKEN_HARD_LIMIT) {
418
+ const truncatedText = this.truncateToTokenLimit(resultText, MCP_CONFIG.TOKEN_HARD_LIMIT);
419
+ result.content = [
420
+ { type: 'text', text: truncatedText },
421
+ {
422
+ type: 'text',
423
+ text: `\n\n⚠️ Output truncated: ${tokenCount.toLocaleString()} tokens exceeded limit of ${MCP_CONFIG.TOKEN_HARD_LIMIT.toLocaleString()} tokens`
424
+ }
425
+ ];
426
+ this.emit('token-limit-exceeded', {
427
+ toolName,
428
+ serverName: tool.serverName,
429
+ originalTokens: tokenCount,
430
+ truncatedTokens: MCP_CONFIG.TOKEN_HARD_LIMIT
431
+ });
432
+ }
433
+ else if (tokenCount > MCP_CONFIG.TOKEN_WARNING_THRESHOLD) {
434
+ this.emit('token-warning', {
435
+ toolName,
436
+ serverName: tool.serverName,
437
+ tokenCount,
438
+ threshold: MCP_CONFIG.TOKEN_WARNING_THRESHOLD
439
+ });
440
+ }
441
+ }
442
+ return Ok(result);
443
+ }
444
+ catch (error) {
445
+ return Err(error instanceof Error ? error : new Error(String(error)));
446
+ }
447
+ }
448
+ /**
449
+ * Truncate text to fit within token limit
450
+ * UNICODE FIX: Uses grapheme clusters
451
+ */
452
+ truncateToTokenLimit(text, maxTokens) {
453
+ const chars = Array.from(text);
454
+ let low = 0;
455
+ let high = chars.length;
456
+ let result = text;
457
+ while (low <= high) {
458
+ const mid = Math.floor((low + high) / 2);
459
+ const truncated = chars.slice(0, mid).join('');
460
+ const tokens = this.tokenCounter.countTokens(truncated);
461
+ if (tokens <= maxTokens) {
462
+ result = truncated;
463
+ low = mid + 1;
464
+ }
465
+ else {
466
+ high = mid - 1;
467
+ }
468
+ }
469
+ return result;
470
+ }
471
+ /**
472
+ * Get all tools
473
+ */
474
+ getTools() {
475
+ return Array.from(this.tools.values());
476
+ }
477
+ /**
478
+ * Get all connected servers
479
+ */
480
+ getServers() {
481
+ const connected = [];
482
+ for (const [serverName, state] of this.connections.entries()) {
483
+ if (state.status === 'connected') {
484
+ connected.push(serverName);
485
+ }
486
+ }
487
+ return connected;
488
+ }
489
+ /**
490
+ * Get connection state for a server
491
+ */
492
+ getConnectionState(serverName) {
493
+ return this.connections.get(serverName);
494
+ }
495
+ /**
496
+ * Get transport type for a server
497
+ */
498
+ getTransportType(serverName) {
499
+ const state = this.connections.get(serverName);
500
+ if (!state) {
501
+ return Err(new Error(`Server ${serverName} not found`));
502
+ }
503
+ if (state.status !== 'connected') {
504
+ return Err(new Error(`Server ${serverName} not connected`));
505
+ }
506
+ const type = state.transport.getType();
507
+ return Ok(type);
508
+ }
509
+ /**
510
+ * Schedule reconnection for a failed server with exponential backoff
511
+ *
512
+ * Phase 2: Automatic reconnection logic
513
+ *
514
+ * @param serverName - Server to reconnect
515
+ * @param config - Server configuration
516
+ */
517
+ scheduleReconnection(serverName, config) {
518
+ // Cancel any existing reconnection timer
519
+ this.cancelReconnection(serverName);
520
+ // Get current attempt count
521
+ const attempts = this.reconnectionAttempts.get(serverName) || 0;
522
+ // Check if we've exceeded max retries
523
+ if (attempts >= this.reconnectionConfig.maxRetries) {
524
+ this.emit('reconnection-failed', serverName, attempts, 'Max retries exceeded');
525
+ return;
526
+ }
527
+ // Calculate exponential backoff delay
528
+ const baseDelay = this.reconnectionConfig.initialDelayMs;
529
+ const multiplier = Math.pow(this.reconnectionConfig.backoffMultiplier, attempts);
530
+ const calculatedDelay = Math.min(baseDelay * multiplier, this.reconnectionConfig.maxDelayMs);
531
+ // Emit reconnection scheduled event
532
+ this.emit('reconnection-scheduled', serverName, attempts + 1, calculatedDelay);
533
+ // Schedule reconnection attempt
534
+ const timer = setTimeout(async () => {
535
+ // Increment attempt count
536
+ this.reconnectionAttempts.set(serverName, attempts + 1);
537
+ // Attempt reconnection
538
+ const result = await this.addServer(config);
539
+ if (result.success) {
540
+ // Success! Reset attempt counter
541
+ this.reconnectionAttempts.delete(serverName);
542
+ this.emit('reconnection-succeeded', serverName, attempts + 1);
543
+ }
544
+ else {
545
+ // Failed - will be rescheduled by addServer error handling
546
+ // (which calls scheduleReconnection again)
547
+ }
548
+ }, calculatedDelay);
549
+ this.reconnectionTimers.set(serverName, timer);
550
+ }
551
+ /**
552
+ * Cancel reconnection for a server
553
+ */
554
+ cancelReconnection(serverName) {
555
+ const timer = this.reconnectionTimers.get(serverName);
556
+ if (timer) {
557
+ clearTimeout(timer);
558
+ this.reconnectionTimers.delete(serverName);
559
+ }
560
+ }
561
+ /**
562
+ * Perform health check on a single server
563
+ *
564
+ * @param serverName - Server to check
565
+ * @returns Result indicating health status
566
+ */
567
+ async healthCheck(serverName) {
568
+ const state = this.connections.get(serverName);
569
+ if (!state) {
570
+ return Err(new Error(`Server ${serverName} not found`));
571
+ }
572
+ if (state.status !== 'connected') {
573
+ return Ok(false); // Not connected = not healthy
574
+ }
575
+ try {
576
+ // Simple health check: try to list tools
577
+ await state.client.listTools();
578
+ return Ok(true); // Healthy
579
+ }
580
+ catch (error) {
581
+ // Server is unhealthy
582
+ this.emit('server-unhealthy', serverName, error);
583
+ // Transition to failed state
584
+ const err = error instanceof Error ? error : new Error(String(error));
585
+ this.connections.set(serverName, {
586
+ status: 'failed',
587
+ serverName,
588
+ error: err,
589
+ failedAt: Date.now()
590
+ });
591
+ // Close the connection
592
+ try {
593
+ await state.client.close();
594
+ await state.transport.disconnect();
595
+ }
596
+ catch (closeError) {
597
+ console.warn(`Error closing unhealthy server ${serverName}:`, closeError);
598
+ }
599
+ // Schedule reconnection
600
+ const config = this.serverConfigs.get(serverName);
601
+ if (config && this.reconnectionConfig.enabled && !this.disposed) {
602
+ this.scheduleReconnection(serverName, config);
603
+ }
604
+ return Ok(false); // Unhealthy
605
+ }
606
+ }
607
+ /**
608
+ * Start periodic health checks for all connected servers
609
+ */
610
+ startHealthChecks() {
611
+ if (this.healthCheckTimer) {
612
+ return; // Already running
613
+ }
614
+ const runHealthChecks = async () => {
615
+ const connectedServers = Array.from(this.connections.entries())
616
+ .filter(([_, state]) => state.status === 'connected')
617
+ .map(([name, _]) => name);
618
+ for (const serverName of connectedServers) {
619
+ await this.healthCheck(serverName);
620
+ }
621
+ };
622
+ // Run initial health check
623
+ runHealthChecks().catch(error => {
624
+ console.warn('Health check error:', error);
625
+ });
626
+ // Schedule periodic checks
627
+ this.healthCheckTimer = setInterval(() => {
628
+ runHealthChecks().catch(error => {
629
+ console.warn('Health check error:', error);
630
+ });
631
+ }, this.healthCheckConfig.intervalMs);
632
+ }
633
+ /**
634
+ * Stop periodic health checks
635
+ */
636
+ stopHealthChecks() {
637
+ if (this.healthCheckTimer) {
638
+ clearInterval(this.healthCheckTimer);
639
+ this.healthCheckTimer = null;
640
+ }
641
+ }
642
+ /**
643
+ * Shutdown all servers
644
+ */
645
+ async shutdown() {
646
+ const serverNames = Array.from(this.connections.keys());
647
+ const results = await Promise.allSettled(serverNames.map(name => this.removeServer(name)));
648
+ const errors = [];
649
+ results.forEach((result, index) => {
650
+ if (result.status === 'rejected') {
651
+ console.warn(`Failed to remove server ${serverNames[index]}:`, result.reason);
652
+ errors.push(result.reason);
653
+ }
654
+ else if (!result.value.success) {
655
+ errors.push(result.value.error);
656
+ }
657
+ });
658
+ if (errors.length > 0) {
659
+ return Err(new AggregateError(errors, 'Shutdown had errors'));
660
+ }
661
+ return Ok(undefined);
662
+ }
663
+ /**
664
+ * Ensure servers initialized
665
+ */
666
+ async ensureServersInitialized() {
667
+ if (this.initializationPromise) {
668
+ return this.initializationPromise;
669
+ }
670
+ if (this.connections.size === 0 && !this.initializationPromise) {
671
+ this.initializationPromise = (async () => {
672
+ try {
673
+ const { loadMCPConfig } = await import('../mcp/config.js');
674
+ const config = loadMCPConfig();
675
+ const initPromises = config.servers.map(async (serverConfig) => {
676
+ const serverName = createServerName(serverConfig.name);
677
+ if (!serverName) {
678
+ console.warn(`Invalid server name: ${serverConfig.name}`);
679
+ return;
680
+ }
681
+ const result = await this.addServer(serverConfig);
682
+ if (!result.success) {
683
+ console.warn(`Failed to initialize MCP server ${serverName}:`, result.error);
684
+ }
685
+ });
686
+ await Promise.all(initPromises);
687
+ return Ok(undefined);
688
+ }
689
+ catch (error) {
690
+ console.error('Failed to initialize MCP servers:', error);
691
+ return Err(error instanceof Error ? error : new Error(String(error)));
692
+ }
693
+ finally {
694
+ this.initializationPromise = null;
695
+ }
696
+ })();
697
+ }
698
+ if (this.initializationPromise) {
699
+ return await this.initializationPromise;
700
+ }
701
+ return Ok(undefined);
702
+ }
703
+ /**
704
+ * Dispose all resources
705
+ */
706
+ async dispose() {
707
+ if (this.disposed) {
708
+ return Ok(undefined);
709
+ }
710
+ this.disposed = true;
711
+ // Phase 2: Stop health checks
712
+ this.stopHealthChecks();
713
+ // Phase 2: Cancel all reconnection timers
714
+ for (const timer of this.reconnectionTimers.values()) {
715
+ clearTimeout(timer);
716
+ }
717
+ this.reconnectionTimers.clear();
718
+ this.reconnectionAttempts.clear();
719
+ this.serverConfigs.clear();
720
+ const shutdownResult = await this.shutdown();
721
+ this.removeAllListeners();
722
+ return shutdownResult;
723
+ }
724
+ }
725
+ /**
726
+ * Helper functions for creating branded types
727
+ */
728
+ function createToolName(name) {
729
+ // Tool names can have double underscores for MCP prefix
730
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
731
+ return null;
732
+ }
733
+ if (name.length < 1 || name.length > 128) {
734
+ return null;
735
+ }
736
+ return name;
737
+ }
738
+ // Re-export createServerName from type-safety
739
+ export { createServerName, createToolName };
740
+ //# sourceMappingURL=client-v2.js.map