wave-agent-sdk 1.0.5 → 1.0.7

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