agent-world 0.11.1 → 0.12.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 (267) hide show
  1. package/README.md +17 -7
  2. package/dist/cli/commands.d.ts +109 -0
  3. package/dist/cli/commands.js +2024 -0
  4. package/dist/cli/display.d.ts +124 -0
  5. package/dist/cli/display.js +381 -0
  6. package/dist/cli/hitl.d.ts +33 -0
  7. package/dist/cli/hitl.js +81 -0
  8. package/dist/cli/index.d.ts +2 -0
  9. package/dist/cli/stream.d.ts +41 -0
  10. package/dist/cli/stream.js +222 -0
  11. package/dist/core/activity-tracker.d.ts +16 -0
  12. package/dist/core/activity-tracker.d.ts.map +1 -0
  13. package/dist/core/activity-tracker.js +91 -0
  14. package/dist/core/activity-tracker.js.map +1 -0
  15. package/dist/core/ai-commands.d.ts +16 -0
  16. package/dist/core/ai-commands.d.ts.map +1 -0
  17. package/dist/core/ai-commands.js +24 -0
  18. package/dist/core/ai-commands.js.map +1 -0
  19. package/dist/core/ai-sdk-patch.d.ts +24 -0
  20. package/dist/core/ai-sdk-patch.d.ts.map +1 -0
  21. package/dist/core/ai-sdk-patch.js +169 -0
  22. package/dist/core/ai-sdk-patch.js.map +1 -0
  23. package/dist/core/anthropic-direct.d.ts +52 -0
  24. package/dist/core/anthropic-direct.d.ts.map +1 -0
  25. package/dist/core/anthropic-direct.js +301 -0
  26. package/dist/core/anthropic-direct.js.map +1 -0
  27. package/dist/core/approval-cache.d.ts +104 -0
  28. package/dist/core/approval-cache.d.ts.map +1 -0
  29. package/dist/core/approval-cache.js +150 -0
  30. package/dist/core/approval-cache.js.map +1 -0
  31. package/dist/core/chat-constants.d.ts +20 -0
  32. package/dist/core/chat-constants.d.ts.map +1 -0
  33. package/dist/core/chat-constants.js +22 -0
  34. package/dist/core/chat-constants.js.map +1 -0
  35. package/dist/core/create-agent-tool.d.ts +66 -0
  36. package/dist/core/create-agent-tool.d.ts.map +1 -0
  37. package/dist/core/create-agent-tool.js +212 -0
  38. package/dist/core/create-agent-tool.js.map +1 -0
  39. package/dist/core/events/approval-checker.d.ts +61 -0
  40. package/dist/core/events/approval-checker.d.ts.map +1 -0
  41. package/dist/core/events/approval-checker.js +226 -0
  42. package/dist/core/events/approval-checker.js.map +1 -0
  43. package/dist/core/events/index.d.ts +25 -0
  44. package/dist/core/events/index.d.ts.map +1 -0
  45. package/dist/core/events/index.js +30 -0
  46. package/dist/core/events/index.js.map +1 -0
  47. package/dist/core/events/memory-manager.d.ts +73 -0
  48. package/dist/core/events/memory-manager.d.ts.map +1 -0
  49. package/dist/core/events/memory-manager.js +1218 -0
  50. package/dist/core/events/memory-manager.js.map +1 -0
  51. package/dist/core/events/mention-logic.d.ts +39 -0
  52. package/dist/core/events/mention-logic.d.ts.map +1 -0
  53. package/dist/core/events/mention-logic.js +163 -0
  54. package/dist/core/events/mention-logic.js.map +1 -0
  55. package/dist/core/events/orchestrator.d.ts +69 -0
  56. package/dist/core/events/orchestrator.d.ts.map +1 -0
  57. package/dist/core/events/orchestrator.js +883 -0
  58. package/dist/core/events/orchestrator.js.map +1 -0
  59. package/dist/core/events/persistence.d.ts +41 -0
  60. package/dist/core/events/persistence.d.ts.map +1 -0
  61. package/dist/core/events/persistence.js +296 -0
  62. package/dist/core/events/persistence.js.map +1 -0
  63. package/dist/core/events/publishers.d.ts +81 -0
  64. package/dist/core/events/publishers.d.ts.map +1 -0
  65. package/dist/core/events/publishers.js +272 -0
  66. package/dist/core/events/publishers.js.map +1 -0
  67. package/dist/core/events/subscribers.d.ts +45 -0
  68. package/dist/core/events/subscribers.d.ts.map +1 -0
  69. package/dist/core/events/subscribers.js +288 -0
  70. package/dist/core/events/subscribers.js.map +1 -0
  71. package/dist/core/events/tool-bridge-logging.d.ts +28 -0
  72. package/dist/core/events/tool-bridge-logging.d.ts.map +1 -0
  73. package/dist/core/events/tool-bridge-logging.js +94 -0
  74. package/dist/core/events/tool-bridge-logging.js.map +1 -0
  75. package/dist/core/events-metadata.d.ts +72 -0
  76. package/dist/core/events-metadata.d.ts.map +1 -0
  77. package/dist/core/events-metadata.js +167 -0
  78. package/dist/core/events-metadata.js.map +1 -0
  79. package/dist/core/events.d.ts +186 -0
  80. package/dist/core/events.d.ts.map +1 -0
  81. package/dist/core/events.js +1248 -0
  82. package/dist/core/events.js.map +1 -0
  83. package/dist/core/export.d.ts +106 -0
  84. package/dist/core/export.d.ts.map +1 -0
  85. package/dist/core/export.js +705 -0
  86. package/dist/core/export.js.map +1 -0
  87. package/dist/core/file-tools.d.ts +114 -0
  88. package/dist/core/file-tools.d.ts.map +1 -0
  89. package/dist/core/file-tools.js +370 -0
  90. package/dist/core/file-tools.js.map +1 -0
  91. package/dist/core/google-direct.d.ts +58 -0
  92. package/dist/core/google-direct.d.ts.map +1 -0
  93. package/dist/core/google-direct.js +298 -0
  94. package/dist/core/google-direct.js.map +1 -0
  95. package/dist/core/hitl.d.ts +54 -0
  96. package/dist/core/hitl.d.ts.map +1 -0
  97. package/dist/core/hitl.js +153 -0
  98. package/dist/core/hitl.js.map +1 -0
  99. package/dist/core/index.d.ts +59 -0
  100. package/dist/core/index.d.ts.map +1 -0
  101. package/dist/core/index.js +70 -0
  102. package/dist/core/index.js.map +1 -0
  103. package/dist/core/llm-config.d.ts +128 -0
  104. package/dist/core/llm-config.d.ts.map +1 -0
  105. package/dist/core/llm-config.js +164 -0
  106. package/dist/core/llm-config.js.map +1 -0
  107. package/dist/core/llm-manager.d.ts +163 -0
  108. package/dist/core/llm-manager.d.ts.map +1 -0
  109. package/dist/core/llm-manager.js +669 -0
  110. package/dist/core/llm-manager.js.map +1 -0
  111. package/dist/core/load-skill-tool.d.ts +55 -0
  112. package/dist/core/load-skill-tool.d.ts.map +1 -0
  113. package/dist/core/load-skill-tool.js +468 -0
  114. package/dist/core/load-skill-tool.js.map +1 -0
  115. package/dist/core/logger.d.ts +88 -0
  116. package/dist/core/logger.d.ts.map +1 -0
  117. package/dist/core/logger.js +358 -0
  118. package/dist/core/logger.js.map +1 -0
  119. package/dist/core/managers.d.ts +131 -0
  120. package/dist/core/managers.d.ts.map +1 -0
  121. package/dist/core/managers.js +1223 -0
  122. package/dist/core/managers.js.map +1 -0
  123. package/dist/core/mcp-server-registry.d.ts +304 -0
  124. package/dist/core/mcp-server-registry.d.ts.map +1 -0
  125. package/dist/core/mcp-server-registry.js +1769 -0
  126. package/dist/core/mcp-server-registry.js.map +1 -0
  127. package/dist/core/mcp-tools.d.ts +56 -0
  128. package/dist/core/mcp-tools.d.ts.map +1 -0
  129. package/dist/core/mcp-tools.js +186 -0
  130. package/dist/core/mcp-tools.js.map +1 -0
  131. package/dist/core/message-prep.d.ts +81 -0
  132. package/dist/core/message-prep.d.ts.map +1 -0
  133. package/dist/core/message-prep.js +223 -0
  134. package/dist/core/message-prep.js.map +1 -0
  135. package/dist/core/message-processing-control.d.ts +54 -0
  136. package/dist/core/message-processing-control.d.ts.map +1 -0
  137. package/dist/core/message-processing-control.js +139 -0
  138. package/dist/core/message-processing-control.js.map +1 -0
  139. package/dist/core/openai-direct.d.ts +80 -0
  140. package/dist/core/openai-direct.d.ts.map +1 -0
  141. package/dist/core/openai-direct.js +374 -0
  142. package/dist/core/openai-direct.js.map +1 -0
  143. package/dist/core/shell-cmd-tool.d.ts +235 -0
  144. package/dist/core/shell-cmd-tool.d.ts.map +1 -0
  145. package/dist/core/shell-cmd-tool.js +1157 -0
  146. package/dist/core/shell-cmd-tool.js.map +1 -0
  147. package/dist/core/shell-process-registry.d.ts +88 -0
  148. package/dist/core/shell-process-registry.d.ts.map +1 -0
  149. package/dist/core/shell-process-registry.js +309 -0
  150. package/dist/core/shell-process-registry.js.map +1 -0
  151. package/dist/core/skill-registry.d.ts +75 -0
  152. package/dist/core/skill-registry.d.ts.map +1 -0
  153. package/dist/core/skill-registry.js +369 -0
  154. package/dist/core/skill-registry.js.map +1 -0
  155. package/dist/core/skill-script-runner.d.ts +89 -0
  156. package/dist/core/skill-script-runner.d.ts.map +1 -0
  157. package/dist/core/skill-script-runner.js +274 -0
  158. package/dist/core/skill-script-runner.js.map +1 -0
  159. package/dist/core/skill-selector.d.ts +65 -0
  160. package/dist/core/skill-selector.d.ts.map +1 -0
  161. package/dist/core/skill-selector.js +190 -0
  162. package/dist/core/skill-selector.js.map +1 -0
  163. package/dist/core/skill-settings.d.ts +20 -0
  164. package/dist/core/skill-settings.d.ts.map +1 -0
  165. package/dist/core/skill-settings.js +40 -0
  166. package/dist/core/skill-settings.js.map +1 -0
  167. package/dist/core/storage/agent-storage.d.ts +134 -0
  168. package/dist/core/storage/agent-storage.d.ts.map +1 -0
  169. package/dist/core/storage/agent-storage.js +498 -0
  170. package/dist/core/storage/agent-storage.js.map +1 -0
  171. package/dist/core/storage/eventStorage/fileEventStorage.d.ts +100 -0
  172. package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -0
  173. package/dist/core/storage/eventStorage/fileEventStorage.js +494 -0
  174. package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -0
  175. package/dist/core/storage/eventStorage/index.d.ts +31 -0
  176. package/dist/core/storage/eventStorage/index.d.ts.map +1 -0
  177. package/dist/core/storage/eventStorage/index.js +31 -0
  178. package/dist/core/storage/eventStorage/index.js.map +1 -0
  179. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +87 -0
  180. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -0
  181. package/dist/core/storage/eventStorage/memoryEventStorage.js +244 -0
  182. package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -0
  183. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +45 -0
  184. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -0
  185. package/dist/core/storage/eventStorage/sqliteEventStorage.js +301 -0
  186. package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -0
  187. package/dist/core/storage/eventStorage/types.d.ts +142 -0
  188. package/dist/core/storage/eventStorage/types.d.ts.map +1 -0
  189. package/dist/core/storage/eventStorage/types.js +43 -0
  190. package/dist/core/storage/eventStorage/types.js.map +1 -0
  191. package/dist/core/storage/eventStorage/validation.d.ts +30 -0
  192. package/dist/core/storage/eventStorage/validation.d.ts.map +1 -0
  193. package/dist/core/storage/eventStorage/validation.js +68 -0
  194. package/dist/core/storage/eventStorage/validation.js.map +1 -0
  195. package/dist/core/storage/legacy-migrations.d.ts +45 -0
  196. package/dist/core/storage/legacy-migrations.d.ts.map +1 -0
  197. package/dist/core/storage/legacy-migrations.js +295 -0
  198. package/dist/core/storage/legacy-migrations.js.map +1 -0
  199. package/dist/core/storage/memory-storage.d.ts +105 -0
  200. package/dist/core/storage/memory-storage.d.ts.map +1 -0
  201. package/dist/core/storage/memory-storage.js +415 -0
  202. package/dist/core/storage/memory-storage.js.map +1 -0
  203. package/dist/core/storage/migration-runner.d.ts +96 -0
  204. package/dist/core/storage/migration-runner.d.ts.map +1 -0
  205. package/dist/core/storage/migration-runner.js +306 -0
  206. package/dist/core/storage/migration-runner.js.map +1 -0
  207. package/dist/core/storage/queue-storage.d.ts +147 -0
  208. package/dist/core/storage/queue-storage.d.ts.map +1 -0
  209. package/dist/core/storage/queue-storage.js +290 -0
  210. package/dist/core/storage/queue-storage.js.map +1 -0
  211. package/dist/core/storage/skill-storage.d.ts +136 -0
  212. package/dist/core/storage/skill-storage.d.ts.map +1 -0
  213. package/dist/core/storage/skill-storage.js +474 -0
  214. package/dist/core/storage/skill-storage.js.map +1 -0
  215. package/dist/core/storage/sqlite-schema.d.ts +95 -0
  216. package/dist/core/storage/sqlite-schema.d.ts.map +1 -0
  217. package/dist/core/storage/sqlite-schema.js +156 -0
  218. package/dist/core/storage/sqlite-schema.js.map +1 -0
  219. package/dist/core/storage/sqlite-storage.d.ts +146 -0
  220. package/dist/core/storage/sqlite-storage.d.ts.map +1 -0
  221. package/dist/core/storage/sqlite-storage.js +709 -0
  222. package/dist/core/storage/sqlite-storage.js.map +1 -0
  223. package/dist/core/storage/storage-factory.d.ts +61 -0
  224. package/dist/core/storage/storage-factory.d.ts.map +1 -0
  225. package/dist/core/storage/storage-factory.js +794 -0
  226. package/dist/core/storage/storage-factory.js.map +1 -0
  227. package/dist/core/storage/validation.d.ts +36 -0
  228. package/dist/core/storage/validation.d.ts.map +1 -0
  229. package/dist/core/storage/validation.js +79 -0
  230. package/dist/core/storage/validation.js.map +1 -0
  231. package/dist/core/storage/world-storage.d.ts +114 -0
  232. package/dist/core/storage/world-storage.d.ts.map +1 -0
  233. package/dist/core/storage/world-storage.js +378 -0
  234. package/dist/core/storage/world-storage.js.map +1 -0
  235. package/dist/core/subscription.d.ts +43 -0
  236. package/dist/core/subscription.d.ts.map +1 -0
  237. package/dist/core/subscription.js +227 -0
  238. package/dist/core/subscription.js.map +1 -0
  239. package/dist/core/tool-utils.d.ts +80 -0
  240. package/dist/core/tool-utils.d.ts.map +1 -0
  241. package/dist/core/tool-utils.js +273 -0
  242. package/dist/core/tool-utils.js.map +1 -0
  243. package/dist/core/types.d.ts +595 -0
  244. package/dist/core/types.d.ts.map +1 -0
  245. package/dist/core/types.js +158 -0
  246. package/dist/core/types.js.map +1 -0
  247. package/dist/core/utils.d.ts +138 -0
  248. package/dist/core/utils.d.ts.map +1 -0
  249. package/dist/core/utils.js +478 -0
  250. package/dist/core/utils.js.map +1 -0
  251. package/dist/core/world-class.d.ts +43 -0
  252. package/dist/core/world-class.d.ts.map +1 -0
  253. package/dist/core/world-class.js +90 -0
  254. package/dist/core/world-class.js.map +1 -0
  255. package/dist/index.d.ts +18 -0
  256. package/dist/public/assets/agent-sprites-DJFgj-zP.png +0 -0
  257. package/dist/public/assets/border-KHK37r8y.svg +83 -0
  258. package/dist/public/assets/index-C9kPXL6G.css +1 -0
  259. package/dist/public/assets/index-DOQEHGWt.js +96 -0
  260. package/dist/public/index.html +21 -0
  261. package/dist/server/api.d.ts +2 -0
  262. package/dist/server/api.js +1124 -0
  263. package/dist/server/index.d.ts +29 -0
  264. package/dist/server/sse-handler.d.ts +62 -0
  265. package/dist/server/sse-handler.js +234 -0
  266. package/package.json +15 -3
  267. package/scripts/launch-electron.js +0 -58
