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