wave-agent-sdk 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +23 -1
- package/dist/agent.js +34 -4
- package/dist/managers/aiManager.d.ts +36 -0
- package/dist/managers/aiManager.js +240 -8
- package/dist/managers/messageManager.d.ts +1 -3
- package/dist/managers/messageManager.js +19 -9
- package/dist/managers/slashCommandManager.js +28 -0
- package/dist/managers/subagentManager.d.ts +0 -2
- package/dist/services/session.js +2 -9
- package/dist/telemetry/instrumentation.d.ts +1 -1
- package/dist/telemetry/instrumentation.js +57 -28
- package/dist/tools/bashTool.js +16 -6
- package/dist/tools/grepTool.js +11 -3
- package/dist/types/index.d.ts +3 -0
- package/dist/types/messaging.d.ts +2 -0
- package/dist/utils/containerSetup.js +0 -3
- package/dist/utils/messageOperations.d.ts +10 -1
- package/dist/utils/messageOperations.js +19 -1
- package/dist/utils/notificationXml.js +5 -0
- package/dist/utils/ripgrep.d.ts +4 -4
- package/dist/utils/ripgrep.js +4 -27
- package/package.json +20 -8
- package/bin/rg +0 -79
- package/dist/managers/forkedAgentManager.d.ts +0 -50
- package/dist/managers/forkedAgentManager.js +0 -130
- package/scripts/install_ripgrep.js +0 -111
- package/scripts/postinstall.js +0 -38
- package/src/agent.ts +0 -1271
- package/src/constants/subagents.ts +0 -4
- package/src/constants/toolLimits.ts +0 -15
- package/src/constants/tools.ts +0 -24
- package/src/core/plugin.ts +0 -237
- package/src/core/session.ts +0 -9
- package/src/index.ts +0 -38
- package/src/managers/MemoryRuleManager.ts +0 -198
- package/src/managers/aiManager.ts +0 -2429
- package/src/managers/backgroundTaskManager.ts +0 -480
- package/src/managers/bangManager.ts +0 -119
- package/src/managers/cronManager.ts +0 -401
- package/src/managers/foregroundTaskManager.ts +0 -30
- package/src/managers/forkedAgentManager.ts +0 -216
- package/src/managers/hookManager.ts +0 -1089
- package/src/managers/liveConfigManager.ts +0 -445
- package/src/managers/lspManager.ts +0 -470
- package/src/managers/mcpManager.ts +0 -1010
- package/src/managers/messageManager.ts +0 -1180
- package/src/managers/messageQueue.ts +0 -157
- package/src/managers/permissionManager.ts +0 -1140
- package/src/managers/planManager.ts +0 -112
- package/src/managers/pluginManager.ts +0 -330
- package/src/managers/pluginScopeManager.ts +0 -124
- package/src/managers/reversionManager.ts +0 -178
- package/src/managers/skillManager.ts +0 -578
- package/src/managers/slashCommandManager.ts +0 -564
- package/src/managers/subagentManager.ts +0 -915
- package/src/managers/toolManager.ts +0 -489
- package/src/managers/workflowManager.ts +0 -493
- package/src/prompts/autoMemory.ts +0 -33
- package/src/prompts/autoMemoryExtraction.ts +0 -146
- package/src/prompts/index.ts +0 -506
- package/src/prompts/planModeReminders.ts +0 -126
- package/src/services/GitService.ts +0 -131
- package/src/services/MarketplaceService.ts +0 -1052
- package/src/services/MemoryRuleService.ts +0 -71
- package/src/services/aiService.ts +0 -919
- package/src/services/authService.ts +0 -565
- package/src/services/autoMemoryService.ts +0 -280
- package/src/services/configurationService.ts +0 -1520
- package/src/services/fileWatcher.ts +0 -319
- package/src/services/hook.ts +0 -348
- package/src/services/initializationService.ts +0 -357
- package/src/services/interactionService.ts +0 -229
- package/src/services/jsonlHandler.ts +0 -295
- package/src/services/memory.ts +0 -234
- package/src/services/pluginLoader.ts +0 -252
- package/src/services/remoteSettingsService.ts +0 -366
- package/src/services/reversionService.ts +0 -122
- package/src/services/session.ts +0 -922
- package/src/services/taskManager.ts +0 -321
- package/src/telemetry/events.ts +0 -65
- package/src/telemetry/instrumentation.ts +0 -499
- package/src/telemetry/sessionTracing.ts +0 -348
- package/src/tools/agentTool.ts +0 -322
- package/src/tools/askUserQuestion.ts +0 -160
- package/src/tools/bashTool.ts +0 -621
- package/src/tools/buildTool.ts +0 -61
- package/src/tools/cronCreateTool.ts +0 -161
- package/src/tools/cronDeleteTool.ts +0 -51
- package/src/tools/cronListTool.ts +0 -47
- package/src/tools/editTool.ts +0 -313
- package/src/tools/enterPlanMode.ts +0 -124
- package/src/tools/enterWorktreeTool.ts +0 -190
- package/src/tools/exitPlanMode.ts +0 -139
- package/src/tools/exitWorktreeTool.ts +0 -236
- package/src/tools/globTool.ts +0 -167
- package/src/tools/grepTool.ts +0 -399
- package/src/tools/lspTool.ts +0 -883
- package/src/tools/readTool.ts +0 -426
- package/src/tools/skillTool.ts +0 -248
- package/src/tools/taskManagementTools.ts +0 -574
- package/src/tools/taskStopTool.ts +0 -78
- package/src/tools/types.ts +0 -137
- package/src/tools/webFetchTool.ts +0 -373
- package/src/tools/workflowTool.ts +0 -205
- package/src/tools/writeTool.ts +0 -267
- package/src/types/agent.ts +0 -128
- package/src/types/auth.ts +0 -19
- package/src/types/commands.ts +0 -30
- package/src/types/config.ts +0 -43
- package/src/types/configuration.ts +0 -168
- package/src/types/core.ts +0 -105
- package/src/types/cron.ts +0 -13
- package/src/types/environment.ts +0 -106
- package/src/types/fileSearch.ts +0 -4
- package/src/types/history.ts +0 -7
- package/src/types/hooks.ts +0 -256
- package/src/types/index.ts +0 -41
- package/src/types/lsp.ts +0 -98
- package/src/types/marketplace.ts +0 -71
- package/src/types/mcp.ts +0 -43
- package/src/types/memoryRule.ts +0 -31
- package/src/types/messaging.ts +0 -114
- package/src/types/permissions.ts +0 -80
- package/src/types/plugins.ts +0 -39
- package/src/types/processes.ts +0 -99
- package/src/types/reversion.ts +0 -29
- package/src/types/session.ts +0 -12
- package/src/types/skills.ts +0 -108
- package/src/types/tasks.ts +0 -13
- package/src/types/telemetry.ts +0 -98
- package/src/types/tools.ts +0 -44
- package/src/types/workflow.ts +0 -6
- package/src/utils/abortUtils.ts +0 -118
- package/src/utils/atomicWrite.ts +0 -61
- package/src/utils/bashParser.ts +0 -899
- package/src/utils/cacheControlUtils.ts +0 -426
- package/src/utils/commandArgumentParser.ts +0 -161
- package/src/utils/commandPathResolver.ts +0 -88
- package/src/utils/configPaths.ts +0 -220
- package/src/utils/configValidator.ts +0 -100
- package/src/utils/constants.ts +0 -39
- package/src/utils/container.ts +0 -92
- package/src/utils/containerSetup.ts +0 -371
- package/src/utils/convertMessagesForAPI.ts +0 -326
- package/src/utils/cronTasks.ts +0 -128
- package/src/utils/cronTasksLock.ts +0 -182
- package/src/utils/cronToHuman.ts +0 -99
- package/src/utils/customCommands.ts +0 -95
- package/src/utils/editUtils.ts +0 -20
- package/src/utils/fileFormat.ts +0 -40
- package/src/utils/fileSearch.ts +0 -151
- package/src/utils/fileUtils.ts +0 -223
- package/src/utils/gitUtils.ts +0 -315
- package/src/utils/globalLogger.ts +0 -128
- package/src/utils/groupMessagesByApiRound.ts +0 -121
- package/src/utils/hookMatcher.ts +0 -176
- package/src/utils/markdownParser.ts +0 -317
- package/src/utils/mcpUtils.ts +0 -138
- package/src/utils/messageOperations.ts +0 -641
- package/src/utils/modelCapabilities.ts +0 -30
- package/src/utils/nameGenerator.ts +0 -99
- package/src/utils/notificationXml.ts +0 -57
- package/src/utils/openaiClient.ts +0 -251
- package/src/utils/parseCronExpression.ts +0 -78
- package/src/utils/path.ts +0 -72
- package/src/utils/pathEncoder.ts +0 -400
- package/src/utils/pathSafety.ts +0 -40
- package/src/utils/promptHistory.ts +0 -168
- package/src/utils/ripgrep.ts +0 -35
- package/src/utils/shellResolver.ts +0 -190
- package/src/utils/skillParser.ts +0 -246
- package/src/utils/stringUtils.ts +0 -156
- package/src/utils/subagentParser.ts +0 -279
- package/src/utils/taskReminder.ts +0 -96
- package/src/utils/tokenCalculation.ts +0 -43
- package/src/utils/tokenEstimate.ts +0 -34
- package/src/utils/toolResultStorage.ts +0 -117
- package/src/utils/worktreeSession.ts +0 -26
- package/src/utils/worktreeUtils.ts +0 -786
- package/src/workflow/budgetTracker.ts +0 -34
- package/src/workflow/concurrencyLimiter.ts +0 -47
- package/src/workflow/journal.ts +0 -95
- package/src/workflow/progressReporter.ts +0 -141
- package/src/workflow/runState.ts +0 -65
- package/src/workflow/scriptRuntime.ts +0 -274
- package/src/workflow/structuredOutput.ts +0 -123
- package/src/workflow/types.ts +0 -101
- package/src/workflow/workflowApis.ts +0 -410
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/linux-x86_64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
|
@@ -1,1010 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
-
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
6
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7
|
-
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
8
|
-
import { ChatCompletionFunctionTool } from "openai/resources.js";
|
|
9
|
-
import { createMcpToolPlugin, findToolServer } from "../utils/mcpUtils.js";
|
|
10
|
-
import type { ToolPlugin, ToolResult, ToolContext } from "../tools/types.js";
|
|
11
|
-
import { Container } from "../utils/container.js";
|
|
12
|
-
import type { ConfigurationService } from "../services/configurationService.js";
|
|
13
|
-
import type {
|
|
14
|
-
Logger,
|
|
15
|
-
McpServerConfig,
|
|
16
|
-
McpConfig,
|
|
17
|
-
McpTool,
|
|
18
|
-
McpServerStatus,
|
|
19
|
-
} from "../types/index.js";
|
|
20
|
-
|
|
21
|
-
interface McpConnection {
|
|
22
|
-
client: Client;
|
|
23
|
-
transport: Transport;
|
|
24
|
-
process: null; // StdioClientTransport manages process internally
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface McpManagerCallbacks {
|
|
28
|
-
onMcpServersChange?: (servers: McpServerStatus[]) => void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
import { logger } from "../utils/globalLogger.js";
|
|
32
|
-
|
|
33
|
-
export interface McpManagerOptions {
|
|
34
|
-
callbacks?: McpManagerCallbacks;
|
|
35
|
-
logger?: Logger;
|
|
36
|
-
/** Pre-configured MCP servers passed from constructor options */
|
|
37
|
-
mcpServers?: Record<string, McpServerConfig>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Expand environment variables in a string value.
|
|
42
|
-
* Supports ${VAR} and ${VAR:-default} patterns.
|
|
43
|
-
*/
|
|
44
|
-
const WAVE_TEMPLATE_VARS = ["WAVE_PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT"];
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Expand environment variables in a string value.
|
|
48
|
-
* Supports ${VAR} and ${VAR:-default} patterns.
|
|
49
|
-
*
|
|
50
|
-
* @param env - Merged env to resolve against (session snapshot over OS env).
|
|
51
|
-
* Defaults to `process.env` for backward compatibility with tests
|
|
52
|
-
* and standalone callers.
|
|
53
|
-
*/
|
|
54
|
-
export function expandEnvVars(
|
|
55
|
-
value: string,
|
|
56
|
-
env: Record<string, string | undefined> = process.env as Record<
|
|
57
|
-
string,
|
|
58
|
-
string | undefined
|
|
59
|
-
>,
|
|
60
|
-
): string {
|
|
61
|
-
return value.replace(/\$\{([^}]+)\}/g, (_match, expr: string) => {
|
|
62
|
-
const [varName, ...rest] = expr.split(":-");
|
|
63
|
-
const defaultValue = rest.join(":-");
|
|
64
|
-
// Skip Wave-specific template variables — they are handled at spawn time
|
|
65
|
-
if (WAVE_TEMPLATE_VARS.includes(varName)) {
|
|
66
|
-
return _match; // return original ${...} string untouched
|
|
67
|
-
}
|
|
68
|
-
return env[varName] ?? process.env[varName] ?? defaultValue;
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Walk an MCP config and resolve environment variables in all string fields.
|
|
74
|
-
* Expands ${VAR} against the session env snapshot (over OS env); falls back to
|
|
75
|
-
* process.env. WAVE_PLUGIN_ROOT is skipped — handled at spawn time.
|
|
76
|
-
*/
|
|
77
|
-
export function resolveMcpConfig(
|
|
78
|
-
config: McpConfig,
|
|
79
|
-
env: Record<string, string | undefined> = process.env as Record<
|
|
80
|
-
string,
|
|
81
|
-
string | undefined
|
|
82
|
-
>,
|
|
83
|
-
): McpConfig {
|
|
84
|
-
const resolved: McpConfig = { mcpServers: {} };
|
|
85
|
-
|
|
86
|
-
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
87
|
-
const resolvedServer: McpServerConfig = { ...serverConfig };
|
|
88
|
-
|
|
89
|
-
if (resolvedServer.command) {
|
|
90
|
-
resolvedServer.command = expandEnvVars(resolvedServer.command, env);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (resolvedServer.args) {
|
|
94
|
-
resolvedServer.args = resolvedServer.args.map((a) =>
|
|
95
|
-
expandEnvVars(a, env),
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (resolvedServer.env) {
|
|
100
|
-
const resolvedEnv: Record<string, string> = {};
|
|
101
|
-
for (const [key, val] of Object.entries(resolvedServer.env)) {
|
|
102
|
-
resolvedEnv[key] = expandEnvVars(val, env);
|
|
103
|
-
}
|
|
104
|
-
resolvedServer.env = resolvedEnv;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (resolvedServer.url) {
|
|
108
|
-
resolvedServer.url = expandEnvVars(resolvedServer.url, env);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (resolvedServer.headers) {
|
|
112
|
-
const resolvedHeaders: Record<string, string> = {};
|
|
113
|
-
for (const [key, val] of Object.entries(resolvedServer.headers)) {
|
|
114
|
-
resolvedHeaders[key] = expandEnvVars(val, env);
|
|
115
|
-
}
|
|
116
|
-
resolvedServer.headers = resolvedHeaders;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
resolved.mcpServers[name] = resolvedServer;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return resolved;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export class McpManager {
|
|
126
|
-
private config: McpConfig | null = null;
|
|
127
|
-
private servers: Map<string, McpServerStatus> = new Map();
|
|
128
|
-
private connections: Map<string, McpConnection> = new Map();
|
|
129
|
-
private configPath: string = "";
|
|
130
|
-
private workdir: string = "";
|
|
131
|
-
private callbacks: McpManagerCallbacks;
|
|
132
|
-
private mcpServers: Record<string, McpServerConfig> | undefined;
|
|
133
|
-
|
|
134
|
-
private reconnectTimers: Map<string, NodeJS.Timeout> = new Map();
|
|
135
|
-
private reconnectAttempts: Map<string, number> = new Map();
|
|
136
|
-
|
|
137
|
-
constructor(
|
|
138
|
-
private container: Container,
|
|
139
|
-
options: McpManagerOptions = {},
|
|
140
|
-
) {
|
|
141
|
-
this.callbacks = options.callbacks || {};
|
|
142
|
-
this.mcpServers = options.mcpServers;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Merged env for this session: OS env overlaid with the per-session settings
|
|
147
|
-
* snapshot. MCP env-var expansion (${VAR}) resolves against this so multiple
|
|
148
|
-
* sessions in one `wave --stdio` process don't read each other's settings env.
|
|
149
|
-
*/
|
|
150
|
-
private get envSnapshot(): Record<string, string> {
|
|
151
|
-
return (
|
|
152
|
-
this.container
|
|
153
|
-
.get<ConfigurationService>("ConfigurationService")
|
|
154
|
-
?.getMergedEnv?.() ?? (process.env as Record<string, string>)
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Initialize MCP manager with working directory and optionally auto-connect
|
|
160
|
-
*/
|
|
161
|
-
async initialize(
|
|
162
|
-
workdir: string,
|
|
163
|
-
autoConnect: boolean = false,
|
|
164
|
-
): Promise<void> {
|
|
165
|
-
this.configPath = join(workdir, ".mcp.json");
|
|
166
|
-
this.workdir = workdir;
|
|
167
|
-
|
|
168
|
-
// Register constructor-provided servers before loading .mcp.json
|
|
169
|
-
if (this.mcpServers) {
|
|
170
|
-
for (const [name, config] of Object.entries(this.mcpServers)) {
|
|
171
|
-
this.addServer(name, config);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (autoConnect) {
|
|
176
|
-
logger?.debug("Initializing MCP servers...");
|
|
177
|
-
|
|
178
|
-
// Load workspace MCP configuration (always read, merge with any plugin servers already added)
|
|
179
|
-
await this.loadConfig();
|
|
180
|
-
|
|
181
|
-
const config = this.config;
|
|
182
|
-
|
|
183
|
-
if (config && config.mcpServers) {
|
|
184
|
-
// Connect to all configured servers in background to avoid blocking agent initialization
|
|
185
|
-
Object.keys(config.mcpServers).forEach((serverName) => {
|
|
186
|
-
logger?.debug(`Connecting to MCP server: ${serverName}`);
|
|
187
|
-
this.connectServer(serverName)
|
|
188
|
-
.then((success) => {
|
|
189
|
-
if (success) {
|
|
190
|
-
logger?.debug(
|
|
191
|
-
`Successfully connected to MCP server: ${serverName}`,
|
|
192
|
-
);
|
|
193
|
-
} else {
|
|
194
|
-
logger?.warn(`Failed to connect to MCP server: ${serverName}`);
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
.catch((error) => {
|
|
198
|
-
logger?.error(
|
|
199
|
-
`Background connection to MCP server ${serverName} failed:`,
|
|
200
|
-
error,
|
|
201
|
-
);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
logger?.debug("MCP servers initialization started in background");
|
|
207
|
-
// Trigger state change callback after starting initialization
|
|
208
|
-
this.callbacks.onMcpServersChange?.(this.getAllServers());
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async ensureConfigLoaded(): Promise<McpConfig | null> {
|
|
213
|
-
if (!this.config) {
|
|
214
|
-
return this.loadConfig();
|
|
215
|
-
}
|
|
216
|
-
return this.config;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async loadConfig(): Promise<McpConfig | null> {
|
|
220
|
-
if (!this.configPath) {
|
|
221
|
-
logger?.warn("MCP config path not set. Call initialize() first.");
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
const configContent = await fs.readFile(this.configPath, "utf-8");
|
|
227
|
-
const rawConfig: McpConfig = JSON.parse(configContent);
|
|
228
|
-
const workspaceConfig = resolveMcpConfig(rawConfig, this.envSnapshot);
|
|
229
|
-
|
|
230
|
-
// Extract original (pre-resolution) URLs for safe display
|
|
231
|
-
const originalUrls: Record<string, string | undefined> = {};
|
|
232
|
-
for (const [name, serverConfig] of Object.entries(rawConfig.mcpServers)) {
|
|
233
|
-
originalUrls[name] = serverConfig.url;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Merge workspace config with any existing config (e.g., from plugins or constructor)
|
|
237
|
-
// Constructor-provided servers take precedence, then workspace config, then existing config
|
|
238
|
-
const merged: McpConfig = { mcpServers: {} };
|
|
239
|
-
if (this.config) {
|
|
240
|
-
Object.assign(merged.mcpServers, this.config.mcpServers);
|
|
241
|
-
}
|
|
242
|
-
Object.assign(merged.mcpServers, workspaceConfig.mcpServers);
|
|
243
|
-
// Constructor-provided servers override both for same names
|
|
244
|
-
if (this.mcpServers) {
|
|
245
|
-
Object.assign(merged.mcpServers, this.mcpServers);
|
|
246
|
-
}
|
|
247
|
-
this.config = merged;
|
|
248
|
-
|
|
249
|
-
// Initialize server statuses (preserve existing status for already known servers)
|
|
250
|
-
if (this.config) {
|
|
251
|
-
for (const [name, config] of Object.entries(this.config.mcpServers)) {
|
|
252
|
-
const existingServer = this.servers.get(name);
|
|
253
|
-
|
|
254
|
-
if (existingServer) {
|
|
255
|
-
// Update config but preserve status and other runtime info
|
|
256
|
-
this.servers.set(name, {
|
|
257
|
-
...existingServer,
|
|
258
|
-
config, // Update config in case it changed
|
|
259
|
-
originalUrl: originalUrls[name] ?? existingServer.originalUrl,
|
|
260
|
-
});
|
|
261
|
-
} else {
|
|
262
|
-
// New server, initialize with disconnected status
|
|
263
|
-
this.servers.set(name, {
|
|
264
|
-
name,
|
|
265
|
-
config,
|
|
266
|
-
originalUrl: originalUrls[name],
|
|
267
|
-
status: "disconnected",
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return this.config;
|
|
274
|
-
} catch (error) {
|
|
275
|
-
// Only log error if it's not a "file not found" error
|
|
276
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
277
|
-
logger?.error("Failed to load .mcp.json:", error);
|
|
278
|
-
}
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async saveConfig(config: McpConfig): Promise<boolean> {
|
|
284
|
-
try {
|
|
285
|
-
await fs.writeFile(this.configPath, JSON.stringify(config, null, 2));
|
|
286
|
-
this.config = config;
|
|
287
|
-
return true;
|
|
288
|
-
} catch (error) {
|
|
289
|
-
logger?.error("Failed to save .mcp.json:", error);
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
getConfig(): McpConfig | null {
|
|
295
|
-
return this.config;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
getAllServers(): McpServerStatus[] {
|
|
299
|
-
return Array.from(this.servers.values());
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
getServer(name: string): McpServerStatus | undefined {
|
|
303
|
-
return this.servers.get(name);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
updateServerStatus(name: string, updates: Partial<McpServerStatus>): void {
|
|
307
|
-
const server = this.servers.get(name);
|
|
308
|
-
if (server) {
|
|
309
|
-
this.servers.set(name, { ...server, ...updates });
|
|
310
|
-
// Trigger state change callback
|
|
311
|
-
this.callbacks.onMcpServersChange?.(this.getAllServers());
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
addServer(name: string, config: McpServerConfig): boolean {
|
|
316
|
-
if (this.servers.has(name)) {
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Capture original URL before any resolution for safe display
|
|
321
|
-
const originalUrl = config.url;
|
|
322
|
-
|
|
323
|
-
// Expand env vars against the session snapshot (over OS env, e.g. ${TAVILY_API_KEY})
|
|
324
|
-
const env = this.envSnapshot;
|
|
325
|
-
const resolvedConfig: McpServerConfig = { ...config };
|
|
326
|
-
if (resolvedConfig.command) {
|
|
327
|
-
resolvedConfig.command = expandEnvVars(resolvedConfig.command, env);
|
|
328
|
-
}
|
|
329
|
-
if (resolvedConfig.args) {
|
|
330
|
-
resolvedConfig.args = resolvedConfig.args.map((a) =>
|
|
331
|
-
expandEnvVars(a, env),
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
if (resolvedConfig.env) {
|
|
335
|
-
const resolvedEnv: Record<string, string> = {};
|
|
336
|
-
for (const [key, val] of Object.entries(resolvedConfig.env)) {
|
|
337
|
-
resolvedEnv[key] = expandEnvVars(val, env);
|
|
338
|
-
}
|
|
339
|
-
resolvedConfig.env = resolvedEnv;
|
|
340
|
-
}
|
|
341
|
-
if (resolvedConfig.url) {
|
|
342
|
-
resolvedConfig.url = expandEnvVars(resolvedConfig.url, env);
|
|
343
|
-
}
|
|
344
|
-
if (resolvedConfig.headers) {
|
|
345
|
-
const resolvedHeaders: Record<string, string> = {};
|
|
346
|
-
for (const [key, val] of Object.entries(resolvedConfig.headers)) {
|
|
347
|
-
resolvedHeaders[key] = expandEnvVars(val, env);
|
|
348
|
-
}
|
|
349
|
-
resolvedConfig.headers = resolvedHeaders;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const newServer: McpServerStatus = {
|
|
353
|
-
name,
|
|
354
|
-
config: resolvedConfig,
|
|
355
|
-
originalUrl,
|
|
356
|
-
status: "disconnected",
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
this.servers.set(name, newServer);
|
|
360
|
-
|
|
361
|
-
// Update config
|
|
362
|
-
if (this.config) {
|
|
363
|
-
this.config.mcpServers[name] = resolvedConfig;
|
|
364
|
-
} else {
|
|
365
|
-
this.config = {
|
|
366
|
-
mcpServers: { [name]: resolvedConfig },
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return true;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
removeServer(name: string): boolean {
|
|
374
|
-
// Disconnect if connected
|
|
375
|
-
if (this.connections.has(name)) {
|
|
376
|
-
this.disconnectServer(name);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const removed = this.servers.delete(name);
|
|
380
|
-
|
|
381
|
-
if (removed && this.config) {
|
|
382
|
-
delete this.config.mcpServers[name];
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return removed;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Real MCP connection implementation
|
|
389
|
-
async connectServer(name: string): Promise<boolean> {
|
|
390
|
-
const server = this.servers.get(name);
|
|
391
|
-
if (!server) return false;
|
|
392
|
-
|
|
393
|
-
// Already connected
|
|
394
|
-
if (this.connections.has(name)) return true;
|
|
395
|
-
|
|
396
|
-
this.updateServerStatus(name, { status: "connecting" });
|
|
397
|
-
|
|
398
|
-
try {
|
|
399
|
-
// Create transport - it will manage the process
|
|
400
|
-
let transport: Transport;
|
|
401
|
-
let client: Client;
|
|
402
|
-
let tools: McpTool[] = [];
|
|
403
|
-
|
|
404
|
-
const createClient = () =>
|
|
405
|
-
new Client(
|
|
406
|
-
{
|
|
407
|
-
name: "wave-code",
|
|
408
|
-
version: "1.0.0",
|
|
409
|
-
},
|
|
410
|
-
{
|
|
411
|
-
capabilities: {},
|
|
412
|
-
},
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
const serverType = server.config.type;
|
|
416
|
-
|
|
417
|
-
if (serverType === "http" || (!serverType && server.config.url)) {
|
|
418
|
-
if (!server.config.url) {
|
|
419
|
-
throw new Error(
|
|
420
|
-
`MCP server ${name} with type "http" requires a 'url'`,
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
const url = new URL(server.config.url);
|
|
424
|
-
const headers = server.config.headers;
|
|
425
|
-
logger?.debug(
|
|
426
|
-
`Connecting to MCP server ${name} using Streamable HTTP at ${url.href}`,
|
|
427
|
-
);
|
|
428
|
-
transport = new StreamableHTTPClientTransport(url, {
|
|
429
|
-
requestInit: { headers },
|
|
430
|
-
});
|
|
431
|
-
client = createClient();
|
|
432
|
-
await client.connect(transport);
|
|
433
|
-
const toolsResponse = await client.listTools();
|
|
434
|
-
tools =
|
|
435
|
-
toolsResponse.tools?.map((tool) => ({
|
|
436
|
-
name: tool.name,
|
|
437
|
-
description: tool.description,
|
|
438
|
-
inputSchema: tool.inputSchema,
|
|
439
|
-
})) || [];
|
|
440
|
-
logger?.info(`Connected to MCP server ${name} using Streamable HTTP`);
|
|
441
|
-
} else if (serverType === "sse") {
|
|
442
|
-
if (!server.config.url) {
|
|
443
|
-
throw new Error(
|
|
444
|
-
`MCP server ${name} with type "sse" requires a 'url'`,
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
const url = new URL(server.config.url);
|
|
448
|
-
const headers = server.config.headers;
|
|
449
|
-
logger?.debug(
|
|
450
|
-
`Connecting to MCP server ${name} using SSE at ${url.href}`,
|
|
451
|
-
);
|
|
452
|
-
transport = new SSEClientTransport(url, {
|
|
453
|
-
requestInit: { headers },
|
|
454
|
-
});
|
|
455
|
-
client = createClient();
|
|
456
|
-
await client.connect(transport);
|
|
457
|
-
const toolsResponse = await client.listTools();
|
|
458
|
-
tools =
|
|
459
|
-
toolsResponse.tools?.map((tool) => ({
|
|
460
|
-
name: tool.name,
|
|
461
|
-
description: tool.description,
|
|
462
|
-
inputSchema: tool.inputSchema,
|
|
463
|
-
})) || [];
|
|
464
|
-
logger?.info(`Connected to MCP server ${name} using SSE`);
|
|
465
|
-
} else if (
|
|
466
|
-
serverType === "stdio" ||
|
|
467
|
-
(!serverType && server.config.command)
|
|
468
|
-
) {
|
|
469
|
-
if (!server.config.command) {
|
|
470
|
-
throw new Error(
|
|
471
|
-
`MCP server ${name} with type "stdio" requires a 'command'`,
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Base env = OS env overlaid with this session's settings snapshot, so
|
|
476
|
-
// custom env vars from settings.json reach the MCP server subprocess
|
|
477
|
-
// without polluting other sessions sharing one `wave --stdio` process.
|
|
478
|
-
const env: Record<string, string> = {
|
|
479
|
-
...this.envSnapshot,
|
|
480
|
-
...(server.config.env || {}),
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
// For plugin servers, substitute ${WAVE_PLUGIN_ROOT} and ${CLAUDE_PLUGIN_ROOT}
|
|
484
|
-
let command = server.config.command;
|
|
485
|
-
let args = server.config.args || [];
|
|
486
|
-
if (server.config.pluginRoot) {
|
|
487
|
-
env.WAVE_PLUGIN_ROOT = server.config.pluginRoot;
|
|
488
|
-
env.CLAUDE_PLUGIN_ROOT = server.config.pluginRoot;
|
|
489
|
-
command = command.replace(
|
|
490
|
-
/\$\{WAVE_PLUGIN_ROOT\}/g,
|
|
491
|
-
server.config.pluginRoot,
|
|
492
|
-
);
|
|
493
|
-
command = command.replace(
|
|
494
|
-
/\$\{CLAUDE_PLUGIN_ROOT\}/g,
|
|
495
|
-
server.config.pluginRoot,
|
|
496
|
-
);
|
|
497
|
-
args = args.map((arg) => {
|
|
498
|
-
let result = arg.replace(
|
|
499
|
-
/\$\{WAVE_PLUGIN_ROOT\}/g,
|
|
500
|
-
server.config.pluginRoot!,
|
|
501
|
-
);
|
|
502
|
-
result = result.replace(
|
|
503
|
-
/\$\{CLAUDE_PLUGIN_ROOT\}/g,
|
|
504
|
-
server.config.pluginRoot!,
|
|
505
|
-
);
|
|
506
|
-
return result;
|
|
507
|
-
});
|
|
508
|
-
// Also expand plugin root in user-provided env values
|
|
509
|
-
for (const [key, value] of Object.entries(server.config.env || {})) {
|
|
510
|
-
let expanded = value.replace(
|
|
511
|
-
/\$\{WAVE_PLUGIN_ROOT\}/g,
|
|
512
|
-
server.config.pluginRoot!,
|
|
513
|
-
);
|
|
514
|
-
expanded = expanded.replace(
|
|
515
|
-
/\$\{CLAUDE_PLUGIN_ROOT\}/g,
|
|
516
|
-
server.config.pluginRoot!,
|
|
517
|
-
);
|
|
518
|
-
env[key] = expanded;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
transport = new StdioClientTransport({
|
|
522
|
-
command,
|
|
523
|
-
args,
|
|
524
|
-
env,
|
|
525
|
-
cwd: this.workdir, // Use the agent's workdir as the process working directory
|
|
526
|
-
stderr: "pipe", // Pipe stderr to capture it
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
// Buffer stderr silently (MCP servers use stderr for all logging;
|
|
530
|
-
// stdout is reserved for JSON-RPC). Only dump it on connection
|
|
531
|
-
// success (once) or failure — not line-by-line in real time.
|
|
532
|
-
// Aligns with Claude Code's approach to avoid noise from [INFO],
|
|
533
|
-
// [DEBUG], Warning: lines that are not actual errors.
|
|
534
|
-
const stderr = (transport as StdioClientTransport).stderr;
|
|
535
|
-
let stderrOutput = "";
|
|
536
|
-
if (stderr) {
|
|
537
|
-
stderr.on("data", (chunk: Buffer) => {
|
|
538
|
-
if (stderrOutput.length < 64 * 1024 * 1024) {
|
|
539
|
-
stderrOutput += chunk.toString();
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
client = createClient();
|
|
545
|
-
try {
|
|
546
|
-
await client.connect(transport);
|
|
547
|
-
} catch (error) {
|
|
548
|
-
if (stderrOutput.trim()) {
|
|
549
|
-
logger?.error(
|
|
550
|
-
`[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`,
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
throw error;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Dump accumulated stderr once after successful connection
|
|
557
|
-
if (stderrOutput.trim()) {
|
|
558
|
-
logger?.debug(
|
|
559
|
-
`[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`,
|
|
560
|
-
);
|
|
561
|
-
stderrOutput = "";
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const toolsResponse = await client.listTools();
|
|
565
|
-
tools =
|
|
566
|
-
toolsResponse.tools?.map((tool) => ({
|
|
567
|
-
name: tool.name,
|
|
568
|
-
description: tool.description,
|
|
569
|
-
inputSchema: tool.inputSchema,
|
|
570
|
-
})) || [];
|
|
571
|
-
} else if (serverType) {
|
|
572
|
-
// Unknown type value
|
|
573
|
-
throw new Error(
|
|
574
|
-
`MCP server ${name} has unknown type "${serverType}". Must be "stdio", "sse", or "http"`,
|
|
575
|
-
);
|
|
576
|
-
} else {
|
|
577
|
-
throw new Error(
|
|
578
|
-
`MCP server ${name} configuration must include either 'command' or 'url'`,
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// Handle transport errors (e.g. SSE stream disconnected)
|
|
583
|
-
// The SDK auto-reconnects for transient SSE errors, so we use
|
|
584
|
-
// "reconnecting" status instead of "error". We then poll listTools()
|
|
585
|
-
// to detect when reconnection succeeds.
|
|
586
|
-
transport.onerror = (error: Error) => {
|
|
587
|
-
const isTransient = error.message?.includes("SSE stream disconnected");
|
|
588
|
-
if (isTransient) {
|
|
589
|
-
logger?.warn(
|
|
590
|
-
`MCP Server ${name} transient transport error (SDK will auto-reconnect): ${error.message}`,
|
|
591
|
-
);
|
|
592
|
-
this.updateServerStatus(name, {
|
|
593
|
-
status: "reconnecting",
|
|
594
|
-
error: error.message,
|
|
595
|
-
});
|
|
596
|
-
// Poll to detect when the SDK's auto-reconnect succeeds
|
|
597
|
-
this.pollReconnectRecovery(name, client);
|
|
598
|
-
} else {
|
|
599
|
-
logger?.error(`MCP Server ${name} transport error:`, error);
|
|
600
|
-
this.updateServerStatus(name, {
|
|
601
|
-
status: "error",
|
|
602
|
-
error: error.message,
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
transport.onclose = () => {
|
|
608
|
-
logger?.debug(`MCP Server ${name} transport closed`);
|
|
609
|
-
this.connections.delete(name);
|
|
610
|
-
this.updateServerStatus(name, {
|
|
611
|
-
status: "disconnected",
|
|
612
|
-
tools: [],
|
|
613
|
-
toolCount: 0,
|
|
614
|
-
});
|
|
615
|
-
// Auto-reconnect with exponential backoff (not triggered by explicit disconnect)
|
|
616
|
-
if (!this.reconnectTimers.has(name)) {
|
|
617
|
-
this.scheduleReconnect(name);
|
|
618
|
-
}
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
// Store connection
|
|
622
|
-
this.connections.set(name, {
|
|
623
|
-
client,
|
|
624
|
-
transport,
|
|
625
|
-
process: null,
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
// Update status
|
|
629
|
-
this.updateServerStatus(name, {
|
|
630
|
-
status: "connected",
|
|
631
|
-
tools,
|
|
632
|
-
toolCount: tools.length,
|
|
633
|
-
capabilities: ["tools"],
|
|
634
|
-
lastConnected: Date.now(),
|
|
635
|
-
error: undefined,
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
return true;
|
|
639
|
-
} catch (error) {
|
|
640
|
-
logger?.error(`Failed to connect to MCP server ${name}:`, error);
|
|
641
|
-
// updateServerStatus will trigger the callback
|
|
642
|
-
this.updateServerStatus(name, {
|
|
643
|
-
status: "error",
|
|
644
|
-
error: error instanceof Error ? error.message : String(error),
|
|
645
|
-
});
|
|
646
|
-
return false;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* Schedule auto-reconnect with exponential backoff.
|
|
652
|
-
* Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s, ...
|
|
653
|
-
*/
|
|
654
|
-
private scheduleReconnect(name: string): void {
|
|
655
|
-
const attempts = this.reconnectAttempts.get(name) ?? 0;
|
|
656
|
-
const delay = Math.min(1000 * Math.pow(2, attempts), 30000);
|
|
657
|
-
this.reconnectAttempts.set(name, attempts + 1);
|
|
658
|
-
|
|
659
|
-
logger?.info(
|
|
660
|
-
`Scheduling MCP server ${name} reconnect in ${delay}ms (attempt ${attempts + 1})`,
|
|
661
|
-
);
|
|
662
|
-
|
|
663
|
-
const timer = setTimeout(() => {
|
|
664
|
-
this.reconnectTimers.delete(name);
|
|
665
|
-
logger?.debug(`Auto-reconnecting MCP server: ${name}`);
|
|
666
|
-
this.connectServer(name)
|
|
667
|
-
.then((success) => {
|
|
668
|
-
if (success) {
|
|
669
|
-
logger?.info(`Auto-reconnected MCP server: ${name}`);
|
|
670
|
-
this.reconnectAttempts.delete(name);
|
|
671
|
-
} else {
|
|
672
|
-
logger?.warn(`Auto-reconnect failed for MCP server: ${name}`);
|
|
673
|
-
// Will be retried via onclose handler
|
|
674
|
-
}
|
|
675
|
-
})
|
|
676
|
-
.catch((error) => {
|
|
677
|
-
logger?.error(`Auto-reconnect error for MCP server ${name}:`, error);
|
|
678
|
-
});
|
|
679
|
-
}, delay);
|
|
680
|
-
|
|
681
|
-
this.reconnectTimers.set(name, timer);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Poll listTools() to detect when the SDK's auto-reconnect succeeds
|
|
686
|
-
* after a transient SSE disconnect. Restores status to "connected".
|
|
687
|
-
*/
|
|
688
|
-
private async pollReconnectRecovery(
|
|
689
|
-
name: string,
|
|
690
|
-
client: Client,
|
|
691
|
-
): Promise<void> {
|
|
692
|
-
const maxAttempts = 10;
|
|
693
|
-
const delay = 3000; // Match SDK's reconnection delay
|
|
694
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
695
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
696
|
-
const serverStatus = this.servers.get(name);
|
|
697
|
-
if (serverStatus?.status !== "reconnecting") {
|
|
698
|
-
return; // Status changed by onclose or user action — stop polling
|
|
699
|
-
}
|
|
700
|
-
try {
|
|
701
|
-
const toolsResponse = await client.listTools();
|
|
702
|
-
const tools =
|
|
703
|
-
toolsResponse.tools?.map((tool) => ({
|
|
704
|
-
name: tool.name,
|
|
705
|
-
description: tool.description,
|
|
706
|
-
inputSchema: tool.inputSchema,
|
|
707
|
-
})) || [];
|
|
708
|
-
logger?.info(
|
|
709
|
-
`MCP Server ${name} auto-reconnected successfully (attempt ${i + 1})`,
|
|
710
|
-
);
|
|
711
|
-
this.updateServerStatus(name, {
|
|
712
|
-
status: "connected",
|
|
713
|
-
tools,
|
|
714
|
-
toolCount: tools.length,
|
|
715
|
-
lastConnected: Date.now(),
|
|
716
|
-
error: undefined,
|
|
717
|
-
});
|
|
718
|
-
this.reconnectAttempts.delete(name);
|
|
719
|
-
return;
|
|
720
|
-
} catch {
|
|
721
|
-
logger?.debug(
|
|
722
|
-
`MCP Server ${name} reconnect recovery check ${i + 1}/${maxAttempts} failed`,
|
|
723
|
-
);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
// If we exhausted attempts, fall through to McpManager's own reconnect
|
|
727
|
-
logger?.warn(
|
|
728
|
-
`MCP Server ${name} SDK auto-reconnect did not recover after ${maxAttempts} attempts`,
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
private cancelReconnect(name: string): void {
|
|
733
|
-
const timer = this.reconnectTimers.get(name);
|
|
734
|
-
if (timer) {
|
|
735
|
-
clearTimeout(timer);
|
|
736
|
-
this.reconnectTimers.delete(name);
|
|
737
|
-
}
|
|
738
|
-
this.reconnectAttempts.delete(name);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
async disconnectServer(name: string): Promise<boolean> {
|
|
742
|
-
// Cancel any pending reconnect attempts
|
|
743
|
-
this.cancelReconnect(name);
|
|
744
|
-
|
|
745
|
-
const connection = this.connections.get(name);
|
|
746
|
-
if (!connection) return false;
|
|
747
|
-
|
|
748
|
-
try {
|
|
749
|
-
// Close client connection and transport
|
|
750
|
-
await connection.client.close();
|
|
751
|
-
await connection.transport.close();
|
|
752
|
-
|
|
753
|
-
// Remove connection
|
|
754
|
-
this.connections.delete(name);
|
|
755
|
-
|
|
756
|
-
// updateServerStatus will trigger the callback
|
|
757
|
-
this.updateServerStatus(name, {
|
|
758
|
-
status: "disconnected",
|
|
759
|
-
tools: [],
|
|
760
|
-
toolCount: 0,
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
return true;
|
|
764
|
-
} catch (error) {
|
|
765
|
-
logger?.error(`Error disconnecting from MCP server ${name}:`, error);
|
|
766
|
-
return false;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// Get all tools from connected servers.
|
|
771
|
-
// "reconnecting" servers retain their last-known tool snapshot so that
|
|
772
|
-
// transient SSE disconnects don't churn the tool list (which would bust
|
|
773
|
-
// the prompt cache). Tools are only dropped once the server truly fails
|
|
774
|
-
// (status "error"/"disconnected").
|
|
775
|
-
getAllConnectedTools(): McpTool[] {
|
|
776
|
-
const allTools: McpTool[] = [];
|
|
777
|
-
for (const server of this.servers.values()) {
|
|
778
|
-
const usable =
|
|
779
|
-
server.status === "connected" || server.status === "reconnecting";
|
|
780
|
-
if (usable && server.tools) {
|
|
781
|
-
allTools.push(...server.tools);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
return allTools;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// Execute MCP tool
|
|
788
|
-
async executeMcpTool(
|
|
789
|
-
toolName: string,
|
|
790
|
-
args: Record<string, unknown>,
|
|
791
|
-
context?: ToolContext,
|
|
792
|
-
): Promise<{
|
|
793
|
-
success: boolean;
|
|
794
|
-
content: string;
|
|
795
|
-
serverName?: string;
|
|
796
|
-
images?: Array<{ data: string; mediaType?: string }>;
|
|
797
|
-
}> {
|
|
798
|
-
// Check if it's a prefixed name: mcp__[serverName]__[toolName]
|
|
799
|
-
if (!toolName.startsWith("mcp__")) {
|
|
800
|
-
throw new Error(
|
|
801
|
-
`Invalid MCP tool name: ${toolName}. Must start with 'mcp__'`,
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// Permission check
|
|
806
|
-
if (context?.permissionManager) {
|
|
807
|
-
const permissionContext = context.permissionManager.createContext(
|
|
808
|
-
toolName,
|
|
809
|
-
context.permissionMode || "default",
|
|
810
|
-
context.canUseToolCallback,
|
|
811
|
-
args,
|
|
812
|
-
context.toolCallId,
|
|
813
|
-
);
|
|
814
|
-
|
|
815
|
-
const decision =
|
|
816
|
-
await context.permissionManager.checkPermission(permissionContext);
|
|
817
|
-
if (decision.behavior === "deny") {
|
|
818
|
-
throw new Error(decision.message || "Permission denied");
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
const parts = toolName.split("__");
|
|
823
|
-
if (parts.length < 3) {
|
|
824
|
-
throw new Error(
|
|
825
|
-
`Invalid MCP tool name format: ${toolName}. Expected 'mcp__[server]__[tool]'`,
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
const targetServerName = parts[1];
|
|
830
|
-
const actualToolName = parts.slice(2).join("__");
|
|
831
|
-
|
|
832
|
-
const server = this.servers.get(targetServerName);
|
|
833
|
-
if (server && server.status === "connected" && server.tools) {
|
|
834
|
-
const tool = server.tools.find((t) => t.name === actualToolName);
|
|
835
|
-
if (tool) {
|
|
836
|
-
const connection = this.connections.get(targetServerName);
|
|
837
|
-
if (connection) {
|
|
838
|
-
return this.executeToolOnConnection(
|
|
839
|
-
connection,
|
|
840
|
-
actualToolName,
|
|
841
|
-
args,
|
|
842
|
-
targetServerName,
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
throw new Error(`Tool ${toolName} not found on any connected MCP server`);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
private async executeToolOnConnection(
|
|
852
|
-
connection: McpConnection,
|
|
853
|
-
toolName: string,
|
|
854
|
-
args: Record<string, unknown>,
|
|
855
|
-
serverName: string,
|
|
856
|
-
): Promise<{
|
|
857
|
-
success: boolean;
|
|
858
|
-
content: string;
|
|
859
|
-
serverName?: string;
|
|
860
|
-
images?: Array<{ data: string; mediaType?: string }>;
|
|
861
|
-
}> {
|
|
862
|
-
try {
|
|
863
|
-
const result = await connection.client.callTool({
|
|
864
|
-
name: toolName,
|
|
865
|
-
arguments: args,
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
// Separate text content and image data
|
|
869
|
-
const textContent: string[] = [];
|
|
870
|
-
const images: Array<{ data: string; mediaType?: string }> = [];
|
|
871
|
-
|
|
872
|
-
if (Array.isArray(result.content)) {
|
|
873
|
-
result.content.forEach(
|
|
874
|
-
(c: {
|
|
875
|
-
type: string;
|
|
876
|
-
text?: string;
|
|
877
|
-
data?: string;
|
|
878
|
-
resource?: { uri: string };
|
|
879
|
-
[key: string]: unknown;
|
|
880
|
-
}) => {
|
|
881
|
-
if (c.type === "text") {
|
|
882
|
-
textContent.push(c.text || "");
|
|
883
|
-
} else if (c.type === "image" && c.data) {
|
|
884
|
-
images.push({
|
|
885
|
-
data: c.data,
|
|
886
|
-
mediaType: "image/png", // Default to PNG
|
|
887
|
-
});
|
|
888
|
-
} else if (c.type === "resource") {
|
|
889
|
-
textContent.push(`[Resource: ${c.resource?.uri || ""}]`);
|
|
890
|
-
} else {
|
|
891
|
-
textContent.push(JSON.stringify(c, null, 2));
|
|
892
|
-
}
|
|
893
|
-
},
|
|
894
|
-
);
|
|
895
|
-
} else if (result.content) {
|
|
896
|
-
textContent.push(String(result.content));
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
const textContentStr =
|
|
900
|
-
textContent.length > 0
|
|
901
|
-
? textContent.join("\n")
|
|
902
|
-
: images.length > 0
|
|
903
|
-
? `Tool returned ${images.length} image(s).`
|
|
904
|
-
: "No content";
|
|
905
|
-
|
|
906
|
-
return {
|
|
907
|
-
success: true,
|
|
908
|
-
content: textContentStr,
|
|
909
|
-
images: images.length > 0 ? images : undefined,
|
|
910
|
-
serverName,
|
|
911
|
-
};
|
|
912
|
-
} catch (error) {
|
|
913
|
-
throw new Error(
|
|
914
|
-
`Tool execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
915
|
-
);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// Cleanup all connections
|
|
920
|
-
async cleanup(): Promise<void> {
|
|
921
|
-
// Cancel all pending reconnect timers
|
|
922
|
-
for (const name of this.reconnectTimers.keys()) {
|
|
923
|
-
this.cancelReconnect(name);
|
|
924
|
-
}
|
|
925
|
-
const disconnectPromises = Array.from(this.connections.keys()).map((name) =>
|
|
926
|
-
this.disconnectServer(name),
|
|
927
|
-
);
|
|
928
|
-
await Promise.all(disconnectPromises);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// ========== Tools Registry Methods ==========
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Get all currently available MCP tools as plugins
|
|
935
|
-
*/
|
|
936
|
-
getMcpToolPlugins(): ToolPlugin[] {
|
|
937
|
-
const mcpTools = new Map<string, ToolPlugin>();
|
|
938
|
-
|
|
939
|
-
// Get all connected MCP tools
|
|
940
|
-
const connectedTools = this.getAllConnectedTools();
|
|
941
|
-
const servers = this.getAllServers();
|
|
942
|
-
|
|
943
|
-
// Find server name for each tool and create plugins
|
|
944
|
-
for (const tool of connectedTools) {
|
|
945
|
-
// Find which server this tool belongs to
|
|
946
|
-
const server = findToolServer(tool.name, servers);
|
|
947
|
-
|
|
948
|
-
if (server) {
|
|
949
|
-
const plugin = createMcpToolPlugin(
|
|
950
|
-
tool,
|
|
951
|
-
server.name,
|
|
952
|
-
(
|
|
953
|
-
name: string,
|
|
954
|
-
args: Record<string, unknown>,
|
|
955
|
-
context?: ToolContext,
|
|
956
|
-
) => this.executeMcpTool(name, args, context),
|
|
957
|
-
);
|
|
958
|
-
mcpTools.set(plugin.name, plugin);
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
return Array.from(mcpTools.values());
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
/**
|
|
966
|
-
* Get all currently available MCP tools as OpenAI function tools
|
|
967
|
-
*/
|
|
968
|
-
getMcpToolsConfig(): ChatCompletionFunctionTool[] {
|
|
969
|
-
return this.getMcpToolPlugins().map((tool) => tool.config);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
/**
|
|
973
|
-
* Execute an MCP tool by name (registry version)
|
|
974
|
-
*/
|
|
975
|
-
async executeMcpToolByRegistry(
|
|
976
|
-
name: string,
|
|
977
|
-
args: Record<string, unknown>,
|
|
978
|
-
context: ToolContext,
|
|
979
|
-
): Promise<ToolResult> {
|
|
980
|
-
const plugins = this.getMcpToolPlugins();
|
|
981
|
-
const tool = plugins.find((plugin) => plugin.name === name);
|
|
982
|
-
|
|
983
|
-
if (!tool) {
|
|
984
|
-
return {
|
|
985
|
-
success: false,
|
|
986
|
-
content: "",
|
|
987
|
-
error: `MCP tool '${name}' not found or server disconnected`,
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
return tool.execute(args, context);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* Check if a tool name belongs to an MCP tool
|
|
995
|
-
*/
|
|
996
|
-
isMcpTool(name: string): boolean {
|
|
997
|
-
if (!name.startsWith("mcp__")) return false;
|
|
998
|
-
|
|
999
|
-
for (const server of this.servers.values()) {
|
|
1000
|
-
if (server.status === "connected" && server.tools) {
|
|
1001
|
-
for (const tool of server.tools) {
|
|
1002
|
-
if (`mcp__${server.name}__${tool.name}` === name) {
|
|
1003
|
-
return true;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
return false;
|
|
1009
|
-
}
|
|
1010
|
-
}
|