@@ -0,0 +1,1769 @@
1
+ /**
2
+ * MCP Server Registry and Tools Integration - Clean Implementation with Runtime AI SDK Patch
3
+ *
4
+ * Logger Categories: mcp.lifecycle, mcp.connection, mcp.tools, mcp.execution
5
+ * Purpose: MCP server management and tool execution
6
+ *
7
+ * Enable with:
8
+ * - LOG_MCP_LIFECYCLE=info - Server start/stop/ready events
9
+ * - LOG_MCP_CONNECTION=debug - Connection establishment details
10
+ * - LOG_MCP_TOOLS=debug - Tool discovery and caching
11
+ * - LOG_MCP_EXECUTION=debug - Tool execution and results
12
+ * - LOG_MCP=debug - Enable all MCP logs
13
+ *
14
+ * What you'll see:
15
+ * - Server lifecycle: start, stop, ready, shutdown
16
+ * - Connection: transport creation, connection attempts
17
+ * - Tools: discovery, caching, schema validation
18
+ * - Execution: tool calls, results, performance metrics
19
+ *
20
+ * Comprehensive MCP (Model Context Protocol) management system providing:
21
+ * - Server lifecycle management with reference counting and connection pooling
22
+ * - Configuration-based server identification and sharing across worlds
23
+ * - Flexible configuration: Supports both 'servers' and 'mcpServers' field names
24
+ * - AI-compatible tool conversion with schema validation
25
+ * - Transport support for stdio, SSE, and streamable HTTP connections
26
+ * - Health monitoring, error handling, and graceful shutdown
27
+ * - Thread-safe registry operations with world-server mapping
28
+ * - Consolidated logging under LOG_LLM_MCP for unified debugging
29
+ * - Enhanced debug logging for MCP communication data flows
30
+ *
31
+ * Key Features:
32
+ * - Azure OpenAI compatibility: Uses runtime AI SDK patch (core/ai-sdk-patch.ts) for schema corruption fix
33
+ * - Function names use underscores, clean schema structures for tool definitions
34
+ * - Smart server sharing: Multiple worlds can share the same server configuration
35
+ * - Automatic cleanup: Servers shut down when no longer referenced (30s delay)
36
+ * - Error resilience: Comprehensive error handling with fallback mechanisms
37
+ * - Schema validation: Creates well-formed, Azure-compatible schemas for all MCP tools
38
+ * - Performance tracking: Tool execution duration and result analysis
39
+ * - Sequence tracking: Tool call dependencies and execution relationships
40
+ * - Data flow debugging: Complete request/response payload logging
41
+ *
42
+ * Connection Resilience & Lifecycle Management (November 2025):
43
+ * - Automatic reconnection: Detects connection-level errors and attempts reconnection
44
+ * - Retry strategy: Up to 2 attempts for transient network failures
45
+ * - Connection error patterns: ECONNRESET, EPIPE, socket hang up, transport errors
46
+ * - Race condition protection: Prevents concurrent reconnection attempts via reconnecting flag
47
+ * - Client lifecycle management: ClientRef pattern for tracking active connections
48
+ * - Proper resource cleanup: Ensures clients are closed on cache eviction and shutdown
49
+ * - Memory leak prevention: Cache entries deleted even if disposal fails
50
+ * - MCP error response detection: Handles both isError and type: 'error' formats
51
+ *
52
+ * Reconnection Logic:
53
+ * - Triggered automatically on connection-level errors during tool execution
54
+ * - First attempt: Try to reconnect and retry the operation
55
+ * - Second attempt: Fail and propagate error if reconnection unsuccessful
56
+ * - Concurrent calls: Wait for in-progress reconnection instead of creating new ones
57
+ * - Cache refresh: Update cache timestamp after successful reconnection
58
+ * - Logging: Detailed tracking of reconnection attempts and outcomes
59
+ *
60
+ * MCP Communication Debug Logging (LOG_LLM_MCP=debug):
61
+ * - Server connection attempts with transport and configuration details
62
+ * - Tool list requests and responses with full payload data
63
+ * - Tool execution requests with complete argument structures
64
+ * - Tool execution responses with full result content and metadata
65
+ * - Request/response data size and structure analysis
66
+ * - Raw JSON payloads for deep debugging of MCP communication
67
+ * - Connection establishment and transport creation logging
68
+ * - Server registration configuration details
69
+ * - Reconnection attempts and retry logic execution
70
+ *
71
+ * MCP Tool Execution Logging (LOG_LLM_MCP=debug):
72
+ * - Tool execution performance metrics with millisecond precision
73
+ * - Tool result content analysis including size and type identification
74
+ * - Tool call sequence tracking with unique sequence IDs
75
+ * - Success/failure status with detailed error information
76
+ * - Parent-child tool call relationship tracking
77
+ * - Argument validation and presence checking
78
+ * - Result preview for debugging without exposing full content
79
+ * - Complete request/response payload logging for troubleshooting
80
+ * - Retry attempt tracking with attempt number and max attempts
81
+ *
82
+ * Schema Approach:
83
+ * - Uses simplified property types (string, number, boolean, array with string items)
84
+ * - Includes additionalProperties: false to prevent schema expansion
85
+ * - Maintains required fields but simplifies complex nested structures
86
+ * - Works with runtime AI SDK patch to prevent schema corruption in Azure OpenAI calls
87
+ *
88
+ * LLM Argument Type Correction:
89
+ * - Automatically fixes common LLM type errors in tool arguments
90
+ * - String to array conversion: "value" -> ["value"]
91
+ * - String to number conversion: "5" -> 5
92
+ * - Empty/invalid enum omission: "" -> (omitted, uses schema default)
93
+ * - Case-insensitive enum matching: "RELEVANCE" -> "relevance"
94
+ * - Null/undefined omission for optional params: null -> (omitted when not required)
95
+ * - Applied transparently during tool execution to prevent MCP validation errors
96
+ * - Logs all corrections for debugging and monitoring
97
+ * - Schema preservation: bulletproofSchema preserves enum, items, min/max, required for validation
98
+ *
99
+ * Architecture: Function-based design with module-level state management
100
+ * Consolidated from: mcp-server-registry.ts + mcp-tools.ts (August 2025)
101
+ * Runtime patch integration: Works with ai-sdk-patch.ts for Azure compatibility (August 2025)
102
+ * Enhanced debug logging: Complete MCP data flow visibility (August 2025)
103
+ * Scenario-based logging: Split into lifecycle, connection, tools, execution (October 2025)
104
+ * Lifecycle management: Connection resilience and automatic reconnection (November 2025)
105
+ * Explicit execution safety system: Replaced heuristic detection with structured metadata (November 2025)
106
+ * 2026-02-14: Added built-in `load_skill` tool registration for progressive skill instruction loading.
107
+ * 2026-02-19: Added built-in `create_agent` tool registration with approval-gated agent creation.
108
+ */
109
+ import { createHash } from 'crypto';
110
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
111
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
112
+ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
113
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
114
+ import { getWorld } from './managers.js';
115
+ import { createCategoryLogger } from './logger.js';
116
+ import { createShellCmdToolDefinition } from './shell-cmd-tool.js';
117
+ import { createLoadSkillToolDefinition } from './load-skill-tool.js';
118
+ import { createCreateAgentToolDefinition } from './create-agent-tool.js';
119
+ import { createReadFileToolDefinition, createListFilesToolDefinition, createGrepToolDefinition, } from './file-tools.js';
120
+ import { wrapToolWithValidation } from './tool-utils.js';
121
+ // Scenario-based loggers for different MCP operations
122
+ const lifecycleLogger = createCategoryLogger('mcp.lifecycle');
123
+ const connectionLogger = createCategoryLogger('mcp.connection');
124
+ const toolsLogger = createCategoryLogger('mcp.tools');
125
+ const executionLogger = createCategoryLogger('mcp.execution');
126
+ // Legacy logger for backward compatibility and general debug logs
127
+ const logger = createCategoryLogger('llm.mcp');
128
+ // === TOOL UTILITY HELPERS ===
129
+ /**
130
+ * Sanitizes tool arguments by redacting sensitive information.
131
+ * Protects passwords, keys, tokens, and other sensitive data.
132
+ *
133
+ * @param args - Tool arguments object
134
+ * @returns Sanitized copy of arguments
135
+ */
136
+ export function sanitizeArgs(args) {
137
+ if (!args || typeof args !== 'object') {
138
+ return args;
139
+ }
140
+ const sensitiveKeys = ['key', 'password', 'token', 'secret', 'apikey', 'api_key'];
141
+ const sanitized = { ...args };
142
+ for (const key in sanitized) {
143
+ if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk))) {
144
+ sanitized[key] = '[REDACTED]';
145
+ }
146
+ }
147
+ return sanitized;
148
+ }
149
+ /**
150
+ * OLLAMA BUG FIX: Translate "$" parameter to proper parameter names
151
+ *
152
+ * Problem: Ollama (Llama 3.2) has a bug where it sends {"$": "value"}
153
+ * instead of proper parameter names like {"query": "value"}
154
+ *
155
+ * Solution: Detect single "$" argument and map it to the first required parameter
156
+ *
157
+ * Reference: https://github.com/ollama/ollama/issues/7860
158
+ */
159
+ function translateOllamaArguments(args, toolSchema) {
160
+ // If args is not an object or doesn't have the "$" bug, return as-is
161
+ if (!args || typeof args !== 'object' || !args.hasOwnProperty('$')) {
162
+ return args;
163
+ }
164
+ // If there are multiple parameters, don't translate (ambiguous)
165
+ const argKeys = Object.keys(args);
166
+ if (argKeys.length !== 1 || argKeys[0] !== '$') {
167
+ return args;
168
+ }
169
+ // Get the schema's required parameters
170
+ const required = toolSchema?.required;
171
+ if (!Array.isArray(required) || required.length === 0) {
172
+ // No required parameters defined, try first property
173
+ const properties = toolSchema?.properties;
174
+ if (properties && typeof properties === 'object') {
175
+ const firstProp = Object.keys(properties)[0];
176
+ if (firstProp) {
177
+ return { [firstProp]: args['$'] };
178
+ }
179
+ }
180
+ return args;
181
+ }
182
+ // Map "$" to the first required parameter
183
+ const firstRequired = required[0];
184
+ return { [firstRequired]: args['$'] };
185
+ }
186
+ /**
187
+ * Validate and correct tool argument types to match schema requirements
188
+ *
189
+ * Problem: LLMs often generate incorrect types for tool arguments:
190
+ * - Strings instead of arrays: "Cantonese" instead of ["Cantonese"]
191
+ * - Strings instead of numbers: "5" instead of 5
192
+ * - Invalid enum values or empty strings
193
+ *
194
+ * Solution: Automatically correct common type mismatches based on schema
195
+ */
196
+ function validateAndCorrectToolArgs(args, toolSchema) {
197
+ if (!args || typeof args !== 'object' || !toolSchema?.properties) {
198
+ logger.debug(`Skipping type correction - invalid input`, {
199
+ hasArgs: !!args,
200
+ argsType: typeof args,
201
+ hasSchema: !!toolSchema,
202
+ hasProperties: !!toolSchema?.properties,
203
+ schemaKeys: toolSchema ? Object.keys(toolSchema) : []
204
+ });
205
+ return args;
206
+ }
207
+ const corrected = {};
208
+ const corrections = [];
209
+ logger.debug(`Starting type correction`, {
210
+ argKeys: Object.keys(args),
211
+ schemaProps: Object.keys(toolSchema.properties),
212
+ schemaPropsDetail: JSON.stringify(toolSchema.properties, null, 2)
213
+ });
214
+ const requiredParams = toolSchema.required || [];
215
+ for (const [key, value] of Object.entries(args)) {
216
+ const propSchema = toolSchema.properties[key];
217
+ if (!propSchema) {
218
+ // Property not in schema - pass through as-is
219
+ corrected[key] = value;
220
+ continue;
221
+ }
222
+ // CRITICAL: Omit null/undefined values for optional parameters
223
+ // MCP servers often reject null for optional params, expecting them to be omitted
224
+ if ((value === null || value === undefined) && !requiredParams.includes(key)) {
225
+ corrections.push(`${key}: null/undefined omitted (optional parameter)`);
226
+ continue;
227
+ }
228
+ // Type correction: string to array
229
+ if (propSchema.type === 'array' && typeof value === 'string' && value !== '') {
230
+ corrected[key] = [value];
231
+ corrections.push(`${key}: string -> array`);
232
+ continue;
233
+ }
234
+ // Type correction: string to number
235
+ if (propSchema.type === 'number' && typeof value === 'string') {
236
+ const numValue = parseFloat(value);
237
+ if (!isNaN(numValue)) {
238
+ corrected[key] = numValue;
239
+ corrections.push(`${key}: "${value}" -> ${numValue}`);
240
+ continue;
241
+ }
242
+ }
243
+ // Type correction: invalid or empty enum value
244
+ if (propSchema.enum && Array.isArray(propSchema.enum)) {
245
+ if (value === '' || value === null || value === undefined) {
246
+ // Omit empty/null/undefined enum values - let schema defaults apply
247
+ corrections.push(`${key}: empty value omitted (will use default)`);
248
+ continue;
249
+ }
250
+ if (!propSchema.enum.includes(value)) {
251
+ // Invalid enum value - try case-insensitive match first
252
+ const lowerValue = typeof value === 'string' ? value.toLowerCase() : value;
253
+ const match = propSchema.enum.find((e) => typeof e === 'string' && e.toLowerCase() === lowerValue);
254
+ if (match) {
255
+ corrected[key] = match;
256
+ corrections.push(`${key}: "${value}" -> "${match}" (case correction)`);
257
+ }
258
+ else {
259
+ // No match - omit invalid value to use schema default
260
+ corrections.push(`${key}: invalid "${value}" omitted (expected: ${propSchema.enum.join('|')})`);
261
+ }
262
+ continue;
263
+ }
264
+ }
265
+ // No correction needed - pass through
266
+ corrected[key] = value;
267
+ }
268
+ if (corrections.length > 0) {
269
+ logger.debug(`Tool argument type corrections applied`, {
270
+ corrections,
271
+ originalArgs: JSON.stringify(args),
272
+ correctedArgs: JSON.stringify(corrected)
273
+ });
274
+ }
275
+ return corrected;
276
+ }
277
+ // === UTILITY FUNCTIONS ===
278
+ // Azure OpenAI requires function names: ^[a-zA-Z0-9_\.-]+$
279
+ const sanitize = (s) => s.replace(/[^\w\-\.]/g, '_');
280
+ const nsName = (server, tool) => `${sanitize(server)}_${sanitize(tool)}`;
281
+ function isMCPErrorResponse(response) {
282
+ if (!response || typeof response !== 'object') {
283
+ return null;
284
+ }
285
+ if ('isError' in response && response.isError) {
286
+ const err = response.error;
287
+ const message = typeof err === 'string'
288
+ ? err
289
+ : err?.message ?? 'Unknown MCP tool error';
290
+ const code = typeof err === 'object' && err?.code ? ` (code: ${err.code})` : '';
291
+ return new Error(`MCP tool error${code}: ${message}`);
292
+ }
293
+ if ('type' in response && response.type === 'error') {
294
+ const err = response.error;
295
+ const message = typeof err === 'string'
296
+ ? err
297
+ : err?.message ?? 'Unknown MCP tool error';
298
+ return new Error(`MCP tool error: ${message}`);
299
+ }
300
+ return null;
301
+ }
302
+ function isConnectionLevelError(error) {
303
+ if (!error || typeof error !== 'object') {
304
+ return false;
305
+ }
306
+ const message = error instanceof Error ? error.message : String(error);
307
+ const lower = message.toLowerCase();
308
+ const errorCode = error?.code ? String(error.code).toLowerCase() : '';
309
+ const CONNECTION_KEYWORDS = [
310
+ 'connection closed',
311
+ 'connection reset',
312
+ 'socket hang up',
313
+ 'broken pipe',
314
+ 'transport error',
315
+ 'cannot call write after a stream was destroyed',
316
+ 'econnreset',
317
+ 'econnrefused',
318
+ 'network connection lost',
319
+ 'read epipe'
320
+ ];
321
+ return CONNECTION_KEYWORDS.some(keyword => lower.includes(keyword) || errorCode.includes(keyword));
322
+ }
323
+ async function safelyCloseClient(client, context) {
324
+ if (!client) {
325
+ return;
326
+ }
327
+ try {
328
+ await client.close();
329
+ logger.debug(`Closed MCP client`, {
330
+ serverName: context.serverName,
331
+ reason: context.reason
332
+ });
333
+ }
334
+ catch (error) {
335
+ logger.warn(`Failed to close MCP client for ${context.serverName}`, {
336
+ reason: context.reason,
337
+ error: error instanceof Error ? error.message : error
338
+ });
339
+ }
340
+ }
341
+ async function disposeToolCacheEntry(entry, reason) {
342
+ await safelyCloseClient(entry.clientRef.current, {
343
+ serverName: entry.serverName,
344
+ reason
345
+ });
346
+ entry.clientRef.current = null;
347
+ }
348
+ async function disposeAllToolCacheEntries(reason) {
349
+ let disposed = 0;
350
+ for (const [key, entry] of Array.from(toolsCache.entries())) {
351
+ try {
352
+ await disposeToolCacheEntry(entry, reason);
353
+ }
354
+ catch (error) {
355
+ logger.error(`Failed to dispose cache entry for ${entry.serverName}`, {
356
+ serverName: entry.serverName,
357
+ reason,
358
+ error: error instanceof Error ? error.message : error
359
+ });
360
+ }
361
+ finally {
362
+ // Always delete from cache to prevent memory leaks
363
+ toolsCache.delete(key);
364
+ disposed++;
365
+ }
366
+ }
367
+ return disposed;
368
+ }
369
+ // === TOOL CACHE KEY FUNCTIONS ===
370
+ /**
371
+ * Generate cache key for server-level tool caching
372
+ * Uses server name as the primary cache key
373
+ */
374
+ function getToolCacheKey(serverName) {
375
+ return sanitize(serverName);
376
+ }
377
+ /**
378
+ * Generate cache key for individual tool (if needed in future)
379
+ * Format: serverName:toolName
380
+ */
381
+ function getIndividualToolKey(serverName, toolName) {
382
+ return `${sanitize(serverName)}:${sanitize(toolName)}`;
383
+ }
384
+ // === TOOL CACHE VALIDATION ===
385
+ /**
386
+ * Check if cached tools are still valid
387
+ * Validates against config changes and TTL expiration
388
+ */
389
+ function isCacheValid(cached, currentConfig) {
390
+ // Check if server config changed
391
+ const currentHash = generateServerId(currentConfig);
392
+ if (cached.serverConfigHash !== currentHash) {
393
+ logger.debug(`Tools cache invalid: config changed for ${cached.serverName}`, {
394
+ serverName: cached.serverName,
395
+ cachedHash: cached.serverConfigHash.slice(0, 8),
396
+ currentHash: currentHash.slice(0, 8)
397
+ });
398
+ return false;
399
+ }
400
+ // Check TTL expiration
401
+ const ttl = cached.ttl || DEFAULT_TTL;
402
+ if (ttl > 0 && Date.now() - cached.cachedAt.getTime() > ttl) {
403
+ logger.debug(`Tools cache invalid: TTL expired for ${cached.serverName}`, {
404
+ serverName: cached.serverName,
405
+ cachedAt: cached.cachedAt.toISOString(),
406
+ ttl: ttl,
407
+ age: Date.now() - cached.cachedAt.getTime()
408
+ });
409
+ return false;
410
+ }
411
+ return true;
412
+ }
413
+ /**
414
+ * Evict oldest cache entries when cache size exceeds limit
415
+ */
416
+ async function evictOldestCacheEntries() {
417
+ if (toolsCache.size <= MAX_CACHE_ENTRIES)
418
+ return;
419
+ const entries = Array.from(toolsCache.entries())
420
+ .sort(([, a], [, b]) => a.cachedAt.getTime() - b.cachedAt.getTime());
421
+ const toEvict = entries.slice(0, entries.length - MAX_CACHE_ENTRIES);
422
+ for (const [key, entry] of toEvict) {
423
+ await disposeToolCacheEntry(entry, 'cache-eviction');
424
+ toolsCache.delete(key);
425
+ logger.debug(`Evicted old tools cache entry: ${entry.serverName}`, {
426
+ serverName: entry.serverName,
427
+ cachedAt: entry.cachedAt.toISOString(),
428
+ age: Date.now() - entry.cachedAt.getTime()
429
+ });
430
+ }
431
+ }
432
+ // === AZURE OPENAI COMPATIBILITY ===
433
+ /**
434
+ * Create simplified schema for Azure OpenAI compatibility
435
+ * Works with bulletproof schema normalization to ensure clean schema structure
436
+ */
437
+ function createSimpleSchema(originalSchema) {
438
+ // Always return a fresh, clean object
439
+ const baseSchema = {
440
+ type: 'object',
441
+ properties: {},
442
+ additionalProperties: false
443
+ };
444
+ // For tools with no parameters or empty parameters, use minimal schema
445
+ if (!originalSchema ||
446
+ !originalSchema.properties ||
447
+ typeof originalSchema.properties !== 'object' ||
448
+ Object.keys(originalSchema.properties).length === 0) {
449
+ return baseSchema;
450
+ }
451
+ // For tools with parameters, create simplified property definitions
452
+ const simpleProperties = {};
453
+ for (const [propName, propDef] of Object.entries(originalSchema.properties)) {
454
+ const prop = propDef;
455
+ // Simplify property types for better compatibility
456
+ if (prop.type === 'string') {
457
+ simpleProperties[propName] = { type: 'string' };
458
+ }
459
+ else if (prop.type === 'number' || prop.type === 'integer') {
460
+ simpleProperties[propName] = { type: 'number' };
461
+ }
462
+ else if (prop.type === 'boolean') {
463
+ simpleProperties[propName] = { type: 'boolean' };
464
+ }
465
+ else if (prop.type === 'array') {
466
+ simpleProperties[propName] = {
467
+ type: 'array',
468
+ items: { type: 'string' } // Simplify array items to string
469
+ };
470
+ }
471
+ else {
472
+ // Default everything else to string for schema simplicity
473
+ simpleProperties[propName] = { type: 'string' };
474
+ }
475
+ // Add description if available
476
+ if (prop.description && typeof prop.description === 'string') {
477
+ simpleProperties[propName].description = prop.description;
478
+ }
479
+ }
480
+ return {
481
+ ...baseSchema,
482
+ properties: simpleProperties,
483
+ ...(originalSchema.required && Array.isArray(originalSchema.required) ?
484
+ { required: [...originalSchema.required] } : {})
485
+ };
486
+ } /**
487
+ * Validate and bulletproof JSON schema for Azure OpenAI compatibility
488
+ * Uses double normalization: bulletproof + simplification for maximum protection
489
+ */
490
+ export function validateToolSchema(schema) {
491
+ return createSimpleSchema(schema);
492
+ }
493
+ // === CONFIGURATION PARSING ===
494
+ /**
495
+ * Convert MCP config JSON format to normalized server configs
496
+ * Supports both 'servers' and 'mcpServers' field names
497
+ */
498
+ export function parseServersFromConfig(config) {
499
+ const servers = [];
500
+ // Support both 'servers' and 'mcpServers' fields
501
+ const serverDefs = config.servers || config.mcpServers;
502
+ if (!serverDefs) {
503
+ return servers;
504
+ }
505
+ for (const [name, serverDef] of Object.entries(serverDefs)) {
506
+ if ('command' in serverDef) {
507
+ // Stdio transport (default)
508
+ servers.push({
509
+ name,
510
+ transport: serverDef.transport || 'stdio',
511
+ command: serverDef.command,
512
+ args: serverDef.args,
513
+ env: serverDef.env
514
+ });
515
+ }
516
+ else if ('url' in serverDef) {
517
+ // HTTP/SSE transport - handle both 'transport' and legacy 'type' fields
518
+ const transportType = ('transport' in serverDef)
519
+ ? serverDef.transport
520
+ : ('type' in serverDef)
521
+ ? (serverDef.type === 'http' ? 'streamable-http' : serverDef.type)
522
+ : 'streamable-http';
523
+ servers.push({
524
+ name,
525
+ transport: transportType,
526
+ url: serverDef.url,
527
+ headers: serverDef.headers
528
+ });
529
+ }
530
+ }
531
+ return servers;
532
+ }
533
+ // === CLIENT CONNECTION ===
534
+ /**
535
+ * Connect to an MCP server using the specified configuration
536
+ */
537
+ export async function connectMCPServer(serverConfig) {
538
+ // Debug log: Connection details being used
539
+ logger.debug(`MCP server connection attempt`, {
540
+ serverName: serverConfig.name,
541
+ transport: serverConfig.transport,
542
+ connectionConfig: serverConfig.transport === 'stdio' ? {
543
+ command: serverConfig.command,
544
+ args: serverConfig.args,
545
+ env: serverConfig.env ? Object.keys(serverConfig.env) : []
546
+ } : {
547
+ url: serverConfig.url,
548
+ headers: serverConfig.headers ? Object.keys(serverConfig.headers) : []
549
+ }
550
+ });
551
+ // Handle traditional MCP SDK transports (stdio, sse, streamable-http)
552
+ const transportType = serverConfig.transport || 'stdio';
553
+ const transport = transportType === 'stdio'
554
+ ? new StdioClientTransport({
555
+ command: serverConfig.command,
556
+ args: serverConfig.args ?? [],
557
+ env: serverConfig.env
558
+ })
559
+ : transportType === 'sse'
560
+ ? new SSEClientTransport(new URL(serverConfig.url), {
561
+ requestInit: { headers: serverConfig.headers }
562
+ })
563
+ : new StreamableHTTPClientTransport(new URL(serverConfig.url), {
564
+ requestInit: { headers: serverConfig.headers }
565
+ });
566
+ const client = new Client({ name: 'my-app', version: '1.0.0' }, { capabilities: {} });
567
+ logger.debug(`MCP server transport created, initiating connection`, {
568
+ serverName: serverConfig.name,
569
+ transport: serverConfig.transport
570
+ });
571
+ await client.connect(transport);
572
+ logger.debug(`MCP server connection established successfully`, {
573
+ serverName: serverConfig.name,
574
+ transport: serverConfig.transport
575
+ });
576
+ return client;
577
+ }
578
+ // === TOOL CONVERSION ===
579
+ /**
580
+ * Bulletproof schema normalization to prevent AI SDK corruption
581
+ * This is our surgical fix - normalize schemas right before they go to AI SDK
582
+ */
583
+ function bulletproofSchema(schema) {
584
+ if (!schema || typeof schema !== 'object') {
585
+ return {
586
+ type: 'object',
587
+ properties: {},
588
+ additionalProperties: false
589
+ };
590
+ }
591
+ // Create a completely fresh object to avoid any corruption
592
+ const normalized = {
593
+ type: 'object',
594
+ properties: {},
595
+ additionalProperties: false
596
+ };
597
+ // Copy properties safely - preserve critical schema information for validation
598
+ if (schema.properties && typeof schema.properties === 'object') {
599
+ for (const [key, value] of Object.entries(schema.properties)) {
600
+ const prop = value;
601
+ normalized.properties[key] = {
602
+ type: prop?.type || 'string',
603
+ ...(prop?.description && { description: prop.description }),
604
+ // Preserve enum values for validation
605
+ ...(prop?.enum && Array.isArray(prop.enum) && { enum: prop.enum }),
606
+ // Preserve array item schema
607
+ ...(prop?.items && { items: prop.items }),
608
+ // Preserve numeric constraints
609
+ ...(prop?.minimum !== undefined && { minimum: prop.minimum }),
610
+ ...(prop?.maximum !== undefined && { maximum: prop.maximum })
611
+ };
612
+ }
613
+ }
614
+ // Copy required array safely
615
+ if (schema.required && Array.isArray(schema.required)) {
616
+ normalized.required = [...schema.required];
617
+ }
618
+ return normalized;
619
+ }
620
+ // === TOOL CACHE OPERATIONS ===
621
+ /**
622
+ * Fetch tools from MCP server using ephemeral connection and cache results
623
+ * Handles connection lifecycle and error recovery
624
+ */
625
+ async function fetchAndCacheTools(serverConfig) {
626
+ const startTime = performance.now();
627
+ const cacheKey = getToolCacheKey(serverConfig.name);
628
+ logger.debug(`Fetching and caching tools for server: ${serverConfig.name}`, {
629
+ serverName: serverConfig.name,
630
+ cacheKey,
631
+ transport: serverConfig.transport
632
+ });
633
+ const clientRef = { current: null, reconnecting: null };
634
+ let cacheEntry = null;
635
+ try {
636
+ // Create ephemeral connection
637
+ clientRef.current = await connectMCPServer(serverConfig);
638
+ const reconnectClient = async (reason) => {
639
+ // Prevent concurrent reconnection attempts
640
+ if (clientRef.reconnecting) {
641
+ logger.debug(`Reconnection already in progress, waiting...`, {
642
+ serverName: serverConfig.name,
643
+ reason
644
+ });
645
+ await clientRef.reconnecting;
646
+ return;
647
+ }
648
+ clientRef.reconnecting = (async () => {
649
+ logger.warn(`Reconnecting MCP client after failure`, {
650
+ serverName: serverConfig.name,
651
+ reason
652
+ });
653
+ const previousClient = clientRef.current;
654
+ clientRef.current = null;
655
+ await safelyCloseClient(previousClient, {
656
+ serverName: serverConfig.name,
657
+ reason: `${reason}-reconnect`
658
+ });
659
+ try {
660
+ const newClient = await connectMCPServer(serverConfig);
661
+ clientRef.current = newClient;
662
+ if (cacheEntry) {
663
+ cacheEntry.cachedAt = new Date();
664
+ }
665
+ }
666
+ catch (reconnectError) {
667
+ logger.error(`Failed to reconnect MCP client`, {
668
+ serverName: serverConfig.name,
669
+ reason,
670
+ error: reconnectError instanceof Error ? reconnectError.message : reconnectError
671
+ });
672
+ throw reconnectError;
673
+ }
674
+ })();
675
+ try {
676
+ await clientRef.reconnecting;
677
+ }
678
+ finally {
679
+ clientRef.reconnecting = null;
680
+ }
681
+ };
682
+ // Fetch and convert tools
683
+ const tools = await mcpToolsToAiTools(clientRef, serverConfig, reconnectClient);
684
+ // Cache the results
685
+ cacheEntry = {
686
+ tools,
687
+ cachedAt: new Date(),
688
+ serverConfigHash: generateServerId(serverConfig),
689
+ serverName: serverConfig.name,
690
+ ttl: DEFAULT_TTL,
691
+ clientRef,
692
+ reconnectClient,
693
+ serverConfig
694
+ };
695
+ const existingEntry = toolsCache.get(cacheKey);
696
+ if (existingEntry) {
697
+ await disposeToolCacheEntry(existingEntry, 'cache-refresh');
698
+ toolsCache.delete(cacheKey);
699
+ }
700
+ toolsCache.set(cacheKey, cacheEntry);
701
+ // Evict old entries if needed
702
+ await evictOldestCacheEntries();
703
+ const duration = performance.now() - startTime;
704
+ const toolCount = Object.keys(tools).length;
705
+ logger.debug(`Successfully cached tools for server: ${serverConfig.name}`, {
706
+ serverName: serverConfig.name,
707
+ toolCount,
708
+ duration: Math.round(duration * 100) / 100,
709
+ cacheSize: toolsCache.size
710
+ });
711
+ return tools;
712
+ }
713
+ catch (error) {
714
+ const duration = performance.now() - startTime;
715
+ const errorMessage = error instanceof Error ? error.message : String(error);
716
+ await safelyCloseClient(clientRef.current, {
717
+ serverName: serverConfig.name,
718
+ reason: 'fetch-failed'
719
+ });
720
+ clientRef.current = null;
721
+ logger.warn(`Failed to fetch and cache tools for server: ${serverConfig.name}`, {
722
+ serverName: serverConfig.name,
723
+ error: errorMessage,
724
+ duration: Math.round(duration * 100) / 100
725
+ });
726
+ // Return empty tools object on failure - don't break entire tool fetch
727
+ return {};
728
+ }
729
+ }
730
+ /**
731
+ * Convert MCP tools to AI-compatible tool format with bulletproof schema protection
732
+ */
733
+ export async function mcpToolsToAiTools(clientRef, serverConfig, reconnectClient) {
734
+ const serverName = serverConfig.name;
735
+ const transport = serverConfig.transport || 'stdio';
736
+ const ensureClient = () => {
737
+ if (!clientRef.current) {
738
+ throw new Error(`MCP client not connected for server: ${serverName}`);
739
+ }
740
+ return clientRef.current;
741
+ };
742
+ logger.debug(`MCP tools list request starting`, {
743
+ serverName,
744
+ operation: 'listTools',
745
+ transport
746
+ });
747
+ let toolsResponse;
748
+ // Use MCP SDK client with timeout
749
+ const listToolsPromise = ensureClient().listTools();
750
+ const timeoutPromise = new Promise((_, reject) => {
751
+ setTimeout(() => reject(new Error('Timeout waiting for tools list')), 5000);
752
+ });
753
+ toolsResponse = await Promise.race([listToolsPromise, timeoutPromise]);
754
+ const { tools } = toolsResponse;
755
+ // Debug log: Tools data received from MCP server
756
+ logger.debug(`MCP server tools list response`, {
757
+ serverName,
758
+ operation: 'listTools',
759
+ toolsCount: tools.length,
760
+ toolNames: tools.map((t) => t.name),
761
+ toolsPayload: JSON.stringify(tools, null, 2)
762
+ });
763
+ const aiTools = {};
764
+ for (const t of tools) {
765
+ const key = nsName(serverName, t.name);
766
+ // Debug: Log original tool definition from MCP server
767
+ logger.debug(`MCP original tool definition from server: ${serverName}`, {
768
+ toolName: t.name,
769
+ originalDescription: t.description,
770
+ inputSchema: JSON.stringify(t.inputSchema, null, 2)
771
+ });
772
+ // Apply bulletproof schema normalization - this is our surgical fix
773
+ const bulletproofedSchema = bulletproofSchema(t.inputSchema);
774
+ // Validate and simplify further for Azure compatibility
775
+ const finalSchema = validateToolSchema(bulletproofedSchema);
776
+ // Enhance tool description with explicit usage guidance for execute_command
777
+ let enhancedDescription = t.description ?? '';
778
+ if (t.name === 'execute_command') {
779
+ const originalDesc = t.description ?? '';
780
+ enhancedDescription = 'Execute a user-requested shell command only when the user explicitly asks to run one. Do not use for greetings, general Q&A, or normal conversation. Contract: command must be a single executable token and arguments must be passed as separate argv tokens (no mini scripts). Keep execution inside the trusted working directory/scope configured by runtime guardrails. Reject out-of-scope path requests, shell control syntax (`&&`, `||`, pipes, redirects, substitution, backgrounding), and inline eval/script modes such as `sh -c`, `node -e`, `python -c`, or `powershell -Command`. Because commands run through an OS shell, do not execute untrusted command text.';
781
+ logger.debug(`Enhanced tool description for execute_command`, {
782
+ toolName: t.name,
783
+ originalDescription: originalDesc,
784
+ enhancedDescription: enhancedDescription,
785
+ descriptionChanged: originalDesc !== enhancedDescription
786
+ });
787
+ }
788
+ aiTools[key] = {
789
+ description: enhancedDescription,
790
+ parameters: finalSchema,
791
+ // Tool metadata
792
+ location: 'server',
793
+ execute: async (args, sequenceId, parentToolCall, context) => {
794
+ const startTime = performance.now();
795
+ const executionId = `${serverName}-${t.name}-${Date.now()}`;
796
+ const chatId = context?.chatId ?? context?.world?.currentChatId ?? null;
797
+ const worldId = context?.worldId ?? context?.world?.id ?? null;
798
+ const agentId = context?.agentId ?? null;
799
+ logger.debug(`MCP tool execution starting via AI conversion`, {
800
+ executionId,
801
+ serverName,
802
+ toolName: t.name,
803
+ toolKey: key,
804
+ sequenceId,
805
+ parentToolCall,
806
+ argsPresent: !!args,
807
+ argsKeys: args ? Object.keys(args) : [],
808
+ transport,
809
+ worldId,
810
+ chatId,
811
+ agentId
812
+ });
813
+ // NOTE: Tool safety checks are handled in tool-utils.ts wrapToolWithValidation().
814
+ // This ensures consistent execution guardrails for all tools (MCP and built-in).
815
+ // Debug log: Request data being sent to MCP server
816
+ // OLLAMA BUG FIX: Translate "$" arguments to proper parameter names
817
+ let translatedArgs = translateOllamaArguments(args ?? {}, t.inputSchema);
818
+ // TYPE CORRECTION: Validate and fix argument types to match schema
819
+ translatedArgs = validateAndCorrectToolArgs(translatedArgs, t.inputSchema);
820
+ const requestPayload = { name: t.name, arguments: translatedArgs };
821
+ logger.debug(`MCP server request payload`, {
822
+ executionId,
823
+ serverName,
824
+ toolName: t.name,
825
+ requestPayload: JSON.stringify(requestPayload, null, 2)
826
+ });
827
+ const maxAttempts = 2;
828
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
829
+ try {
830
+ const res = await ensureClient().callTool(requestPayload);
831
+ const mcpError = isMCPErrorResponse(res);
832
+ if (mcpError) {
833
+ logger.error(`MCP tool execution returned error payload`, {
834
+ executionId,
835
+ serverName,
836
+ toolName: t.name,
837
+ toolKey: key,
838
+ error: mcpError.message,
839
+ transport
840
+ });
841
+ throw mcpError;
842
+ }
843
+ // Debug log: Raw response data received from MCP server
844
+ logger.debug(`MCP server response payload`, {
845
+ executionId,
846
+ serverName,
847
+ toolName: t.name,
848
+ responsePayload: JSON.stringify(res, null, 2),
849
+ responseType: typeof res,
850
+ hasContent: !!(res?.content),
851
+ contentLength: Array.isArray(res?.content) ? res.content.length : 0
852
+ });
853
+ const duration = performance.now() - startTime;
854
+ // Handle result content - prefer text > json > fallback
855
+ let processedResult;
856
+ let resultType = 'unknown';
857
+ if (res?.content && Array.isArray(res.content)) {
858
+ const textPart = res.content.find((p) => p?.type === 'text');
859
+ if (textPart?.text) {
860
+ processedResult = textPart.text;
861
+ resultType = 'text';
862
+ }
863
+ else {
864
+ const jsonPart = res.content.find((p) => p?.type === 'json');
865
+ if (jsonPart?.json) {
866
+ processedResult = jsonPart.json;
867
+ resultType = 'json';
868
+ }
869
+ }
870
+ }
871
+ if (!processedResult) {
872
+ processedResult = JSON.stringify(res);
873
+ resultType = 'serialized';
874
+ }
875
+ const resultSize = typeof processedResult === 'string' ? processedResult.length : JSON.stringify(processedResult).length;
876
+ logger.debug(`MCP tool execution completed via AI conversion`, {
877
+ executionId,
878
+ serverName,
879
+ toolName: t.name,
880
+ toolKey: key,
881
+ sequenceId,
882
+ parentToolCall,
883
+ status: 'success',
884
+ duration: Math.round(duration * 100) / 100,
885
+ resultType,
886
+ resultSize,
887
+ resultPreview: typeof processedResult === 'string'
888
+ ? processedResult.slice(0, 200) + (resultSize > 200 ? '...' : '')
889
+ : JSON.stringify(processedResult).slice(0, 200) + '...',
890
+ transport
891
+ });
892
+ return processedResult;
893
+ }
894
+ catch (error) {
895
+ const duration = performance.now() - startTime;
896
+ const errorMessage = error instanceof Error ? error.message : String(error);
897
+ const isLastAttempt = attempt === maxAttempts - 1;
898
+ const shouldRetry = !isLastAttempt && isConnectionLevelError(error);
899
+ if (shouldRetry) {
900
+ logger.warn(`MCP tool execution detected transport issue - attempting reconnect`, {
901
+ executionId,
902
+ serverName,
903
+ toolName: t.name,
904
+ toolKey: key,
905
+ status: 'retrying',
906
+ attempt: attempt + 1,
907
+ maxAttempts,
908
+ duration: Math.round(duration * 100) / 100,
909
+ error: errorMessage,
910
+ transport
911
+ });
912
+ try {
913
+ await reconnectClient('call-tool-failure');
914
+ continue;
915
+ }
916
+ catch (reconnectError) {
917
+ logger.error(`MCP tool execution reconnect failed`, {
918
+ executionId,
919
+ serverName,
920
+ toolName: t.name,
921
+ toolKey: key,
922
+ error: reconnectError instanceof Error ? reconnectError.message : reconnectError,
923
+ transport
924
+ });
925
+ throw reconnectError;
926
+ }
927
+ }
928
+ logger.error(`MCP tool execution failed via AI conversion: ${errorMessage}`, {
929
+ executionId,
930
+ serverName,
931
+ toolName: t.name,
932
+ toolKey: key,
933
+ sequenceId,
934
+ parentToolCall,
935
+ status: 'error',
936
+ duration: Math.round(duration * 100) / 100,
937
+ error: errorMessage,
938
+ errorStack: error instanceof Error ? error.stack : undefined,
939
+ transport
940
+ });
941
+ throw error;
942
+ }
943
+ }
944
+ },
945
+ };
946
+ }
947
+ return aiTools;
948
+ }
949
+ // === SERVER REGISTRY STATE ===
950
+ // Module-level server registry state
951
+ const serverRegistry = new Map();
952
+ const worldServerMapping = new Map(); // worldId -> Set of serverIds
953
+ // Module-level tool cache state
954
+ const toolsCache = new Map(); // serverName -> cached tools
955
+ const DEFAULT_TTL = 60 * 60 * 1000; // 1 hour default TTL
956
+ const MAX_CACHE_ENTRIES = 100; // Maximum cached servers
957
+ let isInitialized = false;
958
+ let shutdownInProgress = false;
959
+ // === CORE REGISTRY FUNCTIONS ===
960
+ /**
961
+ * Generate unique server ID based on configuration hash for sharing
962
+ */
963
+ function generateServerId(config) {
964
+ const configString = JSON.stringify({
965
+ name: config.name,
966
+ transport: config.transport,
967
+ ...(config.transport === 'stdio' ? {
968
+ command: config.command,
969
+ args: config.args,
970
+ env: config.env
971
+ } : {
972
+ url: config.url,
973
+ headers: config.headers
974
+ })
975
+ }, null, 0);
976
+ return createHash('sha256').update(configString).digest('hex');
977
+ }
978
+ /**
979
+ * Initialize MCP registry - called on Express app startup
980
+ */
981
+ export function initializeMCPRegistry() {
982
+ if (isInitialized) {
983
+ logger.warn('MCP registry already initialized');
984
+ return;
985
+ }
986
+ isInitialized = true;
987
+ shutdownInProgress = false;
988
+ logger.info('MCP registry initialized');
989
+ }
990
+ /**
991
+ * Register and start an MCP server with reference counting
992
+ * Returns the server ID for tracking. Reuses existing servers when possible.
993
+ */
994
+ export async function registerMCPServer(config, worldId) {
995
+ if (shutdownInProgress) {
996
+ throw new Error('MCP registry is shutting down');
997
+ }
998
+ const serverId = generateServerId(config);
999
+ let serverInstance = serverRegistry.get(serverId);
1000
+ if (serverInstance) {
1001
+ // Reuse existing server
1002
+ serverInstance.referenceCount++;
1003
+ serverInstance.associatedWorlds.add(worldId);
1004
+ const worldServers = worldServerMapping.get(worldId) || new Set();
1005
+ worldServers.add(serverId);
1006
+ worldServerMapping.set(worldId, worldServers);
1007
+ logger.debug(`Reusing existing MCP server: ${config.name}`, {
1008
+ serverId: serverId.slice(0, 8),
1009
+ referenceCount: serverInstance.referenceCount,
1010
+ status: serverInstance.status
1011
+ });
1012
+ return serverId;
1013
+ }
1014
+ // Create new server instance
1015
+ serverInstance = {
1016
+ id: serverId,
1017
+ config,
1018
+ client: null,
1019
+ status: 'starting',
1020
+ referenceCount: 1,
1021
+ startedAt: new Date(),
1022
+ lastHealthCheck: new Date(),
1023
+ associatedWorlds: new Set([worldId])
1024
+ };
1025
+ serverRegistry.set(serverId, serverInstance);
1026
+ const worldServers = worldServerMapping.get(worldId) || new Set();
1027
+ worldServers.add(serverId);
1028
+ worldServerMapping.set(worldId, worldServers);
1029
+ logger.info(`Starting MCP server: ${config.name}`, {
1030
+ serverId: serverId.slice(0, 8),
1031
+ transport: config.transport,
1032
+ worldId
1033
+ });
1034
+ // Debug log: Full server configuration being used
1035
+ logger.debug(`MCP server registration configuration`, {
1036
+ serverId: serverId.slice(0, 8),
1037
+ serverName: config.name,
1038
+ worldId,
1039
+ fullConfig: JSON.stringify(config, null, 2)
1040
+ });
1041
+ // Start server and wait for ready state
1042
+ try {
1043
+ await startServerAsync(serverInstance);
1044
+ logger.debug(`MCP server ready: ${config.name}`, {
1045
+ serverId: serverId.slice(0, 8),
1046
+ status: serverInstance.status
1047
+ });
1048
+ }
1049
+ catch (error) {
1050
+ logger.error(`Failed to start MCP server: ${config.name}`, {
1051
+ serverId: serverId.slice(0, 8),
1052
+ error: error instanceof Error ? error.message : error
1053
+ });
1054
+ serverInstance.status = 'error';
1055
+ serverInstance.error = error instanceof Error ? error : new Error(String(error));
1056
+ throw error;
1057
+ }
1058
+ return serverId;
1059
+ }
1060
+ /**
1061
+ * Unregister MCP server for a world - decreases reference count
1062
+ * Schedules shutdown when no more references exist
1063
+ */
1064
+ export async function unregisterMCPServer(serverId, worldId) {
1065
+ const serverInstance = serverRegistry.get(serverId);
1066
+ if (!serverInstance)
1067
+ return false;
1068
+ serverInstance.associatedWorlds.delete(worldId);
1069
+ serverInstance.referenceCount = Math.max(0, serverInstance.referenceCount - 1);
1070
+ const worldServers = worldServerMapping.get(worldId);
1071
+ if (worldServers) {
1072
+ worldServers.delete(serverId);
1073
+ if (worldServers.size === 0)
1074
+ worldServerMapping.delete(worldId);
1075
+ }
1076
+ logger.debug(`Unregistered MCP server for world: ${worldId}`, {
1077
+ serverId: serverId.slice(0, 8),
1078
+ referenceCount: serverInstance.referenceCount
1079
+ });
1080
+ // Schedule shutdown after 30s if no more references
1081
+ if (serverInstance.referenceCount === 0) {
1082
+ logger.info(`Scheduling MCP server shutdown: ${serverInstance.config.name}`, {
1083
+ serverId: serverId.slice(0, 8)
1084
+ });
1085
+ setTimeout(async () => {
1086
+ if (serverInstance.referenceCount === 0) {
1087
+ await stopServer(serverInstance);
1088
+ serverRegistry.delete(serverId);
1089
+ }
1090
+ }, 30000);
1091
+ }
1092
+ return true;
1093
+ }
1094
+ // === REGISTRY ACCESS FUNCTIONS ===
1095
+ /** Get MCP server instance by ID */
1096
+ export function getMCPServer(serverId) {
1097
+ return serverRegistry.get(serverId) || null;
1098
+ }
1099
+ /** List all MCP servers */
1100
+ export function listMCPServers() {
1101
+ return Array.from(serverRegistry.values());
1102
+ }
1103
+ /** Get server IDs for a specific world */
1104
+ export function getMCPServersForWorld(worldId) {
1105
+ const worldServers = worldServerMapping.get(worldId);
1106
+ return worldServers ? Array.from(worldServers) : [];
1107
+ }
1108
+ // === LIFECYCLE MANAGEMENT ===
1109
+ /** Shutdown all MCP servers - called on Express app shutdown */
1110
+ export async function shutdownAllMCPServers() {
1111
+ if (shutdownInProgress) {
1112
+ logger.warn('Shutdown already in progress');
1113
+ return;
1114
+ }
1115
+ shutdownInProgress = true;
1116
+ logger.info(`Shutting down ${serverRegistry.size} MCP servers`);
1117
+ const shutdownPromises = Array.from(serverRegistry.values()).map(async (serverInstance) => {
1118
+ try {
1119
+ await stopServer(serverInstance);
1120
+ }
1121
+ catch (error) {
1122
+ logger.error(`Error shutting down MCP server: ${serverInstance.config.name}`, {
1123
+ serverId: serverInstance.id.slice(0, 8),
1124
+ error: error instanceof Error ? error.message : error
1125
+ });
1126
+ }
1127
+ });
1128
+ await Promise.allSettled(shutdownPromises);
1129
+ serverRegistry.clear();
1130
+ worldServerMapping.clear();
1131
+ // Clear tools cache during shutdown
1132
+ const cacheEntriesCleared = await disposeAllToolCacheEntries('shutdown');
1133
+ isInitialized = false;
1134
+ shutdownInProgress = false;
1135
+ logger.info('All MCP servers shut down', {
1136
+ cacheEntriesCleared
1137
+ });
1138
+ }
1139
+ // === INTERNAL HELPER FUNCTIONS ===
1140
+ /** Start server asynchronously */
1141
+ async function startServerAsync(serverInstance) {
1142
+ try {
1143
+ const client = await connectMCPServer(serverInstance.config);
1144
+ serverInstance.client = client;
1145
+ serverInstance.status = 'running';
1146
+ serverInstance.lastHealthCheck = new Date();
1147
+ logger.info(`MCP server started successfully: ${serverInstance.config.name}`, {
1148
+ serverId: serverInstance.id.slice(0, 8),
1149
+ referenceCount: serverInstance.referenceCount,
1150
+ transport: serverInstance.config.transport
1151
+ });
1152
+ }
1153
+ catch (error) {
1154
+ serverInstance.status = 'error';
1155
+ serverInstance.error = error instanceof Error ? error : new Error(String(error));
1156
+ throw error;
1157
+ }
1158
+ }
1159
+ /** Stop server and cleanup resources */
1160
+ async function stopServer(serverInstance) {
1161
+ if (serverInstance.status === 'stopping' || serverInstance.status === 'error') {
1162
+ return;
1163
+ }
1164
+ serverInstance.status = 'stopping';
1165
+ if (serverInstance.client) {
1166
+ try {
1167
+ await safelyCloseClient(serverInstance.client, {
1168
+ serverName: serverInstance.config.name,
1169
+ reason: 'stop-server'
1170
+ });
1171
+ serverInstance.client = null;
1172
+ logger.info(`MCP server stopped: ${serverInstance.config.name}`, {
1173
+ serverId: serverInstance.id.slice(0, 8)
1174
+ });
1175
+ }
1176
+ catch (error) {
1177
+ logger.error(`Error stopping MCP server: ${serverInstance.config.name}`, {
1178
+ serverId: serverInstance.id.slice(0, 8),
1179
+ error: error instanceof Error ? error.message : error
1180
+ });
1181
+ }
1182
+ }
1183
+ }
1184
+ // === MONITORING AND UTILITIES ===
1185
+ /** Health check for MCP servers - can be called periodically */
1186
+ export function performHealthCheck() {
1187
+ const now = new Date();
1188
+ for (const serverInstance of serverRegistry.values()) {
1189
+ if (serverInstance.status === 'running' && serverInstance.client) {
1190
+ serverInstance.lastHealthCheck = now;
1191
+ }
1192
+ }
1193
+ }
1194
+ /** Get registry statistics */
1195
+ export function getMCPRegistryStats() {
1196
+ const servers = Array.from(serverRegistry.values());
1197
+ return {
1198
+ totalServers: servers.length,
1199
+ runningServers: servers.filter(s => s.status === 'running').length,
1200
+ errorServers: servers.filter(s => s.status === 'error').length,
1201
+ totalWorlds: worldServerMapping.size
1202
+ };
1203
+ }
1204
+ // === CONFIGURATION VALIDATION ===
1205
+ /** Validate MCP configuration format and content */
1206
+ export function validateMCPConfig(config) {
1207
+ try {
1208
+ // Must have either 'servers' or 'mcpServers' field
1209
+ if (!config?.servers && !config?.mcpServers)
1210
+ return false;
1211
+ const serverDefs = config.servers || config.mcpServers;
1212
+ if (typeof serverDefs !== 'object')
1213
+ return false;
1214
+ for (const [serverName, server] of Object.entries(serverDefs)) {
1215
+ if (!serverName || !server || typeof server !== 'object')
1216
+ return false;
1217
+ const serverConfig = server;
1218
+ const transport = serverConfig.transport ||
1219
+ (serverConfig.type === 'http' ? 'streamable-http' : serverConfig.type) ||
1220
+ 'stdio';
1221
+ if (!['stdio', 'sse', 'streamable-http', 'http'].includes(transport))
1222
+ return false;
1223
+ if (transport === 'stdio') {
1224
+ if (!serverConfig.command || typeof serverConfig.command !== 'string')
1225
+ return false;
1226
+ }
1227
+ else {
1228
+ if (!serverConfig.url || typeof serverConfig.url !== 'string')
1229
+ return false;
1230
+ }
1231
+ }
1232
+ return true;
1233
+ }
1234
+ catch (error) {
1235
+ logger.error('Error validating MCP config', { error: error instanceof Error ? error.message : error });
1236
+ return false;
1237
+ }
1238
+ }
1239
+ /** Parse MCP configuration string safely */
1240
+ export function parseMCPConfig(configString) {
1241
+ try {
1242
+ if (!configString?.trim())
1243
+ return null;
1244
+ const config = JSON.parse(configString);
1245
+ return validateMCPConfig(config) ? config : null;
1246
+ }
1247
+ catch (error) {
1248
+ logger.error('Failed to parse MCP config', {
1249
+ error: error instanceof Error ? error.message : error,
1250
+ configString: configString.slice(0, 100) + (configString.length > 100 ? '...' : '')
1251
+ });
1252
+ return null;
1253
+ }
1254
+ }
1255
+ // === HIGH-LEVEL WORLD INTEGRATION ===
1256
+ /** Start MCP servers for a world based on its configuration */
1257
+ export async function startMCPServersForWorld(worldId, mcpConfig) {
1258
+ if (!mcpConfig) {
1259
+ logger.debug(`No MCP config for world: ${worldId}`);
1260
+ return [];
1261
+ }
1262
+ const config = parseMCPConfig(mcpConfig);
1263
+ if (!config) {
1264
+ logger.error(`Invalid MCP config for world: ${worldId}`);
1265
+ return [];
1266
+ }
1267
+ const serverConfigs = parseServersFromConfig(config);
1268
+ const serverIds = [];
1269
+ const startupPromises = [];
1270
+ for (const serverConfig of serverConfigs) {
1271
+ try {
1272
+ const serverIdPromise = registerMCPServer(serverConfig, worldId);
1273
+ startupPromises.push(serverIdPromise.then(serverId => {
1274
+ serverIds.push(serverId);
1275
+ }).catch(error => {
1276
+ logger.error(`Failed to start MCP server: ${serverConfig.name}`, {
1277
+ worldId,
1278
+ error: error instanceof Error ? error.message : error
1279
+ });
1280
+ }));
1281
+ }
1282
+ catch (error) {
1283
+ logger.error(`Error registering MCP server: ${serverConfig.name}`, {
1284
+ worldId,
1285
+ error: error instanceof Error ? error.message : error
1286
+ });
1287
+ }
1288
+ }
1289
+ await Promise.allSettled(startupPromises);
1290
+ logger.info(`Started ${serverIds.length}/${serverConfigs.length} MCP servers for world: ${worldId}`, {
1291
+ serverIds: serverIds.map(id => id.slice(0, 8))
1292
+ });
1293
+ return serverIds;
1294
+ }
1295
+ /** Stop MCP servers for a world - decreases reference counts */
1296
+ export async function stopMCPServersForWorld(worldId) {
1297
+ const worldServers = worldServerMapping.get(worldId);
1298
+ if (!worldServers?.size) {
1299
+ logger.debug(`No MCP servers to stop for world: ${worldId}`);
1300
+ return;
1301
+ }
1302
+ logger.info(`Stopping MCP servers for world: ${worldId}`, {
1303
+ serverCount: worldServers.size,
1304
+ serverIds: Array.from(worldServers).map(id => id.slice(0, 8))
1305
+ });
1306
+ const stopPromises = Array.from(worldServers).map(async (serverId) => {
1307
+ try {
1308
+ await unregisterMCPServer(serverId, worldId);
1309
+ }
1310
+ catch (error) {
1311
+ logger.error(`Error stopping MCP server: ${serverId.slice(0, 8)}`, {
1312
+ worldId,
1313
+ error: error instanceof Error ? error.message : error
1314
+ });
1315
+ }
1316
+ });
1317
+ await Promise.allSettled(stopPromises);
1318
+ }
1319
+ /** Update MCP servers for a world when configuration changes */
1320
+ export async function updateMCPServersForWorld(worldId, newMcpConfig) {
1321
+ logger.info(`Updating MCP servers for world: ${worldId}`);
1322
+ await stopMCPServersForWorld(worldId);
1323
+ return await startMCPServersForWorld(worldId, newMcpConfig);
1324
+ }
1325
+ /**
1326
+ * Get built-in tools that are always available to all worlds
1327
+ * These tools don't require MCP server configuration
1328
+ * Built-in tools:
1329
+ * - shell_cmd: Execute shell commands
1330
+ * - load_skill: Load full SKILL.md instructions by registry skill_id
1331
+ * - create_agent: Create a new agent after explicit user approval
1332
+ * - read_file: Read file contents with pagination controls
1333
+ * - list_files: List directory entries
1334
+ * - grep: Recursive text search across files
1335
+ *
1336
+ * @returns Record of built-in tool definitions
1337
+ */
1338
+ function getBuiltInTools() {
1339
+ const shellCmdTool = createShellCmdToolDefinition();
1340
+ const loadSkillTool = createLoadSkillToolDefinition();
1341
+ const createAgentTool = createCreateAgentToolDefinition();
1342
+ const readFileTool = createReadFileToolDefinition();
1343
+ const listFilesTool = createListFilesToolDefinition();
1344
+ const grepTool = createGrepToolDefinition();
1345
+ return {
1346
+ 'shell_cmd': wrapToolWithValidation(shellCmdTool, 'shell_cmd'),
1347
+ 'load_skill': wrapToolWithValidation(loadSkillTool, 'load_skill'),
1348
+ 'create_agent': wrapToolWithValidation(createAgentTool, 'create_agent'),
1349
+ 'read_file': wrapToolWithValidation(readFileTool, 'read_file'),
1350
+ 'list_files': wrapToolWithValidation(listFilesTool, 'list_files'),
1351
+ 'grep': wrapToolWithValidation(grepTool, 'grep'),
1352
+ 'grep_search': wrapToolWithValidation(grepTool, 'grep_search'),
1353
+ };
1354
+ }
1355
+ /**
1356
+ * Get MCP tools available for a world with on-demand server startup
1357
+ */
1358
+ /**
1359
+ * Get MCP tools available for a world with registry-level caching
1360
+ * Uses cache-first strategy to avoid repeated tool fetching during ephemeral connections
1361
+ * Includes built-in tools (`shell_cmd`, `load_skill`) in addition to MCP server tools
1362
+ */
1363
+ export async function getMCPToolsForWorld(worldId) {
1364
+ const startTime = performance.now();
1365
+ const world = await getWorld(worldId);
1366
+ // Start with built-in tools
1367
+ const allTools = getBuiltInTools();
1368
+ if (!world?.mcpConfig) {
1369
+ logger.debug(`No MCP config for world: ${worldId}, returning built-in tools only`, {
1370
+ worldId,
1371
+ builtInToolCount: Object.keys(allTools).length
1372
+ });
1373
+ return allTools;
1374
+ }
1375
+ const config = parseMCPConfig(world.mcpConfig);
1376
+ if (!config) {
1377
+ logger.error(`Invalid MCP config for world: ${worldId}, returning built-in tools only`, {
1378
+ worldId,
1379
+ builtInToolCount: Object.keys(allTools).length
1380
+ });
1381
+ return allTools;
1382
+ }
1383
+ const serverConfigs = parseServersFromConfig(config);
1384
+ const serverPromises = [];
1385
+ let cacheHits = 0;
1386
+ let cacheMisses = 0;
1387
+ for (const serverConfig of serverConfigs) {
1388
+ const serverPromise = (async () => {
1389
+ try {
1390
+ const cacheKey = getToolCacheKey(serverConfig.name);
1391
+ const cached = toolsCache.get(cacheKey);
1392
+ const cacheIsValid = cached ? isCacheValid(cached, serverConfig) : false;
1393
+ // Check cache first
1394
+ if (cached && cacheIsValid) {
1395
+ // Cache hit
1396
+ cacheHits++;
1397
+ Object.assign(allTools, cached.tools);
1398
+ logger.debug(`Tools cache hit for server: ${serverConfig.name}`, {
1399
+ serverName: serverConfig.name,
1400
+ toolCount: Object.keys(cached.tools).length,
1401
+ age: Date.now() - cached.cachedAt.getTime()
1402
+ });
1403
+ return;
1404
+ }
1405
+ if (cached && !cacheIsValid) {
1406
+ await disposeToolCacheEntry(cached, 'stale-cache-entry');
1407
+ toolsCache.delete(cacheKey);
1408
+ }
1409
+ // Cache miss - fetch and cache tools
1410
+ cacheMisses++;
1411
+ if (cached) {
1412
+ logger.debug(`Tools cache miss (invalid) for server: ${serverConfig.name}`, {
1413
+ serverName: serverConfig.name,
1414
+ reason: cacheIsValid ? 'unknown' : 'invalid'
1415
+ });
1416
+ }
1417
+ else {
1418
+ logger.debug(`Tools cache miss (not found) for server: ${serverConfig.name}`, {
1419
+ serverName: serverConfig.name
1420
+ });
1421
+ }
1422
+ const serverTools = await fetchAndCacheTools(serverConfig);
1423
+ Object.assign(allTools, serverTools);
1424
+ }
1425
+ catch (error) {
1426
+ logger.error(`Failed to get tools from MCP server: ${serverConfig.name}`, {
1427
+ worldId,
1428
+ error: error instanceof Error ? error.message : error
1429
+ });
1430
+ }
1431
+ })();
1432
+ serverPromises.push(serverPromise);
1433
+ }
1434
+ await Promise.allSettled(serverPromises);
1435
+ const totalTools = Object.keys(allTools).length;
1436
+ const builtInToolCount = Object.keys(getBuiltInTools()).length;
1437
+ const mcpToolCount = totalTools - builtInToolCount;
1438
+ const duration = performance.now() - startTime;
1439
+ logger.info(`Retrieved ${totalTools} total tools for world: ${worldId} (${builtInToolCount} built-in, ${mcpToolCount} from MCP)`, {
1440
+ worldId,
1441
+ totalTools,
1442
+ builtInToolCount,
1443
+ mcpToolCount,
1444
+ cacheHits,
1445
+ cacheMisses,
1446
+ cacheHitRate: cacheHits + cacheMisses > 0 ? Math.round((cacheHits / (cacheHits + cacheMisses)) * 100) : 0,
1447
+ duration: Math.round(duration * 100) / 100,
1448
+ cacheSize: toolsCache.size
1449
+ });
1450
+ return allTools;
1451
+ }
1452
+ /**
1453
+ * Execute MCP tool by server ID and tool name
1454
+ *
1455
+ * @param serverId - Server ID from registry
1456
+ * @param toolName - Name of the tool to execute
1457
+ * @param args - Tool arguments (will be validated if schema provided)
1458
+ * @param sequenceId - Optional sequence ID for tracking
1459
+ * @param parentToolCall - Optional parent tool call ID
1460
+ * @param toolSchema - Optional tool input schema for parameter validation.
1461
+ * If provided, applies validateAndCorrectToolArgs to fix
1462
+ * common LLM parameter mistakes (string→array, string→number,
1463
+ * invalid enums, etc.)
1464
+ *
1465
+ * @example
1466
+ * // With validation (recommended for external callers)
1467
+ * const schema = { properties: { limit: { type: 'number' } }, required: [] };
1468
+ * const result = await executeMCPTool(serverId, 'searchAgents', args, seq, parent, schema);
1469
+ *
1470
+ * // Without validation (legacy behavior)
1471
+ * const result = await executeMCPTool(serverId, 'searchAgents', args);
1472
+ */
1473
+ export async function executeMCPTool(serverId, toolName, args, sequenceId, parentToolCall, toolSchema) {
1474
+ const serverInstance = serverRegistry.get(serverId);
1475
+ if (!serverInstance) {
1476
+ throw new Error(`MCP server not found: ${serverId.slice(0, 8)}`);
1477
+ }
1478
+ if (serverInstance.status !== 'running' || !serverInstance.client) {
1479
+ throw new Error(`MCP server not available: ${serverInstance.config.name} (status: ${serverInstance.status})`);
1480
+ }
1481
+ const startTime = performance.now();
1482
+ const executionId = `${serverId.slice(0, 8)}-${toolName}-${Date.now()}`;
1483
+ logger.debug(`MCP tool execution starting`, {
1484
+ executionId,
1485
+ serverId: serverId.slice(0, 8),
1486
+ toolName,
1487
+ serverName: serverInstance.config.name,
1488
+ sequenceId,
1489
+ parentToolCall,
1490
+ argsPresent: !!args,
1491
+ argsKeys: args ? Object.keys(args) : [],
1492
+ hasSchema: !!toolSchema
1493
+ });
1494
+ // OLLAMA BUG FIX: Translate "$" arguments to proper parameter names
1495
+ let validatedArgs = translateOllamaArguments(args || {}, toolSchema);
1496
+ // ENHANCEMENT: Apply parameter validation if schema provided
1497
+ // This ensures executeMCPTool has same validation as mcpToolsToAiTools wrapper
1498
+ if (toolSchema) {
1499
+ validatedArgs = validateAndCorrectToolArgs(validatedArgs, toolSchema);
1500
+ logger.debug(`Parameter validation applied in executeMCPTool`, {
1501
+ executionId,
1502
+ serverId: serverId.slice(0, 8),
1503
+ toolName,
1504
+ hasSchema: true,
1505
+ originalArgs: args,
1506
+ validatedArgs
1507
+ });
1508
+ }
1509
+ const requestPayload = { name: toolName, arguments: validatedArgs };
1510
+ logger.debug(`MCP server direct request payload`, {
1511
+ executionId,
1512
+ serverId: serverId.slice(0, 8),
1513
+ toolName,
1514
+ serverName: serverInstance.config.name,
1515
+ requestPayload: JSON.stringify(requestPayload, null, 2)
1516
+ });
1517
+ const maxAttempts = 2;
1518
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1519
+ if (!serverInstance.client) {
1520
+ throw new Error(`MCP server not available: ${serverInstance.config.name} (client disconnected)`);
1521
+ }
1522
+ try {
1523
+ const result = await serverInstance.client.callTool(requestPayload);
1524
+ const mcpError = isMCPErrorResponse(result);
1525
+ if (mcpError) {
1526
+ logger.error(`MCP tool execution returned error payload`, {
1527
+ executionId,
1528
+ serverId: serverId.slice(0, 8),
1529
+ toolName,
1530
+ serverName: serverInstance.config.name,
1531
+ error: mcpError.message
1532
+ });
1533
+ throw mcpError;
1534
+ }
1535
+ // Debug log: Raw response data received from MCP server
1536
+ logger.debug(`MCP server direct response payload`, {
1537
+ executionId,
1538
+ serverId: serverId.slice(0, 8),
1539
+ toolName,
1540
+ serverName: serverInstance.config.name,
1541
+ responsePayload: JSON.stringify(result, null, 2),
1542
+ responseType: typeof result,
1543
+ hasContent: !!(result?.content),
1544
+ contentLength: Array.isArray(result?.content) ? result.content.length : 0
1545
+ });
1546
+ const duration = performance.now() - startTime;
1547
+ const hasContent = result?.content && Array.isArray(result.content) && result.content.length > 0;
1548
+ const contentTypes = hasContent ? result.content.map((c) => c?.type).filter(Boolean) : [];
1549
+ const resultSize = hasContent ? JSON.stringify(result).length : 0;
1550
+ logger.debug(`MCP tool execution completed`, {
1551
+ executionId,
1552
+ serverId: serverId.slice(0, 8),
1553
+ toolName,
1554
+ serverName: serverInstance.config.name,
1555
+ sequenceId,
1556
+ parentToolCall,
1557
+ status: 'success',
1558
+ duration: Math.round(duration * 100) / 100, // Round to 2 decimal places
1559
+ hasContent,
1560
+ contentTypes,
1561
+ resultSize,
1562
+ resultPreview: hasContent ? JSON.stringify(result).slice(0, 200) + (resultSize > 200 ? '...' : '') : null
1563
+ });
1564
+ return result;
1565
+ }
1566
+ catch (error) {
1567
+ const duration = performance.now() - startTime;
1568
+ const errorMessage = error instanceof Error ? error.message : String(error);
1569
+ const isLastAttempt = attempt === maxAttempts - 1;
1570
+ const shouldRetry = !isLastAttempt && isConnectionLevelError(error);
1571
+ if (shouldRetry) {
1572
+ logger.warn(`MCP tool execution detected transport issue - attempting direct reconnect`, {
1573
+ executionId,
1574
+ serverId: serverId.slice(0, 8),
1575
+ toolName,
1576
+ serverName: serverInstance.config.name,
1577
+ status: 'retrying',
1578
+ attempt: attempt + 1,
1579
+ maxAttempts,
1580
+ duration: Math.round(duration * 100) / 100,
1581
+ error: errorMessage
1582
+ });
1583
+ await safelyCloseClient(serverInstance.client, {
1584
+ serverName: serverInstance.config.name,
1585
+ reason: 'direct-call-reconnect'
1586
+ });
1587
+ serverInstance.client = null;
1588
+ try {
1589
+ serverInstance.client = await connectMCPServer(serverInstance.config);
1590
+ serverInstance.status = 'running';
1591
+ serverInstance.lastHealthCheck = new Date();
1592
+ continue;
1593
+ }
1594
+ catch (reconnectError) {
1595
+ const reconnectMessage = reconnectError instanceof Error ? reconnectError.message : String(reconnectError);
1596
+ logger.error(`MCP direct tool execution reconnect failed`, {
1597
+ executionId,
1598
+ serverId: serverId.slice(0, 8),
1599
+ toolName,
1600
+ serverName: serverInstance.config.name,
1601
+ error: reconnectMessage
1602
+ });
1603
+ serverInstance.status = 'error';
1604
+ serverInstance.error = reconnectError instanceof Error ? reconnectError : new Error(reconnectMessage);
1605
+ throw reconnectError;
1606
+ }
1607
+ }
1608
+ logger.error(`MCP tool execution failed: ${errorMessage}`, {
1609
+ executionId,
1610
+ serverId: serverId.slice(0, 8),
1611
+ toolName,
1612
+ serverName: serverInstance.config.name,
1613
+ sequenceId,
1614
+ parentToolCall,
1615
+ status: 'error',
1616
+ duration: Math.round(duration * 100) / 100,
1617
+ error: errorMessage,
1618
+ errorStack: error instanceof Error ? error.stack : undefined
1619
+ });
1620
+ throw error;
1621
+ }
1622
+ }
1623
+ }
1624
+ /**
1625
+ * Restart MCP server by ID
1626
+ */
1627
+ export async function restartMCPServer(serverId) {
1628
+ const serverInstance = serverRegistry.get(serverId);
1629
+ if (!serverInstance)
1630
+ return false;
1631
+ logger.info(`Restarting MCP server: ${serverInstance.config.name}`, {
1632
+ serverId: serverId.slice(0, 8)
1633
+ });
1634
+ try {
1635
+ await stopServer(serverInstance);
1636
+ await startServerAsync(serverInstance);
1637
+ return true;
1638
+ }
1639
+ catch (error) {
1640
+ logger.error(`Failed to restart MCP server: ${serverInstance.config.name}`, {
1641
+ serverId: serverId.slice(0, 8),
1642
+ error: error instanceof Error ? error.message : error
1643
+ });
1644
+ serverInstance.status = 'error';
1645
+ serverInstance.error = error instanceof Error ? error : new Error(String(error));
1646
+ return false;
1647
+ }
1648
+ }
1649
+ // === TOOL CACHE MANAGEMENT ===
1650
+ /**
1651
+ * Clear cached tools for specific server or all servers
1652
+ * Useful for forcing cache refresh when server tools change
1653
+ */
1654
+ export async function clearToolsCache(serverName) {
1655
+ if (serverName) {
1656
+ const cacheKey = getToolCacheKey(serverName);
1657
+ const entry = toolsCache.get(cacheKey);
1658
+ let deleted = false;
1659
+ if (entry) {
1660
+ await disposeToolCacheEntry(entry, 'manual-clear');
1661
+ deleted = toolsCache.delete(cacheKey);
1662
+ }
1663
+ logger.info(`Cleared tools cache for server: ${serverName}`, {
1664
+ serverName,
1665
+ found: deleted,
1666
+ remainingEntries: toolsCache.size
1667
+ });
1668
+ }
1669
+ else {
1670
+ const entriesCleared = await disposeAllToolCacheEntries('manual-clear-all');
1671
+ logger.info(`Cleared all tools cache entries`, {
1672
+ entriesCleared
1673
+ });
1674
+ }
1675
+ }
1676
+ /**
1677
+ * Get tools cache statistics for monitoring and debugging
1678
+ */
1679
+ export function getToolsCacheStats() {
1680
+ const entries = Array.from(toolsCache.values());
1681
+ if (entries.length === 0) {
1682
+ return {
1683
+ totalEntries: 0,
1684
+ totalTools: 0,
1685
+ cacheSize: 0,
1686
+ memoryUsage: { approximate: '0 B' }
1687
+ };
1688
+ }
1689
+ const sortedByDate = entries.sort((a, b) => a.cachedAt.getTime() - b.cachedAt.getTime());
1690
+ const totalTools = entries.reduce((sum, entry) => sum + Object.keys(entry.tools).length, 0);
1691
+ // Rough memory usage estimate (ignore non-serializable client references)
1692
+ const approximateSize = JSON.stringify(entries.map(entry => ({
1693
+ serverName: entry.serverName,
1694
+ cachedAt: entry.cachedAt.toISOString(),
1695
+ toolCount: Object.keys(entry.tools).length,
1696
+ transport: entry.serverConfig.transport
1697
+ }))).length;
1698
+ const formatSize = (bytes) => {
1699
+ if (bytes < 1024)
1700
+ return `${bytes} B`;
1701
+ if (bytes < 1024 * 1024)
1702
+ return `${Math.round(bytes / 1024)} KB`;
1703
+ return `${Math.round(bytes / (1024 * 1024) * 100) / 100} MB`;
1704
+ };
1705
+ return {
1706
+ totalEntries: entries.length,
1707
+ totalTools,
1708
+ cacheSize: toolsCache.size,
1709
+ oldestEntry: sortedByDate.length > 0 ? {
1710
+ serverName: sortedByDate[0].serverName,
1711
+ cachedAt: sortedByDate[0].cachedAt
1712
+ } : undefined,
1713
+ newestEntry: sortedByDate.length > 0 ? {
1714
+ serverName: sortedByDate[sortedByDate.length - 1].serverName,
1715
+ cachedAt: sortedByDate[sortedByDate.length - 1].cachedAt
1716
+ } : undefined,
1717
+ memoryUsage: { approximate: formatSize(approximateSize) }
1718
+ };
1719
+ }
1720
+ /**
1721
+ * Refresh cached tools for a specific server (force cache miss on next access)
1722
+ */
1723
+ export async function refreshServerToolsCache(serverName) {
1724
+ const cacheKey = getToolCacheKey(serverName);
1725
+ const entry = toolsCache.get(cacheKey);
1726
+ if (entry) {
1727
+ await disposeToolCacheEntry(entry, 'manual-refresh');
1728
+ toolsCache.delete(cacheKey);
1729
+ logger.info(`Marked tools cache for refresh: ${serverName}`, {
1730
+ serverName,
1731
+ remainingEntries: toolsCache.size
1732
+ });
1733
+ return true;
1734
+ }
1735
+ logger.debug(`Tools cache not found for server: ${serverName}`, {
1736
+ serverName
1737
+ });
1738
+ return false;
1739
+ }
1740
+ /** Get MCP system health status */
1741
+ export function getMCPSystemHealth() {
1742
+ const servers = Array.from(serverRegistry.values());
1743
+ const healthyServers = servers.filter(s => s.status === 'running');
1744
+ const unhealthyServers = servers.filter(s => s.status === 'error');
1745
+ const errors = unhealthyServers.map(s => s.error?.message || 'Unknown error');
1746
+ let status;
1747
+ if (servers.length === 0) {
1748
+ status = 'healthy'; // No servers is healthy
1749
+ }
1750
+ else if (unhealthyServers.length === 0) {
1751
+ status = 'healthy';
1752
+ }
1753
+ else if (healthyServers.length > unhealthyServers.length) {
1754
+ status = 'degraded';
1755
+ }
1756
+ else {
1757
+ status = 'unhealthy';
1758
+ }
1759
+ return {
1760
+ status,
1761
+ details: {
1762
+ totalServers: servers.length,
1763
+ healthyServers: healthyServers.length,
1764
+ unhealthyServers: unhealthyServers.length,
1765
+ errors
1766
+ }
1767
+ };
1768
+ }
1769
+ //# sourceMappingURL=mcp-server-registry.js.map