wave-agent-sdk 1.2.0 → 1.3.0

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 (175) hide show
  1. package/dist/agent.d.ts +58 -4
  2. package/dist/agent.js +91 -19
  3. package/dist/builtin/index.js +2 -0
  4. package/dist/builtin/skills/settings.js +1 -12
  5. package/dist/builtin/skills/wave-daemon.d.ts +1 -0
  6. package/dist/builtin/skills/wave-daemon.js +194 -0
  7. package/dist/constants/images.d.ts +26 -0
  8. package/dist/constants/images.js +26 -0
  9. package/dist/constants/index.d.ts +16 -0
  10. package/dist/constants/index.js +16 -0
  11. package/dist/constants/memory.d.ts +26 -0
  12. package/dist/constants/memory.js +34 -0
  13. package/dist/constants/messages.d.ts +11 -0
  14. package/dist/constants/messages.js +11 -0
  15. package/dist/constants/plugins.d.ts +8 -0
  16. package/dist/constants/plugins.js +8 -0
  17. package/dist/constants/tools.d.ts +1 -0
  18. package/dist/constants/tools.js +1 -0
  19. package/dist/core/plugin.d.ts +53 -13
  20. package/dist/core/plugin.js +134 -26
  21. package/dist/core/session.d.ts +1 -1
  22. package/dist/core/session.js +1 -1
  23. package/dist/exec/catalog.d.ts +140 -0
  24. package/dist/exec/catalog.js +470 -0
  25. package/dist/exec/catalogAnnouncement.d.ts +89 -0
  26. package/dist/exec/catalogAnnouncement.js +293 -0
  27. package/dist/exec/constants.d.ts +51 -0
  28. package/dist/exec/constants.js +51 -0
  29. package/dist/exec/execRuntime.d.ts +55 -0
  30. package/dist/exec/execRuntime.js +217 -0
  31. package/dist/exec/workerSource.d.ts +28 -0
  32. package/dist/exec/workerSource.js +299 -0
  33. package/dist/host/index.d.ts +23 -0
  34. package/dist/host/index.js +23 -0
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +6 -0
  37. package/dist/managers/MemoryRuleManager.d.ts +6 -0
  38. package/dist/managers/MemoryRuleManager.js +12 -0
  39. package/dist/managers/aiManager.d.ts +35 -1
  40. package/dist/managers/aiManager.js +190 -21
  41. package/dist/managers/backgroundTaskManager.js +14 -0
  42. package/dist/managers/hookManager.d.ts +13 -0
  43. package/dist/managers/hookManager.js +31 -4
  44. package/dist/managers/liveConfigManager.d.ts +33 -0
  45. package/dist/managers/liveConfigManager.js +103 -8
  46. package/dist/managers/lspManager.d.ts +9 -0
  47. package/dist/managers/lspManager.js +47 -18
  48. package/dist/managers/mcpManager.d.ts +45 -10
  49. package/dist/managers/mcpManager.js +103 -1
  50. package/dist/managers/messageManager.d.ts +48 -5
  51. package/dist/managers/messageManager.js +107 -21
  52. package/dist/managers/permissionManager.d.ts +40 -0
  53. package/dist/managers/permissionManager.js +63 -8
  54. package/dist/managers/pluginManager.d.ts +46 -2
  55. package/dist/managers/pluginManager.js +117 -11
  56. package/dist/managers/pluginScopeManager.d.ts +15 -2
  57. package/dist/managers/pluginScopeManager.js +20 -1
  58. package/dist/managers/skillManager.d.ts +19 -0
  59. package/dist/managers/skillManager.js +44 -0
  60. package/dist/managers/slashCommandManager.d.ts +10 -0
  61. package/dist/managers/slashCommandManager.js +35 -3
  62. package/dist/managers/subagentManager.d.ts +8 -0
  63. package/dist/managers/subagentManager.js +20 -0
  64. package/dist/managers/toolManager.d.ts +29 -3
  65. package/dist/managers/toolManager.js +87 -13
  66. package/dist/prompts/autoMemory.d.ts +9 -0
  67. package/dist/prompts/autoMemory.js +30 -31
  68. package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
  69. package/dist/prompts/autoMemoryExtraction.js +8 -111
  70. package/dist/prompts/memoryTypes.d.ts +63 -0
  71. package/dist/prompts/memoryTypes.js +191 -0
  72. package/dist/services/GitService.d.ts +7 -0
  73. package/dist/services/GitService.js +23 -0
  74. package/dist/services/MarketplaceService.d.ts +101 -17
  75. package/dist/services/MarketplaceService.js +318 -119
  76. package/dist/services/artifactContent.d.ts +84 -0
  77. package/dist/services/artifactContent.js +204 -0
  78. package/dist/services/artifactSession.d.ts +6 -0
  79. package/dist/services/artifactSession.js +17 -0
  80. package/dist/services/autoMemoryService.js +5 -13
  81. package/dist/services/configurationService.d.ts +60 -9
  82. package/dist/services/configurationService.js +129 -54
  83. package/dist/services/contentSummarizer.d.ts +15 -0
  84. package/dist/services/contentSummarizer.js +45 -0
  85. package/dist/services/execAvailability.d.ts +9 -0
  86. package/dist/services/execAvailability.js +32 -0
  87. package/dist/services/fileWatcher.js +61 -6
  88. package/dist/services/initializationService.js +19 -15
  89. package/dist/services/interactionService.d.ts +9 -1
  90. package/dist/services/interactionService.js +28 -8
  91. package/dist/services/jsonlHandler.d.ts +84 -0
  92. package/dist/services/jsonlHandler.js +209 -14
  93. package/dist/services/memory.d.ts +3 -1
  94. package/dist/services/memory.js +13 -9
  95. package/dist/services/officialMarketplaceMirror.js +3 -2
  96. package/dist/services/pluginLoader.d.ts +12 -4
  97. package/dist/services/pluginLoader.js +38 -7
  98. package/dist/services/remoteSettingsService.js +16 -2
  99. package/dist/services/session.d.ts +74 -0
  100. package/dist/services/session.js +144 -3
  101. package/dist/services/sessionEntries.d.ts +2 -0
  102. package/dist/services/sessionEntries.js +20 -0
  103. package/dist/stdio/index.d.ts +3 -1
  104. package/dist/stdio/index.js +3 -1
  105. package/dist/stdio/notificationRouter.js +1 -0
  106. package/dist/stdio/stdioAgent.d.ts +14 -7
  107. package/dist/stdio/stdioAgent.js +19 -0
  108. package/dist/tools/artifactTool.js +406 -273
  109. package/dist/tools/bashTool.js +8 -6
  110. package/dist/tools/editTool.js +6 -3
  111. package/dist/tools/execTool.d.ts +2 -0
  112. package/dist/tools/execTool.js +165 -0
  113. package/dist/tools/grepTool.js +7 -1
  114. package/dist/tools/readTool.js +30 -2
  115. package/dist/tools/types.d.ts +34 -8
  116. package/dist/tools/webFetchTool.js +15 -166
  117. package/dist/tools/workflowTool.js +40 -8
  118. package/dist/tools/writeTool.js +6 -3
  119. package/dist/types/agent.d.ts +20 -5
  120. package/dist/types/configuration.d.ts +39 -1
  121. package/dist/types/marketplace.d.ts +40 -2
  122. package/dist/types/mcp.d.ts +39 -0
  123. package/dist/types/permissions.d.ts +22 -0
  124. package/dist/types/permissions.js +17 -0
  125. package/dist/types/plugins.d.ts +26 -2
  126. package/dist/types/skills.d.ts +15 -0
  127. package/dist/utils/constants.d.ts +10 -0
  128. package/dist/utils/constants.js +10 -0
  129. package/dist/utils/containerSetup.js +43 -0
  130. package/dist/utils/convertMessagesForAPI.d.ts +7 -1
  131. package/dist/utils/convertMessagesForAPI.js +64 -14
  132. package/dist/utils/fileChangeReminder.d.ts +20 -0
  133. package/dist/utils/fileChangeReminder.js +153 -0
  134. package/dist/utils/fileSearch.js +4 -3
  135. package/dist/utils/fileUtils.d.ts +33 -0
  136. package/dist/utils/fileUtils.js +81 -0
  137. package/dist/utils/frontmatterYaml.d.ts +33 -0
  138. package/dist/utils/frontmatterYaml.js +192 -0
  139. package/dist/utils/imageBudget.d.ts +85 -0
  140. package/dist/utils/imageBudget.js +109 -0
  141. package/dist/utils/imageDimensions.d.ts +83 -0
  142. package/dist/utils/imageDimensions.js +232 -0
  143. package/dist/utils/imageProcessor.d.ts +66 -0
  144. package/dist/utils/imageProcessor.js +84 -0
  145. package/dist/utils/imageRewrite.d.ts +29 -0
  146. package/dist/utils/imageRewrite.js +251 -0
  147. package/dist/utils/markdownParser.d.ts +5 -1
  148. package/dist/utils/markdownParser.js +9 -51
  149. package/dist/utils/mcpInstructions.d.ts +61 -0
  150. package/dist/utils/mcpInstructions.js +126 -0
  151. package/dist/utils/mcpUtils.d.ts +7 -0
  152. package/dist/utils/mcpUtils.js +11 -2
  153. package/dist/utils/memoryAge.d.ts +32 -0
  154. package/dist/utils/memoryAge.js +47 -0
  155. package/dist/utils/memoryEntrypoint.d.ts +20 -0
  156. package/dist/utils/memoryEntrypoint.js +49 -0
  157. package/dist/utils/memoryIndex.d.ts +30 -0
  158. package/dist/utils/memoryIndex.js +76 -0
  159. package/dist/utils/messageOperations.d.ts +6 -2
  160. package/dist/utils/messageOperations.js +40 -29
  161. package/dist/utils/nestedMemory.d.ts +22 -0
  162. package/dist/utils/nestedMemory.js +61 -0
  163. package/dist/utils/npmTarball.d.ts +19 -0
  164. package/dist/utils/npmTarball.js +92 -0
  165. package/dist/utils/pluginSource.d.ts +37 -0
  166. package/dist/utils/pluginSource.js +73 -0
  167. package/dist/utils/ripgrep.d.ts +18 -4
  168. package/dist/utils/ripgrep.js +56 -4
  169. package/dist/utils/runtimeDeps.d.ts +35 -0
  170. package/dist/utils/runtimeDeps.js +426 -0
  171. package/dist/utils/skillParser.js +22 -52
  172. package/dist/utils/subagentParser.js +39 -43
  173. package/dist/utils/userSettings.d.ts +90 -0
  174. package/dist/utils/userSettings.js +291 -0
  175. package/package.json +10 -7
