@defai.digital/ax-cli 3.7.2 → 3.8.2
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/README.md +148 -53
- package/dist/agent/context-manager.d.ts +15 -1
- package/dist/agent/context-manager.js +50 -19
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/dependency-resolver.js +13 -7
- package/dist/agent/dependency-resolver.js.map +1 -1
- package/dist/agent/llm-agent.d.ts +37 -0
- package/dist/agent/llm-agent.js +318 -98
- package/dist/agent/llm-agent.js.map +1 -1
- package/dist/agent/status-reporter.d.ts +114 -0
- package/dist/agent/status-reporter.js +335 -0
- package/dist/agent/status-reporter.js.map +1 -0
- package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js +8 -2
- package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js.map +1 -1
- package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js +3 -1
- package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js.map +1 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-const.js +3 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-const.js.map +1 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js +3 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js.map +1 -1
- package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js +9 -3
- package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js.map +1 -1
- package/dist/analyzers/git/churn-calculator.d.ts +2 -0
- package/dist/analyzers/git/churn-calculator.js +42 -8
- package/dist/analyzers/git/churn-calculator.js.map +1 -1
- package/dist/analyzers/git/hotspot-detector.js +2 -2
- package/dist/analyzers/git/hotspot-detector.js.map +1 -1
- package/dist/analyzers/metrics/metrics-analyzer.js +1 -1
- package/dist/analyzers/metrics/metrics-analyzer.js.map +1 -1
- package/dist/analyzers/security/security-analyzer.js +1 -1
- package/dist/analyzers/security/security-analyzer.js.map +1 -1
- package/dist/checkpoint/manager.d.ts +1 -0
- package/dist/checkpoint/manager.js +49 -9
- package/dist/checkpoint/manager.js.map +1 -1
- package/dist/checkpoint/storage.js +2 -2
- package/dist/checkpoint/storage.js.map +1 -1
- package/dist/commands/mcp-migrate.d.ts +9 -0
- package/dist/commands/mcp-migrate.js +172 -0
- package/dist/commands/mcp-migrate.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +211 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/vscode.d.ts +7 -0
- package/dist/commands/vscode.js +363 -0
- package/dist/commands/vscode.js.map +1 -0
- package/dist/index.js +79 -30
- package/dist/index.js.map +1 -1
- package/dist/llm/client.js +33 -4
- package/dist/llm/client.js.map +1 -1
- package/dist/mcp/automatosx-loader.d.ts +84 -0
- package/dist/mcp/automatosx-loader.js +238 -0
- package/dist/mcp/automatosx-loader.js.map +1 -0
- package/dist/mcp/client-mutex-patch.d.ts +36 -0
- package/dist/mcp/client-mutex-patch.js +75 -0
- package/dist/mcp/client-mutex-patch.js.map +1 -0
- package/dist/mcp/client-v2.d.ts +229 -0
- package/dist/mcp/client-v2.js +740 -0
- package/dist/mcp/client-v2.js.map +1 -0
- package/dist/mcp/client.d.ts +111 -13
- package/dist/mcp/client.js +168 -253
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/config-detector-v2.d.ts +83 -0
- package/dist/mcp/config-detector-v2.js +328 -0
- package/dist/mcp/config-detector-v2.js.map +1 -0
- package/dist/mcp/config-detector.d.ts +90 -0
- package/dist/mcp/config-detector.js +242 -0
- package/dist/mcp/config-detector.js.map +1 -0
- package/dist/mcp/config-migrator-v2.d.ts +89 -0
- package/dist/mcp/config-migrator-v2.js +288 -0
- package/dist/mcp/config-migrator-v2.js.map +1 -0
- package/dist/mcp/config-migrator.d.ts +63 -0
- package/dist/mcp/config-migrator.js +269 -0
- package/dist/mcp/config-migrator.js.map +1 -0
- package/dist/mcp/config-v2.d.ts +106 -0
- package/dist/mcp/config-v2.js +417 -0
- package/dist/mcp/config-v2.js.map +1 -0
- package/dist/mcp/config.d.ts +12 -1
- package/dist/mcp/config.js +95 -10
- package/dist/mcp/config.js.map +1 -1
- package/dist/mcp/error-formatter.d.ts +46 -0
- package/dist/mcp/error-formatter.js +244 -0
- package/dist/mcp/error-formatter.js.map +1 -0
- package/dist/mcp/health.d.ts +5 -0
- package/dist/mcp/health.js +22 -2
- package/dist/mcp/health.js.map +1 -1
- package/dist/mcp/invariants.d.ts +141 -0
- package/dist/mcp/invariants.js +243 -0
- package/dist/mcp/invariants.js.map +1 -0
- package/dist/mcp/mutex-safe.d.ts +153 -0
- package/dist/mcp/mutex-safe.js +260 -0
- package/dist/mcp/mutex-safe.js.map +1 -0
- package/dist/mcp/mutex.d.ts +73 -0
- package/dist/mcp/mutex.js +137 -0
- package/dist/mcp/mutex.js.map +1 -0
- package/dist/mcp/reconnection.d.ts +4 -0
- package/dist/mcp/reconnection.js +25 -1
- package/dist/mcp/reconnection.js.map +1 -1
- package/dist/mcp/transports-v2.d.ts +152 -0
- package/dist/mcp/transports-v2.js +481 -0
- package/dist/mcp/transports-v2.js.map +1 -0
- package/dist/mcp/type-safety.d.ts +231 -0
- package/dist/mcp/type-safety.js +273 -0
- package/dist/mcp/type-safety.js.map +1 -0
- package/dist/planner/task-planner.js +13 -0
- package/dist/planner/task-planner.js.map +1 -1
- package/dist/planner/types.d.ts +6 -6
- package/dist/schemas/confirmation-schemas.d.ts +2 -2
- package/dist/schemas/settings-schemas.d.ts +196 -0
- package/dist/schemas/settings-schemas.js +146 -5
- package/dist/schemas/settings-schemas.js.map +1 -1
- package/dist/sdk/index.d.ts +118 -2
- package/dist/sdk/index.js +146 -4
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/testing.d.ts +182 -0
- package/dist/sdk/testing.js +231 -0
- package/dist/sdk/testing.js.map +1 -1
- package/dist/sdk/version.d.ts +114 -15
- package/dist/sdk/version.js +137 -15
- package/dist/sdk/version.js.map +1 -1
- package/dist/tools/bash.js +54 -9
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +170 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/search.js +12 -2
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/text-editor.js +84 -26
- package/dist/tools/text-editor.js.map +1 -1
- package/dist/ui/components/chat-history.js +6 -1
- package/dist/ui/components/chat-history.js.map +1 -1
- package/dist/ui/components/chat-input.d.ts +2 -1
- package/dist/ui/components/chat-input.js +5 -2
- package/dist/ui/components/chat-input.js.map +1 -1
- package/dist/ui/components/chat-interface.js +187 -5
- package/dist/ui/components/chat-interface.js.map +1 -1
- package/dist/ui/components/context-breakdown.d.ts +23 -0
- package/dist/ui/components/context-breakdown.js +124 -0
- package/dist/ui/components/context-breakdown.js.map +1 -0
- package/dist/ui/components/keyboard-help.d.ts +17 -0
- package/dist/ui/components/keyboard-help.js +116 -0
- package/dist/ui/components/keyboard-help.js.map +1 -0
- package/dist/ui/components/keyboard-hints.js +2 -2
- package/dist/ui/components/keyboard-hints.js.map +1 -1
- package/dist/ui/components/quick-actions.js +43 -7
- package/dist/ui/components/quick-actions.js.map +1 -1
- package/dist/ui/components/status-bar.d.ts +3 -0
- package/dist/ui/components/status-bar.js +25 -16
- package/dist/ui/components/status-bar.js.map +1 -1
- package/dist/ui/components/toast-notification.d.ts +42 -0
- package/dist/ui/components/toast-notification.js +30 -2
- package/dist/ui/components/toast-notification.js.map +1 -1
- package/dist/ui/components/tool-group-display.js +34 -4
- package/dist/ui/components/tool-group-display.js.map +1 -1
- package/dist/ui/components/welcome-panel.js +2 -2
- package/dist/ui/components/welcome-panel.js.map +1 -1
- package/dist/ui/hooks/use-enhanced-input.d.ts +9 -1
- package/dist/ui/hooks/use-enhanced-input.js +901 -90
- package/dist/ui/hooks/use-enhanced-input.js.map +1 -1
- package/dist/ui/hooks/use-input-handler.d.ts +11 -1
- package/dist/ui/hooks/use-input-handler.js +67 -3
- package/dist/ui/hooks/use-input-handler.js.map +1 -1
- package/dist/ui/hooks/use-input-history.d.ts +1 -1
- package/dist/ui/hooks/use-input-history.js +50 -14
- package/dist/ui/hooks/use-input-history.js.map +1 -1
- package/dist/ui/utils/bracketed-paste-handler.d.ts +97 -0
- package/dist/ui/utils/bracketed-paste-handler.js +322 -0
- package/dist/ui/utils/bracketed-paste-handler.js.map +1 -0
- package/dist/ui/utils/change-summarizer.js +16 -6
- package/dist/ui/utils/change-summarizer.js.map +1 -1
- package/dist/ui/utils/tool-grouper.d.ts +10 -1
- package/dist/ui/utils/tool-grouper.js +143 -30
- package/dist/ui/utils/tool-grouper.js.map +1 -1
- package/dist/utils/auto-accept-logger.d.ts +173 -0
- package/dist/utils/auto-accept-logger.js +420 -0
- package/dist/utils/auto-accept-logger.js.map +1 -0
- package/dist/utils/background-task-manager.d.ts +11 -0
- package/dist/utils/background-task-manager.js +124 -38
- package/dist/utils/background-task-manager.js.map +1 -1
- package/dist/utils/confirmation-service.d.ts +1 -0
- package/dist/utils/confirmation-service.js +6 -1
- package/dist/utils/confirmation-service.js.map +1 -1
- package/dist/utils/encryption.d.ts +8 -0
- package/dist/utils/encryption.js +44 -27
- package/dist/utils/encryption.js.map +1 -1
- package/dist/utils/enhanced-error-messages.d.ts +33 -0
- package/dist/utils/enhanced-error-messages.js +420 -0
- package/dist/utils/enhanced-error-messages.js.map +1 -0
- package/dist/utils/error-handler.d.ts +13 -3
- package/dist/utils/error-handler.js +16 -4
- package/dist/utils/error-handler.js.map +1 -1
- package/dist/utils/external-editor.d.ts +47 -0
- package/dist/utils/external-editor.js +179 -0
- package/dist/utils/external-editor.js.map +1 -0
- package/dist/utils/history-migration.d.ts +9 -0
- package/dist/utils/history-migration.js +36 -0
- package/dist/utils/history-migration.js.map +1 -0
- package/dist/utils/paste-utils.js +12 -11
- package/dist/utils/paste-utils.js.map +1 -1
- package/dist/utils/rate-limiter.js +20 -1
- package/dist/utils/rate-limiter.js.map +1 -1
- package/dist/utils/safety-rules.d.ts +64 -0
- package/dist/utils/safety-rules.js +225 -0
- package/dist/utils/safety-rules.js.map +1 -0
- package/dist/utils/settings-manager.d.ts +89 -1
- package/dist/utils/settings-manager.js +359 -3
- package/dist/utils/settings-manager.js.map +1 -1
- package/dist/utils/token-counter.d.ts +2 -0
- package/dist/utils/token-counter.js +32 -9
- package/dist/utils/token-counter.js.map +1 -1
- package/dist/utils/version.d.ts +11 -2
- package/dist/utils/version.js +54 -21
- package/dist/utils/version.js.map +1 -1
- 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
|