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.
Files changed (194) hide show
  1. package/dist/agent.d.ts +23 -1
  2. package/dist/agent.js +34 -4
  3. package/dist/managers/aiManager.d.ts +36 -0
  4. package/dist/managers/aiManager.js +240 -8
  5. package/dist/managers/messageManager.d.ts +1 -3
  6. package/dist/managers/messageManager.js +19 -9
  7. package/dist/managers/slashCommandManager.js +28 -0
  8. package/dist/managers/subagentManager.d.ts +0 -2
  9. package/dist/services/session.js +2 -9
  10. package/dist/telemetry/instrumentation.d.ts +1 -1
  11. package/dist/telemetry/instrumentation.js +57 -28
  12. package/dist/tools/bashTool.js +16 -6
  13. package/dist/tools/grepTool.js +11 -3
  14. package/dist/types/index.d.ts +3 -0
  15. package/dist/types/messaging.d.ts +2 -0
  16. package/dist/utils/containerSetup.js +0 -3
  17. package/dist/utils/messageOperations.d.ts +10 -1
  18. package/dist/utils/messageOperations.js +19 -1
  19. package/dist/utils/notificationXml.js +5 -0
  20. package/dist/utils/ripgrep.d.ts +4 -4
  21. package/dist/utils/ripgrep.js +4 -27
  22. package/package.json +20 -8
  23. package/bin/rg +0 -79
  24. package/dist/managers/forkedAgentManager.d.ts +0 -50
  25. package/dist/managers/forkedAgentManager.js +0 -130
  26. package/scripts/install_ripgrep.js +0 -111
  27. package/scripts/postinstall.js +0 -38
  28. package/src/agent.ts +0 -1271
  29. package/src/constants/subagents.ts +0 -4
  30. package/src/constants/toolLimits.ts +0 -15
  31. package/src/constants/tools.ts +0 -24
  32. package/src/core/plugin.ts +0 -237
  33. package/src/core/session.ts +0 -9
  34. package/src/index.ts +0 -38
  35. package/src/managers/MemoryRuleManager.ts +0 -198
  36. package/src/managers/aiManager.ts +0 -2429
  37. package/src/managers/backgroundTaskManager.ts +0 -480
  38. package/src/managers/bangManager.ts +0 -119
  39. package/src/managers/cronManager.ts +0 -401
  40. package/src/managers/foregroundTaskManager.ts +0 -30
  41. package/src/managers/forkedAgentManager.ts +0 -216
  42. package/src/managers/hookManager.ts +0 -1089
  43. package/src/managers/liveConfigManager.ts +0 -445
  44. package/src/managers/lspManager.ts +0 -470
  45. package/src/managers/mcpManager.ts +0 -1010
  46. package/src/managers/messageManager.ts +0 -1180
  47. package/src/managers/messageQueue.ts +0 -157
  48. package/src/managers/permissionManager.ts +0 -1140
  49. package/src/managers/planManager.ts +0 -112
  50. package/src/managers/pluginManager.ts +0 -330
  51. package/src/managers/pluginScopeManager.ts +0 -124
  52. package/src/managers/reversionManager.ts +0 -178
  53. package/src/managers/skillManager.ts +0 -578
  54. package/src/managers/slashCommandManager.ts +0 -564
  55. package/src/managers/subagentManager.ts +0 -915
  56. package/src/managers/toolManager.ts +0 -489
  57. package/src/managers/workflowManager.ts +0 -493
  58. package/src/prompts/autoMemory.ts +0 -33
  59. package/src/prompts/autoMemoryExtraction.ts +0 -146
  60. package/src/prompts/index.ts +0 -506
  61. package/src/prompts/planModeReminders.ts +0 -126
  62. package/src/services/GitService.ts +0 -131
  63. package/src/services/MarketplaceService.ts +0 -1052
  64. package/src/services/MemoryRuleService.ts +0 -71
  65. package/src/services/aiService.ts +0 -919
  66. package/src/services/authService.ts +0 -565
  67. package/src/services/autoMemoryService.ts +0 -280
  68. package/src/services/configurationService.ts +0 -1520
  69. package/src/services/fileWatcher.ts +0 -319
  70. package/src/services/hook.ts +0 -348
  71. package/src/services/initializationService.ts +0 -357
  72. package/src/services/interactionService.ts +0 -229
  73. package/src/services/jsonlHandler.ts +0 -295
  74. package/src/services/memory.ts +0 -234
  75. package/src/services/pluginLoader.ts +0 -252
  76. package/src/services/remoteSettingsService.ts +0 -366
  77. package/src/services/reversionService.ts +0 -122
  78. package/src/services/session.ts +0 -922
  79. package/src/services/taskManager.ts +0 -321
  80. package/src/telemetry/events.ts +0 -65
  81. package/src/telemetry/instrumentation.ts +0 -499
  82. package/src/telemetry/sessionTracing.ts +0 -348
  83. package/src/tools/agentTool.ts +0 -322
  84. package/src/tools/askUserQuestion.ts +0 -160
  85. package/src/tools/bashTool.ts +0 -621
  86. package/src/tools/buildTool.ts +0 -61
  87. package/src/tools/cronCreateTool.ts +0 -161
  88. package/src/tools/cronDeleteTool.ts +0 -51
  89. package/src/tools/cronListTool.ts +0 -47
  90. package/src/tools/editTool.ts +0 -313
  91. package/src/tools/enterPlanMode.ts +0 -124
  92. package/src/tools/enterWorktreeTool.ts +0 -190
  93. package/src/tools/exitPlanMode.ts +0 -139
  94. package/src/tools/exitWorktreeTool.ts +0 -236
  95. package/src/tools/globTool.ts +0 -167
  96. package/src/tools/grepTool.ts +0 -399
  97. package/src/tools/lspTool.ts +0 -883
  98. package/src/tools/readTool.ts +0 -426
  99. package/src/tools/skillTool.ts +0 -248
  100. package/src/tools/taskManagementTools.ts +0 -574
  101. package/src/tools/taskStopTool.ts +0 -78
  102. package/src/tools/types.ts +0 -137
  103. package/src/tools/webFetchTool.ts +0 -373
  104. package/src/tools/workflowTool.ts +0 -205
  105. package/src/tools/writeTool.ts +0 -267
  106. package/src/types/agent.ts +0 -128
  107. package/src/types/auth.ts +0 -19
  108. package/src/types/commands.ts +0 -30
  109. package/src/types/config.ts +0 -43
  110. package/src/types/configuration.ts +0 -168
  111. package/src/types/core.ts +0 -105
  112. package/src/types/cron.ts +0 -13
  113. package/src/types/environment.ts +0 -106
  114. package/src/types/fileSearch.ts +0 -4
  115. package/src/types/history.ts +0 -7
  116. package/src/types/hooks.ts +0 -256
  117. package/src/types/index.ts +0 -41
  118. package/src/types/lsp.ts +0 -98
  119. package/src/types/marketplace.ts +0 -71
  120. package/src/types/mcp.ts +0 -43
  121. package/src/types/memoryRule.ts +0 -31
  122. package/src/types/messaging.ts +0 -114
  123. package/src/types/permissions.ts +0 -80
  124. package/src/types/plugins.ts +0 -39
  125. package/src/types/processes.ts +0 -99
  126. package/src/types/reversion.ts +0 -29
  127. package/src/types/session.ts +0 -12
  128. package/src/types/skills.ts +0 -108
  129. package/src/types/tasks.ts +0 -13
  130. package/src/types/telemetry.ts +0 -98
  131. package/src/types/tools.ts +0 -44
  132. package/src/types/workflow.ts +0 -6
  133. package/src/utils/abortUtils.ts +0 -118
  134. package/src/utils/atomicWrite.ts +0 -61
  135. package/src/utils/bashParser.ts +0 -899
  136. package/src/utils/cacheControlUtils.ts +0 -426
  137. package/src/utils/commandArgumentParser.ts +0 -161
  138. package/src/utils/commandPathResolver.ts +0 -88
  139. package/src/utils/configPaths.ts +0 -220
  140. package/src/utils/configValidator.ts +0 -100
  141. package/src/utils/constants.ts +0 -39
  142. package/src/utils/container.ts +0 -92
  143. package/src/utils/containerSetup.ts +0 -371
  144. package/src/utils/convertMessagesForAPI.ts +0 -326
  145. package/src/utils/cronTasks.ts +0 -128
  146. package/src/utils/cronTasksLock.ts +0 -182
  147. package/src/utils/cronToHuman.ts +0 -99
  148. package/src/utils/customCommands.ts +0 -95
  149. package/src/utils/editUtils.ts +0 -20
  150. package/src/utils/fileFormat.ts +0 -40
  151. package/src/utils/fileSearch.ts +0 -151
  152. package/src/utils/fileUtils.ts +0 -223
  153. package/src/utils/gitUtils.ts +0 -315
  154. package/src/utils/globalLogger.ts +0 -128
  155. package/src/utils/groupMessagesByApiRound.ts +0 -121
  156. package/src/utils/hookMatcher.ts +0 -176
  157. package/src/utils/markdownParser.ts +0 -317
  158. package/src/utils/mcpUtils.ts +0 -138
  159. package/src/utils/messageOperations.ts +0 -641
  160. package/src/utils/modelCapabilities.ts +0 -30
  161. package/src/utils/nameGenerator.ts +0 -99
  162. package/src/utils/notificationXml.ts +0 -57
  163. package/src/utils/openaiClient.ts +0 -251
  164. package/src/utils/parseCronExpression.ts +0 -78
  165. package/src/utils/path.ts +0 -72
  166. package/src/utils/pathEncoder.ts +0 -400
  167. package/src/utils/pathSafety.ts +0 -40
  168. package/src/utils/promptHistory.ts +0 -168
  169. package/src/utils/ripgrep.ts +0 -35
  170. package/src/utils/shellResolver.ts +0 -190
  171. package/src/utils/skillParser.ts +0 -246
  172. package/src/utils/stringUtils.ts +0 -156
  173. package/src/utils/subagentParser.ts +0 -279
  174. package/src/utils/taskReminder.ts +0 -96
  175. package/src/utils/tokenCalculation.ts +0 -43
  176. package/src/utils/tokenEstimate.ts +0 -34
  177. package/src/utils/toolResultStorage.ts +0 -117
  178. package/src/utils/worktreeSession.ts +0 -26
  179. package/src/utils/worktreeUtils.ts +0 -786
  180. package/src/workflow/budgetTracker.ts +0 -34
  181. package/src/workflow/concurrencyLimiter.ts +0 -47
  182. package/src/workflow/journal.ts +0 -95
  183. package/src/workflow/progressReporter.ts +0 -141
  184. package/src/workflow/runState.ts +0 -65
  185. package/src/workflow/scriptRuntime.ts +0 -274
  186. package/src/workflow/structuredOutput.ts +0 -123
  187. package/src/workflow/types.ts +0 -101
  188. package/src/workflow/workflowApis.ts +0 -410
  189. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  190. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  191. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  192. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  193. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  194. 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
- }