@@ -7,7 +7,9 @@
7
7
  * - Coordination between file watchers and configuration updates
8
8
  */
9
9
  import { existsSync } from "fs";
10
+ import { dirname } from "path";
10
11
  import { FileWatcherService, } from "../services/fileWatcher.js";
12
+ import { USER_MEMORY_FILE } from "../utils/constants.js";
11
13
  import { isValidHookEvent } from "../types/hooks.js";
12
14
  import { logger } from "../utils/globalLogger.js";
13
15
  export class LiveConfigManager {
@@ -18,6 +20,13 @@ export class LiveConfigManager {
18
20
  // Configuration state
19
21
  this.currentConfiguration = null;
20
22
  this.lastValidConfiguration = null;
23
+ // Turn-scoped snapshot of the settings.json-derived configuration the running
24
+ // turn reads from. Captured at turn start, dropped at turn end, so a live
25
+ // reload landing mid-turn takes effect at the next turn instead of shifting
26
+ // values under the running one (core/agent-config.md scenario 5). Nested turns
27
+ // (a subagent turn inside its parent's) share the outer snapshot.
28
+ this.turnSnapshot = null;
29
+ this.turnDepth = 0;
21
30
  this.isWatching = false;
22
31
  this.reloadInProgress = false;
23
32
  this.workdir = options.workdir;
@@ -33,6 +42,36 @@ export class LiveConfigManager {
33
42
  get configurationService() {
34
43
  return this.container.get("ConfigurationService");
35
44
  }
45
+ get memoryService() {
46
+ return this.container.get("MemoryService");
47
+ }
48
+ /**
49
+ * Keep the auto-memory system safe zone in sync with the live auto-memory
50
+ * toggle (core/agent-config.md scenario 6). Turning auto-memory off must
51
+ * revoke the privilege its directory got at construction time, and turning it
52
+ * back on must restore it — both without rebuilding the session. Idempotent:
53
+ * the underlying add/remove are dedup'ed by path.
54
+ */
55
+ syncAutoMemorySafeZone() {
56
+ const permissionManager = this.permissionManager;
57
+ const memoryService = this.memoryService;
58
+ if (!permissionManager || !memoryService) {
59
+ return;
60
+ }
61
+ const directories = [
62
+ memoryService.getAutoMemoryDirectory(this.workdir),
63
+ USER_MEMORY_FILE,
64
+ ];
65
+ const enabled = this.configurationService.resolveAutoMemoryEnabledNow();
66
+ for (const directory of directories) {
67
+ if (enabled) {
68
+ permissionManager.addSystemAdditionalDirectory(directory);
69
+ }
70
+ else {
71
+ permissionManager.removeSystemAdditionalDirectory(directory);
72
+ }
73
+ }
74
+ }
36
75
  /**
37
76
  * Initialize configuration watching
38
77
  * Maps to FR-004: System MUST watch settings.json files
@@ -44,18 +83,18 @@ export class LiveConfigManager {
44
83
  this.projectConfigPaths = projectPaths;
45
84
  // Load initial configuration
46
85
  await this.reloadConfiguration();
47
- // Start watching user configs that exist
86
+ // Watch every configuration path, including ones that don't exist yet:
87
+ // chokidar delivers `add` once a missing path appears, but only if its
88
+ // parent chain exists when watching starts. Skipping absent paths (the
89
+ // pre-2026-09-10 behavior) silently dropped the very first save of a
90
+ // user-level settings.json created after session start
91
+ // (core/agent-config.md scenario 7).
48
92
  for (const userPath of userPaths) {
49
- if (existsSync(userPath)) {
50
- await this.fileWatcher.watchFile(userPath, (event) => this.handleFileChange(event, "user"));
51
- }
93
+ await this.fileWatcher.watchFile(this.resolveWatchTarget(userPath), (event) => this.handleFileChange(event, "user"));
52
94
  }
53
- // Start watching local configs that exist
54
95
  if (projectPaths) {
55
96
  for (const projectPath of projectPaths) {
56
- if (existsSync(projectPath)) {
57
- await this.fileWatcher.watchFile(projectPath, (event) => this.handleFileChange(event, "project"));
58
- }
97
+ await this.fileWatcher.watchFile(this.resolveWatchTarget(projectPath), (event) => this.handleFileChange(event, "project"));
59
98
  }
60
99
  }
61
100
  this.isWatching = true;
@@ -112,6 +151,41 @@ export class LiveConfigManager {
112
151
  throw error;
113
152
  }
114
153
  }
154
+ /**
155
+ * Turn boundary: pin the settings.json-derived configuration for the duration
156
+ * of a turn (called by AIManager). A live reload may land mid-turn and the
157
+ * turn must not see it (core/agent-config.md scenario 5) — the change is
158
+ * picked up by the snapshot taken when the next turn starts. Permission rules,
159
+ * hooks and env stay live: those are enforcement state and follow the file
160
+ * immediately.
161
+ */
162
+ onTurnStart() {
163
+ if (this.turnDepth === 0) {
164
+ const configuration = this.getCurrentConfiguration();
165
+ this.turnSnapshot = {
166
+ configuration: configuration ? structuredClone(configuration) : null,
167
+ // `env` on the merged configuration is exactly what was published as the
168
+ // session env snapshot when it was loaded (both come from the same
169
+ // merge), so the snapshot carries both without querying the service.
170
+ env: { ...(configuration?.env ?? {}) },
171
+ };
172
+ }
173
+ this.turnDepth++;
174
+ }
175
+ /** Turn boundary: drop the snapshot so the next turn reads live values. */
176
+ onTurnEnd() {
177
+ if (this.turnDepth === 0) {
178
+ return;
179
+ }
180
+ this.turnDepth--;
181
+ if (this.turnDepth === 0) {
182
+ this.turnSnapshot = null;
183
+ }
184
+ }
185
+ /** Configuration snapshot of the running turn, or null outside a turn. */
186
+ getTurnSnapshot() {
187
+ return this.turnDepth > 0 ? this.turnSnapshot : null;
188
+ }
115
189
  /**
116
190
  * Reload configuration from files
117
191
  * Maps to FR-008: Continue with previous valid configuration on errors
@@ -176,6 +250,7 @@ export class LiveConfigManager {
176
250
  this.permissionManager.updateAllowedRules(this.currentConfiguration.permissions?.allow || []);
177
251
  this.permissionManager.updateDeniedRules(this.currentConfiguration.permissions?.deny || []);
178
252
  this.permissionManager.updateAdditionalDirectories(this.currentConfiguration.permissions?.additionalDirectories || []);
253
+ this.syncAutoMemorySafeZone();
179
254
  }
180
255
  // Trigger reload callback. Awaited so fire-and-forget work spawned by the
181
256
  // callback (e.g. skill rediscovery) completes before the reload resolves —
@@ -304,6 +379,26 @@ export class LiveConfigManager {
304
379
  }
305
380
  return { added, modified, removed };
306
381
  }
382
+ /**
383
+ * Chokidar only starts watching a non-existent path when its parent directory
384
+ * exists at watch time (a path whose parents are all missing is never
385
+ * picked up, not even after they appear). Walk up to the deepest node whose
386
+ * parent exists and watch that instead — events for its descendants still
387
+ * reach this path's callback (FileWatcherService matches by path prefix).
388
+ */
389
+ resolveWatchTarget(configPath) {
390
+ let candidate = configPath;
391
+ while (!existsSync(dirname(candidate))) {
392
+ const parent = dirname(candidate);
393
+ if (parent === candidate) {
394
+ // Reached the filesystem root without an existing parent: nothing to
395
+ // watch (unreachable for absolute paths).
396
+ return configPath;
397
+ }
398
+ candidate = parent;
399
+ }
400
+ return candidate;
401
+ }
307
402
  /**
308
403
  * Get configuration file paths for user and project settings
309
404
  * Returns paths in priority order (local.json first, then .json)
@@ -21,6 +21,14 @@ export declare class LspManager implements ILspManager {
21
21
  constructor(container: Container);
22
22
  initialize(workdir: string): Promise<void>;
23
23
  registerServer(language: string, config: LspServerConfig): void;
24
+ /**
25
+ * Drop every LSP server contributed by a plugin (matched on pluginRoot),
26
+ * stopping any process already started for those languages so the next
27
+ * request starts the reloaded command instead of reusing the stale one.
28
+ * Called by the in-place plugin reload path before re-registering.
29
+ * @returns the number of servers removed
30
+ */
31
+ unregisterServersForPlugin(pluginRoot: string): Promise<number>;
24
32
  private loadConfig;
25
33
  getProcessForFile(filePath: string): Promise<LspProcess | null>;
26
34
  private startServer;
@@ -36,6 +44,7 @@ export declare class LspManager implements ILspManager {
36
44
  success: boolean;
37
45
  content: string;
38
46
  }>;
47
+ private stopProcess;
39
48
  cleanup(): Promise<void>;
40
49
  }
41
50
  export {};
@@ -17,6 +17,32 @@ export class LspManager {
17
17
  this.config[language] = config;
18
18
  logger?.debug(`Registered LSP server for ${language}`);
19
19
  }
20
+ /**
21
+ * Drop every LSP server contributed by a plugin (matched on pluginRoot),
22
+ * stopping any process already started for those languages so the next
23
+ * request starts the reloaded command instead of reusing the stale one.
24
+ * Called by the in-place plugin reload path before re-registering.
25
+ * @returns the number of servers removed
26
+ */
27
+ async unregisterServersForPlugin(pluginRoot) {
28
+ let removed = 0;
29
+ for (const [language, config] of Object.entries(this.config)) {
30
+ if (config.pluginRoot !== pluginRoot) {
31
+ continue;
32
+ }
33
+ delete this.config[language];
34
+ const lspProc = this.processes.get(language);
35
+ if (lspProc) {
36
+ this.processes.delete(language);
37
+ await this.stopProcess(language, lspProc);
38
+ }
39
+ removed += 1;
40
+ }
41
+ if (removed > 0) {
42
+ logger?.debug(`Unregistered ${removed} LSP servers for plugin root ${pluginRoot}`);
43
+ }
44
+ return removed;
45
+ }
20
46
  async loadConfig() {
21
47
  const lspJsonPath = join(this.workdir, ".lsp.json");
22
48
  try {
@@ -326,27 +352,30 @@ export class LspManager {
326
352
  return { success: false, content: `LSP error: ${JSON.stringify(error)}` };
327
353
  }
328
354
  }
329
- async cleanup() {
330
- for (const [language, lspProc] of this.processes.entries()) {
331
- try {
332
- // Try graceful shutdown
333
- const timeout = lspProc.config.shutdownTimeout || 2000;
334
- await this.sendRequest(lspProc, "shutdown", {}, timeout);
335
- await this.sendNotification(lspProc, "exit", {});
336
- // Give it a moment to exit
337
- if (timeout > 100) {
338
- await new Promise((resolve) => setTimeout(resolve, 100));
339
- }
340
- }
341
- catch (error) {
342
- logger?.debug(`Failed to gracefully shutdown LSP for ${language}: ${error}`);
355
+ async stopProcess(language, lspProc) {
356
+ try {
357
+ // Try graceful shutdown
358
+ const timeout = lspProc.config.shutdownTimeout || 2000;
359
+ await this.sendRequest(lspProc, "shutdown", {}, timeout);
360
+ await this.sendNotification(lspProc, "exit", {});
361
+ // Give it a moment to exit
362
+ if (timeout > 100) {
363
+ await new Promise((resolve) => setTimeout(resolve, 100));
343
364
  }
344
- finally {
345
- if (!lspProc.process.killed) {
346
- lspProc.process.kill();
347
- }
365
+ }
366
+ catch (error) {
367
+ logger?.debug(`Failed to gracefully shutdown LSP for ${language}: ${error}`);
368
+ }
369
+ finally {
370
+ if (!lspProc.process.killed) {
371
+ lspProc.process.kill();
348
372
  }
349
373
  }
374
+ }
375
+ async cleanup() {
376
+ for (const [language, lspProc] of this.processes.entries()) {
377
+ await this.stopProcess(language, lspProc);
378
+ }
350
379
  this.processes.clear();
351
380
  }
352
381
  }
@@ -1,7 +1,7 @@
1
1
  import { ChatCompletionFunctionTool } from "openai/resources.js";
2
2
  import type { ToolPlugin, ToolResult, ToolContext } from "../tools/types.js";
3
3
  import { Container } from "../utils/container.js";
4
- import type { Logger, McpServerConfig, McpConfig, McpTool, McpServerStatus } from "../types/index.js";
4
+ import type { Logger, McpServerConfig, McpConfig, McpTool, McpToolCallResult, McpServerStatus } from "../types/index.js";
5
5
  export interface McpManagerCallbacks {
6
6
  onMcpServersChange?: (servers: McpServerStatus[]) => void;
7
7
  }
@@ -69,9 +69,37 @@ export declare class McpManager {
69
69
  getConfig(): McpConfig | null;
70
70
  getAllServers(): McpServerStatus[];
71
71
  getServer(name: string): McpServerStatus | undefined;
72
+ /**
73
+ * The usage notes each usable server described about itself (`initialize`'s
74
+ * `instructions`), for the announcement appended to the conversation.
75
+ *
76
+ * "Usable" is the same predicate `getAllConnectedTools` uses — connected, or
77
+ * reconnecting with its last-known snapshot retained — so a server's prose is
78
+ * visible exactly while its tools are. A server that dropped, failed to connect
79
+ * or went to `error` keeps its snapshot but stops being reported here, which is
80
+ * what keeps a dead server from still talking to the model.
81
+ *
82
+ * `isDenied` receives flattened tool names (`mcp__server__tool`, the shape
83
+ * permission rules match on); a server whose tools are *all* excluded by rules is
84
+ * dropped whole, so prose cannot reach the context after the user ruled the
85
+ * server out. A server exposing no tools at all is still reported: nothing was
86
+ * excluded, it is simply a server that brings context rather than tools.
87
+ */
88
+ getServerInstructions(isDenied?: (toolName: string) => boolean): Array<{
89
+ name: string;
90
+ instructions: string;
91
+ }>;
72
92
  updateServerStatus(name: string, updates: Partial<McpServerStatus>): void;
73
93
  addServer(name: string, config: McpServerConfig): boolean;
74
94
  removeServer(name: string): boolean;
95
+ /**
96
+ * Drop every MCP server contributed by a plugin (matched on pluginRoot),
97
+ * disconnecting each one. Needed instead of re-adding under the same name
98
+ * because addServer refuses a name that is already registered.
99
+ * Called by the in-place plugin reload path before re-registering.
100
+ * @returns the number of servers removed
101
+ */
102
+ removeServersForPlugin(pluginRoot: string): number;
75
103
  /**
76
104
  * Remove a server from a persisted config file (user or project scope),
77
105
  * then disconnect and drop it from the in-memory registry.
@@ -94,15 +122,7 @@ export declare class McpManager {
94
122
  private cancelReconnect;
95
123
  disconnectServer(name: string): Promise<boolean>;
96
124
  getAllConnectedTools(): McpTool[];
97
- executeMcpTool(toolName: string, args: Record<string, unknown>, context?: ToolContext): Promise<{
98
- success: boolean;
99
- content: string;
100
- serverName?: string;
101
- images?: Array<{
102
- data: string;
103
- mediaType?: string;
104
- }>;
105
- }>;
125
+ executeMcpTool(toolName: string, args: Record<string, unknown>, context?: ToolContext): Promise<McpToolCallResult>;
106
126
  private executeToolOnConnection;
107
127
  cleanup(): Promise<void>;
108
128
  /**
@@ -113,6 +133,21 @@ export declare class McpManager {
113
133
  * Get all currently available MCP tools as OpenAI function tools
114
134
  */
115
135
  getMcpToolsConfig(): ChatCompletionFunctionTool[];
136
+ /**
137
+ * Declared output schema per flattened tool name, for the Exec catalog's return
138
+ * types.
139
+ *
140
+ * A separate accessor because `getMcpToolsConfig()` cannot carry it: an OpenAI
141
+ * function declaration has room for `parameters` and nothing else, so the schema
142
+ * a server declared for its *output* has no field to travel in. Correlated with
143
+ * the same `findToolServer` lookup `getMcpToolPlugins` uses and keyed with the
144
+ * same `mcpToolFlatName`, so the two agree on which server owns a tool.
145
+ *
146
+ * Servers that declare no output schema are simply absent, which renders as
147
+ * `Promise<unknown>`: honest about the call returning *something* of unspecified
148
+ * shape.
149
+ */
150
+ getMcpToolOutputSchemas(): Map<string, Record<string, unknown>>;
116
151
  /**
117
152
  * Execute an MCP tool by name (registry version)
118
153
  */
@@ -5,7 +5,8 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
5
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
6
6
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
7
7
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8
- import { createMcpToolPlugin, findToolServer } from "../utils/mcpUtils.js";
8
+ import { createMcpToolPlugin, findToolServer, mcpToolFlatName, } from "../utils/mcpUtils.js";
9
+ import { truncateMcpInstructions } from "../utils/mcpInstructions.js";
9
10
  import { logger } from "../utils/globalLogger.js";
10
11
  /**
11
12
  * Expand environment variables in a string value.
@@ -283,6 +284,38 @@ export class McpManager {
283
284
  getServer(name) {
284
285
  return this.servers.get(name);
285
286
  }
287
+ /**
288
+ * The usage notes each usable server described about itself (`initialize`'s
289
+ * `instructions`), for the announcement appended to the conversation.
290
+ *
291
+ * "Usable" is the same predicate `getAllConnectedTools` uses — connected, or
292
+ * reconnecting with its last-known snapshot retained — so a server's prose is
293
+ * visible exactly while its tools are. A server that dropped, failed to connect
294
+ * or went to `error` keeps its snapshot but stops being reported here, which is
295
+ * what keeps a dead server from still talking to the model.
296
+ *
297
+ * `isDenied` receives flattened tool names (`mcp__server__tool`, the shape
298
+ * permission rules match on); a server whose tools are *all* excluded by rules is
299
+ * dropped whole, so prose cannot reach the context after the user ruled the
300
+ * server out. A server exposing no tools at all is still reported: nothing was
301
+ * excluded, it is simply a server that brings context rather than tools.
302
+ */
303
+ getServerInstructions(isDenied) {
304
+ const result = [];
305
+ for (const server of this.servers.values()) {
306
+ const usable = server.status === "connected" || server.status === "reconnecting";
307
+ const instructions = server.instructions?.trim();
308
+ if (!usable || !instructions)
309
+ continue;
310
+ const tools = server.tools ?? [];
311
+ if (tools.length > 0 &&
312
+ tools.every((tool) => isDenied?.(mcpToolFlatName(server.name, tool.name)))) {
313
+ continue;
314
+ }
315
+ result.push({ name: server.name, instructions });
316
+ }
317
+ return result;
318
+ }
286
319
  updateServerStatus(name, updates) {
287
320
  const server = this.servers.get(name);
288
321
  if (server) {
@@ -353,6 +386,27 @@ export class McpManager {
353
386
  }
354
387
  return removed;
355
388
  }
389
+ /**
390
+ * Drop every MCP server contributed by a plugin (matched on pluginRoot),
391
+ * disconnecting each one. Needed instead of re-adding under the same name
392
+ * because addServer refuses a name that is already registered.
393
+ * Called by the in-place plugin reload path before re-registering.
394
+ * @returns the number of servers removed
395
+ */
396
+ removeServersForPlugin(pluginRoot) {
397
+ let removed = 0;
398
+ for (const [name, server] of Array.from(this.servers.entries())) {
399
+ if (server.config.pluginRoot !== pluginRoot) {
400
+ continue;
401
+ }
402
+ this.removeServer(name);
403
+ removed += 1;
404
+ }
405
+ if (removed > 0) {
406
+ logger?.debug(`Removed ${removed} MCP servers for plugin root ${pluginRoot}`);
407
+ }
408
+ return removed;
409
+ }
356
410
  /**
357
411
  * Remove a server from a persisted config file (user or project scope),
358
412
  * then disconnect and drop it from the in-memory registry.
@@ -400,6 +454,9 @@ export class McpManager {
400
454
  let transport;
401
455
  let client;
402
456
  let tools = [];
457
+ // Server-level usage notes from `initialize`; part of the connection's
458
+ // handshake, so it is read once here rather than re-requested later.
459
+ let instructions;
403
460
  const createClient = () => new Client({
404
461
  name: "wave-code",
405
462
  version: "1.0.0",
@@ -419,12 +476,14 @@ export class McpManager {
419
476
  });
420
477
  client = createClient();
421
478
  await client.connect(transport);
479
+ instructions = truncateMcpInstructions(client.getInstructions());
422
480
  const toolsResponse = await client.listTools();
423
481
  tools =
424
482
  toolsResponse.tools?.map((tool) => ({
425
483
  name: tool.name,
426
484
  description: tool.description,
427
485
  inputSchema: tool.inputSchema,
486
+ outputSchema: tool.outputSchema,
428
487
  })) || [];
429
488
  logger?.info(`Connected to MCP server ${name} using Streamable HTTP`);
430
489
  }
@@ -440,12 +499,14 @@ export class McpManager {
440
499
  });
441
500
  client = createClient();
442
501
  await client.connect(transport);
502
+ instructions = truncateMcpInstructions(client.getInstructions());
443
503
  const toolsResponse = await client.listTools();
444
504
  tools =
445
505
  toolsResponse.tools?.map((tool) => ({
446
506
  name: tool.name,
447
507
  description: tool.description,
448
508
  inputSchema: tool.inputSchema,
509
+ outputSchema: tool.outputSchema,
449
510
  })) || [];
450
511
  logger?.info(`Connected to MCP server ${name} using SSE`);
451
512
  }
@@ -517,12 +578,14 @@ export class McpManager {
517
578
  logger?.debug(`[MCP Server ${name}] Server stderr: ${stderrOutput.trim()}`);
518
579
  stderrOutput = "";
519
580
  }
581
+ instructions = truncateMcpInstructions(client.getInstructions());
520
582
  const toolsResponse = await client.listTools();
521
583
  tools =
522
584
  toolsResponse.tools?.map((tool) => ({
523
585
  name: tool.name,
524
586
  description: tool.description,
525
587
  inputSchema: tool.inputSchema,
588
+ outputSchema: tool.outputSchema,
526
589
  })) || [];
527
590
  }
528
591
  else if (serverType) {
@@ -568,6 +631,7 @@ export class McpManager {
568
631
  status: "disconnected",
569
632
  tools: [],
570
633
  toolCount: 0,
634
+ instructions: undefined,
571
635
  });
572
636
  // Auto-reconnect with exponential backoff. Skipped while an explicit
573
637
  // disconnectServer teardown is in flight — that close is expected, and
@@ -588,6 +652,7 @@ export class McpManager {
588
652
  tools,
589
653
  toolCount: tools.length,
590
654
  capabilities: ["tools"],
655
+ instructions,
591
656
  lastConnected: Date.now(),
592
657
  error: undefined,
593
658
  });
@@ -651,6 +716,7 @@ export class McpManager {
651
716
  name: tool.name,
652
717
  description: tool.description,
653
718
  inputSchema: tool.inputSchema,
719
+ outputSchema: tool.outputSchema,
654
720
  })) || [];
655
721
  logger?.info(`MCP Server ${name} auto-reconnected successfully (attempt ${i + 1})`);
656
722
  this.updateServerStatus(name, {
@@ -692,6 +758,7 @@ export class McpManager {
692
758
  status: "disconnected",
693
759
  tools: [],
694
760
  toolCount: 0,
761
+ instructions: undefined,
695
762
  error: undefined,
696
763
  });
697
764
  }
@@ -716,6 +783,7 @@ export class McpManager {
716
783
  status: "disconnected",
717
784
  tools: [],
718
785
  toolCount: 0,
786
+ instructions: undefined,
719
787
  error: undefined,
720
788
  });
721
789
  return true;
@@ -732,6 +800,7 @@ export class McpManager {
732
800
  status: "disconnected",
733
801
  tools: [],
734
802
  toolCount: 0,
803
+ instructions: undefined,
735
804
  error: error instanceof Error ? error.message : String(error),
736
805
  });
737
806
  return false;
@@ -823,9 +892,17 @@ export class McpManager {
823
892
  : images.length > 0
824
893
  ? `Tool returned ${images.length} image(s).`
825
894
  : "No content";
895
+ // The sandbox's value, decided from the text parts rather than from
896
+ // `textContentStr`: that one carries display placeholders ("No content"),
897
+ // and a script has to be able to tell "the tool said nothing" (`null`) from
898
+ // "the tool said 'No content'". Same rule the catalog renders return types
899
+ // from: structured, else text, else null.
900
+ const text = textContent.join("\n");
901
+ const output = result.structuredContent ?? (text === "" ? null : text);
826
902
  return {
827
903
  success: true,
828
904
  content: textContentStr,
905
+ output,
829
906
  images: images.length > 0 ? images : undefined,
830
907
  serverName,
831
908
  };
@@ -869,6 +946,31 @@ export class McpManager {
869
946
  getMcpToolsConfig() {
870
947
  return this.getMcpToolPlugins().map((tool) => tool.config);
871
948
  }
949
+ /**
950
+ * Declared output schema per flattened tool name, for the Exec catalog's return
951
+ * types.
952
+ *
953
+ * A separate accessor because `getMcpToolsConfig()` cannot carry it: an OpenAI
954
+ * function declaration has room for `parameters` and nothing else, so the schema
955
+ * a server declared for its *output* has no field to travel in. Correlated with
956
+ * the same `findToolServer` lookup `getMcpToolPlugins` uses and keyed with the
957
+ * same `mcpToolFlatName`, so the two agree on which server owns a tool.
958
+ *
959
+ * Servers that declare no output schema are simply absent, which renders as
960
+ * `Promise<unknown>`: honest about the call returning *something* of unspecified
961
+ * shape.
962
+ */
963
+ getMcpToolOutputSchemas() {
964
+ const schemas = new Map();
965
+ const servers = this.getAllServers();
966
+ for (const tool of this.getAllConnectedTools()) {
967
+ const server = findToolServer(tool.name, servers);
968
+ if (server && tool.outputSchema) {
969
+ schemas.set(mcpToolFlatName(server.name, tool.name), tool.outputSchema);
970
+ }
971
+ }
972
+ return schemas;
973
+ }
872
974
  /**
873
975
  * Execute an MCP tool by name (registry version)
874
976
  */