legate 0.1.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 (317) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +345 -0
  4. data/bin/legate +13 -0
  5. data/examples/00_quickstart.rb +51 -0
  6. data/examples/01_simple_agent.rb +105 -0
  7. data/examples/02_multi_tool_agent.rb +140 -0
  8. data/examples/03_custom_tool.rb +93 -0
  9. data/examples/04_agent_instructions.rb +84 -0
  10. data/examples/05_state_and_sessions.rb +91 -0
  11. data/examples/06_callbacks.rb +186 -0
  12. data/examples/07_async_jobs.rb +112 -0
  13. data/examples/08_loop_agent.rb +197 -0
  14. data/examples/09_sequential_workflow.rb +40 -0
  15. data/examples/10_parallel_workflow.rb +34 -0
  16. data/examples/11_agent_delegation.rb +24 -0
  17. data/examples/12_http_client_tool.rb +156 -0
  18. data/examples/13_authentication.rb +220 -0
  19. data/examples/14_mcp_client.rb +154 -0
  20. data/examples/15_mcp_server.rb +79 -0
  21. data/examples/16_webhooks.rb +91 -0
  22. data/examples/README_sequential_agents.md +164 -0
  23. data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
  24. data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
  25. data/examples/advanced/auth/excon_middleware.rb +317 -0
  26. data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
  27. data/examples/advanced/auth/fiber_auth_example.rb +281 -0
  28. data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
  29. data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
  30. data/examples/advanced/auth/oauth2_auth.rb +419 -0
  31. data/examples/advanced/auth/oidc_auth.rb +514 -0
  32. data/examples/advanced/auth/openweather_api.rb +251 -0
  33. data/examples/advanced/auth/openweather_tool.rb +153 -0
  34. data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
  35. data/examples/advanced/auth/service_account.rb +135 -0
  36. data/examples/advanced/auth/test_with_httpbin.rb +202 -0
  37. data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
  38. data/examples/advanced/callback_monitoring.rb +679 -0
  39. data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
  40. data/examples/advanced/mas/loop_workflow.rb +28 -0
  41. data/examples/advanced/mas/mock_planner.rb +77 -0
  42. data/examples/advanced/mas/proper_delegation_example.rb +276 -0
  43. data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
  44. data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
  45. data/examples/advanced/mcp/mcp_server_async.rb +76 -0
  46. data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
  47. data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
  48. data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
  49. data/examples/advanced/random_calculator.rb +104 -0
  50. data/examples/advanced/sleep_agent.rb +153 -0
  51. data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
  52. data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
  53. data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
  54. data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
  55. data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
  56. data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
  57. data/examples/tools/oauth2_example.rb +136 -0
  58. data/examples/tools/sleepy_tool.rb +42 -0
  59. data/lib/legate/activity_log.rb +71 -0
  60. data/lib/legate/agent.rb +959 -0
  61. data/lib/legate/agent_code_generator.rb +185 -0
  62. data/lib/legate/agent_definition.rb +812 -0
  63. data/lib/legate/agentic/decision.rb +49 -0
  64. data/lib/legate/agentic/loop.rb +134 -0
  65. data/lib/legate/agentic.rb +5 -0
  66. data/lib/legate/agents/loop_agent.rb +248 -0
  67. data/lib/legate/agents/parallel_agent.rb +163 -0
  68. data/lib/legate/agents/sequential_agent.rb +190 -0
  69. data/lib/legate/agents.rb +14 -0
  70. data/lib/legate/auth/config.rb +148 -0
  71. data/lib/legate/auth/coordinator.rb +218 -0
  72. data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
  73. data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
  74. data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
  75. data/lib/legate/auth/credential.rb +157 -0
  76. data/lib/legate/auth/encryption.rb +108 -0
  77. data/lib/legate/auth/error.rb +94 -0
  78. data/lib/legate/auth/exchanged_credential.rb +180 -0
  79. data/lib/legate/auth/excon_middleware.rb +285 -0
  80. data/lib/legate/auth/http_client_utils.rb +364 -0
  81. data/lib/legate/auth/manager.rb +531 -0
  82. data/lib/legate/auth/manager_store.rb +394 -0
  83. data/lib/legate/auth/middleware_factory.rb +290 -0
  84. data/lib/legate/auth/runner.rb +279 -0
  85. data/lib/legate/auth/scheme.rb +125 -0
  86. data/lib/legate/auth/schemes/api_key.rb +212 -0
  87. data/lib/legate/auth/schemes/google_service_account.rb +108 -0
  88. data/lib/legate/auth/schemes/http_bearer.rb +98 -0
  89. data/lib/legate/auth/schemes/oauth2.rb +396 -0
  90. data/lib/legate/auth/schemes/openid_connect.rb +346 -0
  91. data/lib/legate/auth/schemes/service_account.rb +388 -0
  92. data/lib/legate/auth/schemes.rb +40 -0
  93. data/lib/legate/auth/token_manager.rb +362 -0
  94. data/lib/legate/auth/token_store.rb +86 -0
  95. data/lib/legate/auth/tool_context_extension.rb +97 -0
  96. data/lib/legate/auth/tool_integration.rb +188 -0
  97. data/lib/legate/auth/url_guard.rb +81 -0
  98. data/lib/legate/auth.rb +453 -0
  99. data/lib/legate/callbacks/callback_context.rb +71 -0
  100. data/lib/legate/cli/agent_commands.rb +950 -0
  101. data/lib/legate/cli/auth_commands.rb +520 -0
  102. data/lib/legate/cli/base_command.rb +24 -0
  103. data/lib/legate/cli/deployment_commands.rb +934 -0
  104. data/lib/legate/cli/output_helper.rb +108 -0
  105. data/lib/legate/cli/session_commands.rb +138 -0
  106. data/lib/legate/cli/skaffold_commands.rb +223 -0
  107. data/lib/legate/cli/tool_commands.rb +261 -0
  108. data/lib/legate/cli/web_commands.rb +182 -0
  109. data/lib/legate/cli.rb +40 -0
  110. data/lib/legate/configuration/webhooks.rb +113 -0
  111. data/lib/legate/configuration.rb +39 -0
  112. data/lib/legate/definition_store.rb +23 -0
  113. data/lib/legate/errors.rb +118 -0
  114. data/lib/legate/event.rb +161 -0
  115. data/lib/legate/gemini_ai_beta_patch.rb +39 -0
  116. data/lib/legate/generators/agent_generator.rb +412 -0
  117. data/lib/legate/generators/code_validator.rb +48 -0
  118. data/lib/legate/generators/legate/install_generator.rb +35 -0
  119. data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
  120. data/lib/legate/generators/legate/templates/initializer.rb +18 -0
  121. data/lib/legate/generators/runtime_tool_loader.rb +76 -0
  122. data/lib/legate/generators/tool_generator.rb +408 -0
  123. data/lib/legate/generators.rb +11 -0
  124. data/lib/legate/global_definition_registry.rb +506 -0
  125. data/lib/legate/global_tool_manager.rb +135 -0
  126. data/lib/legate/llm/adapter.rb +69 -0
  127. data/lib/legate/llm/gemini.rb +172 -0
  128. data/lib/legate/llm/ollama.rb +80 -0
  129. data/lib/legate/llm.rb +34 -0
  130. data/lib/legate/mcp/client.rb +320 -0
  131. data/lib/legate/mcp/connection/sse.rb +292 -0
  132. data/lib/legate/mcp/connection/stdio.rb +273 -0
  133. data/lib/legate/mcp/connection_manager.rb +103 -0
  134. data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
  135. data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
  136. data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
  137. data/lib/legate/mcp/tool_wrapper.rb +138 -0
  138. data/lib/legate/mcp/util/schema_converter.rb +134 -0
  139. data/lib/legate/mcp.rb +23 -0
  140. data/lib/legate/plan_executor.rb +375 -0
  141. data/lib/legate/planner.rb +839 -0
  142. data/lib/legate/rails/railtie.rb +43 -0
  143. data/lib/legate/rails.rb +9 -0
  144. data/lib/legate/redaction.rb +32 -0
  145. data/lib/legate/session.rb +299 -0
  146. data/lib/legate/session_service/active_record.rb +300 -0
  147. data/lib/legate/session_service/base.rb +68 -0
  148. data/lib/legate/session_service/event_broadcast.rb +74 -0
  149. data/lib/legate/session_service/in_memory.rb +188 -0
  150. data/lib/legate/tool/metadata_dsl.rb +122 -0
  151. data/lib/legate/tool.rb +276 -0
  152. data/lib/legate/tool_code_generator.rb +103 -0
  153. data/lib/legate/tool_context.rb +350 -0
  154. data/lib/legate/tool_loader.rb +39 -0
  155. data/lib/legate/tool_registry.rb +73 -0
  156. data/lib/legate/tool_result.rb +61 -0
  157. data/lib/legate/tools/agent_tool.rb +187 -0
  158. data/lib/legate/tools/base/http_client.rb +319 -0
  159. data/lib/legate/tools/base/safe_url.rb +56 -0
  160. data/lib/legate/tools/base_async_job_tool.rb +91 -0
  161. data/lib/legate/tools/calculator.rb +89 -0
  162. data/lib/legate/tools/cat_facts.rb +81 -0
  163. data/lib/legate/tools/check_job_status_tool.rb +48 -0
  164. data/lib/legate/tools/current_time_tool.rb +64 -0
  165. data/lib/legate/tools/echo.rb +43 -0
  166. data/lib/legate/tools/http_request_tool.rb +105 -0
  167. data/lib/legate/tools/random_number_tool.rb +64 -0
  168. data/lib/legate/tools/read_webpage_tool.rb +92 -0
  169. data/lib/legate/tools/sleepy_tool.rb +74 -0
  170. data/lib/legate/tools/webhook_tool.rb +146 -0
  171. data/lib/legate/version.rb +5 -0
  172. data/lib/legate/web/app.rb +984 -0
  173. data/lib/legate/web/public/css/main.css +4980 -0
  174. data/lib/legate/web/public/images/favicon-256.png +0 -0
  175. data/lib/legate/web/public/images/favicon-32.png +0 -0
  176. data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
  177. data/lib/legate/web/public/images/legate-logo-light.png +0 -0
  178. data/lib/legate/web/public/js/legate.js +616 -0
  179. data/lib/legate/web/public/styles/main.scss +4402 -0
  180. data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
  181. data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
  182. data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
  183. data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
  184. data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
  185. data/lib/legate/web/routes/api_routes.rb +56 -0
  186. data/lib/legate/web/routes/authentication_routes.rb +1541 -0
  187. data/lib/legate/web/routes/core_routes.rb +111 -0
  188. data/lib/legate/web/routes/documentation_routes.rb +220 -0
  189. data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
  190. data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
  191. data/lib/legate/web/sass_compiler.rb +73 -0
  192. data/lib/legate/web/views/_active_session_info.slim +25 -0
  193. data/lib/legate/web/views/_activity_list.slim +55 -0
  194. data/lib/legate/web/views/_agent_card.slim +56 -0
  195. data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
  196. data/lib/legate/web/views/_agent_status_controls.slim +71 -0
  197. data/lib/legate/web/views/_agent_tool_table.slim +74 -0
  198. data/lib/legate/web/views/_chat_message.slim +95 -0
  199. data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
  200. data/lib/legate/web/views/_display_agent_description.slim +11 -0
  201. data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
  202. data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
  203. data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
  204. data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
  205. data/lib/legate/web/views/_display_agent_model.slim +17 -0
  206. data/lib/legate/web/views/_display_agent_name.slim +42 -0
  207. data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
  208. data/lib/legate/web/views/_display_agent_type.slim +65 -0
  209. data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
  210. data/lib/legate/web/views/_edit_agent_description.slim +16 -0
  211. data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
  212. data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
  213. data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
  214. data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
  215. data/lib/legate/web/views/_edit_agent_model.slim +23 -0
  216. data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
  217. data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
  218. data/lib/legate/web/views/_edit_agent_type.slim +67 -0
  219. data/lib/legate/web/views/_session_error.slim +4 -0
  220. data/lib/legate/web/views/_skeleton.slim +69 -0
  221. data/lib/legate/web/views/_tool_card.slim +9 -0
  222. data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
  223. data/lib/legate/web/views/agent.slim +436 -0
  224. data/lib/legate/web/views/agent_auth.slim +562 -0
  225. data/lib/legate/web/views/agents.slim +369 -0
  226. data/lib/legate/web/views/auth.slim +112 -0
  227. data/lib/legate/web/views/auth_credential_detail.slim +327 -0
  228. data/lib/legate/web/views/auth_credentials.slim +261 -0
  229. data/lib/legate/web/views/auth_debug.slim +94 -0
  230. data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
  231. data/lib/legate/web/views/auth_mapping_new.slim +123 -0
  232. data/lib/legate/web/views/auth_mappings.slim +120 -0
  233. data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
  234. data/lib/legate/web/views/auth_schemes.slim +259 -0
  235. data/lib/legate/web/views/auth_test.slim +418 -0
  236. data/lib/legate/web/views/chat.slim +192 -0
  237. data/lib/legate/web/views/docs_index.slim +105 -0
  238. data/lib/legate/web/views/docs_show.slim +105 -0
  239. data/lib/legate/web/views/error_404.slim +5 -0
  240. data/lib/legate/web/views/index.slim +148 -0
  241. data/lib/legate/web/views/layout.slim +144 -0
  242. data/lib/legate/web/views/tool_detail.slim +87 -0
  243. data/lib/legate/web/views/tools.slim +50 -0
  244. data/lib/legate/web/webhook_listener.rb +367 -0
  245. data/lib/legate/web.rb +9 -0
  246. data/lib/legate.rb +220 -0
  247. data/public/docs/advanced/callbacks.md +828 -0
  248. data/public/docs/advanced/mcp_schema_conversion.md +59 -0
  249. data/public/docs/authentication/api_reference/config.md +210 -0
  250. data/public/docs/authentication/api_reference/credential.md +246 -0
  251. data/public/docs/authentication/api_reference/encryption.md +218 -0
  252. data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
  253. data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
  254. data/public/docs/authentication/api_reference/index.md +30 -0
  255. data/public/docs/authentication/api_reference/scheme.md +250 -0
  256. data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
  257. data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
  258. data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
  259. data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
  260. data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
  261. data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
  262. data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
  263. data/public/docs/authentication/api_reference/token_manager.md +221 -0
  264. data/public/docs/authentication/api_reference/token_store.md +146 -0
  265. data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
  266. data/public/docs/authentication/guides/api_key.md +190 -0
  267. data/public/docs/authentication/guides/bearer.md +172 -0
  268. data/public/docs/authentication/guides/configuration.md +255 -0
  269. data/public/docs/authentication/guides/custom_flow.md +523 -0
  270. data/public/docs/authentication/guides/index.md +24 -0
  271. data/public/docs/authentication/guides/migration.md +435 -0
  272. data/public/docs/authentication/guides/oauth2.md +252 -0
  273. data/public/docs/authentication/guides/oidc.md +241 -0
  274. data/public/docs/authentication/guides/overview.md +155 -0
  275. data/public/docs/authentication/guides/secure_storage.md +301 -0
  276. data/public/docs/authentication/guides/service_account.md +228 -0
  277. data/public/docs/authentication/guides/token_lifecycle.md +295 -0
  278. data/public/docs/authentication/guides/web_ui_integration.md +504 -0
  279. data/public/docs/authentication/index.md +58 -0
  280. data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
  281. data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
  282. data/public/docs/authentication/troubleshooting/index.md +11 -0
  283. data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
  284. data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
  285. data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
  286. data/public/docs/cli/legate_cli_usage.md +363 -0
  287. data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
  288. data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
  289. data/public/docs/core_concepts/legate_configuration.md +116 -0
  290. data/public/docs/core_concepts/legate_definition_store.md +102 -0
  291. data/public/docs/core_concepts/legate_planner.md +94 -0
  292. data/public/docs/core_concepts/legate_session_service.md +104 -0
  293. data/public/docs/error_handling/legate_error_handling.md +122 -0
  294. data/public/docs/examples.md +199 -0
  295. data/public/docs/getting_started.md +111 -0
  296. data/public/docs/guides/agentic_agents.md +137 -0
  297. data/public/docs/guides/ai_code_generators.md +437 -0
  298. data/public/docs/guides/auto_loading.md +326 -0
  299. data/public/docs/guides/configuring_agent_webhooks.md +219 -0
  300. data/public/docs/guides/http_client_usage.md +264 -0
  301. data/public/docs/guides/llm_providers.md +137 -0
  302. data/public/docs/guides/mcp_client_integration.md +232 -0
  303. data/public/docs/guides/mcp_server_exposure.md +206 -0
  304. data/public/docs/guides/rails_integration.md +128 -0
  305. data/public/docs/guides/sending_outbound_webhooks.md +227 -0
  306. data/public/docs/guides/streaming.md +112 -0
  307. data/public/docs/guides/webhooks.md +288 -0
  308. data/public/docs/introduction.md +51 -0
  309. data/public/docs/multi_agent_systems/advanced_features.md +57 -0
  310. data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
  311. data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
  312. data/public/docs/multi_agent_systems/state_management.md +47 -0
  313. data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
  314. data/public/docs/tools/legate_built_in_tools.md +332 -0
  315. data/public/docs/tools/legate_tools_and_registry.md +263 -0
  316. data/public/docs/web_ui/legate_web_ui.md +137 -0
  317. metadata +823 -0
@@ -0,0 +1,292 @@
1
+ # File: lib/legate/mcp/connection/sse.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'json'
7
+ require_relative '../../errors'
8
+
9
+ module Legate
10
+ module Mcp
11
+ module Connection
12
+ # Manages a connection to an MCP server via HTTP/SSE.
13
+ # Uses Server-Sent Events (SSE) for server-to-client notifications
14
+ # and standard HTTP POST for client-to-server requests/responses.
15
+ # Automatically reconnects with exponential backoff on stream drops.
16
+ class Sse
17
+ MAX_RECONNECT_ATTEMPTS = 5
18
+ RECONNECT_BASE_DELAY = 1
19
+ RECONNECT_MAX_DELAY = 30
20
+
21
+ attr_reader :url, :last_error, :notification_queue
22
+
23
+ def initialize(url:)
24
+ base_uri = URI.parse(url)
25
+ base_path = base_uri.path
26
+ base_path += '/' unless base_path.end_with?('/')
27
+ @base_uri = base_uri.dup
28
+ @base_uri.path = base_path
29
+
30
+ @sse_uri = @base_uri + 'sse'
31
+ @message_uri = @base_uri + 'messages'
32
+ @sse_reader_thread = nil
33
+ @connected = false
34
+ @disconnecting = false
35
+ @request_id_counter = 0
36
+ @notification_queue = Queue.new
37
+ @connect_signal = nil
38
+ @last_error = nil
39
+ Legate.logger.info("SSE Connection initialized for URL: #{@base_uri}, SSE: #{@sse_uri}, Msg: #{@message_uri}")
40
+ end
41
+
42
+ def connected?
43
+ @connected && @sse_reader_thread&.alive?
44
+ end
45
+
46
+ # Connects to the SSE endpoint and starts listening for notifications.
47
+ # Initial connection is synchronous — raises on failure.
48
+ # After a successful connection, stream drops trigger automatic reconnection.
49
+ # @raise [ConnectionError] if the initial connection fails.
50
+ def connect
51
+ return true if connected?
52
+
53
+ Legate.logger.info("Connecting to SSE endpoint: #{@sse_uri}...")
54
+ @disconnecting = false
55
+ @last_error = nil
56
+ @notification_queue.clear
57
+ @connect_signal = Queue.new
58
+
59
+ @sse_reader_thread = Thread.new { connection_loop }
60
+
61
+ result = @connect_signal.pop(timeout: 10)
62
+ @connect_signal = nil
63
+
64
+ raise ConnectionError, @last_error || 'Failed to establish SSE connection within timeout' unless result == :connected
65
+
66
+ true
67
+ end
68
+
69
+ # Disconnects the SSE stream and stops reconnection attempts.
70
+ def disconnect
71
+ return unless @connected || @sse_reader_thread
72
+
73
+ Legate.logger.info('Disconnecting SSE connection...')
74
+ @disconnecting = true
75
+ @connected = false
76
+
77
+ if @sse_reader_thread&.alive?
78
+ @sse_reader_thread.kill
79
+ @sse_reader_thread.join(2)
80
+ end
81
+
82
+ @sse_reader_thread = nil
83
+ @notification_queue.clear
84
+ Legate.logger.info('SSE connection disconnected.')
85
+ end
86
+
87
+ # Sends a request via HTTP POST and returns the response immediately.
88
+ # @param json_rpc_hash [Hash] The JSON-RPC request.
89
+ # @return [Hash] The parsed JSON-RPC response from the server.
90
+ def send_request(json_rpc_hash)
91
+ request_json = json_rpc_hash.to_json
92
+ Legate.logger.debug("-> [MCP Client POST] #{@message_uri} Body: #{request_json}")
93
+
94
+ begin
95
+ http = Net::HTTP.new(@message_uri.hostname, @message_uri.port)
96
+ http.use_ssl = (@message_uri.scheme == 'https')
97
+ http.open_timeout = 5
98
+ http.read_timeout = 15
99
+
100
+ request = Net::HTTP::Post.new(@message_uri.request_uri)
101
+ request['Content-Type'] = 'application/json'
102
+ request['Accept'] = 'application/json'
103
+ request.body = request_json
104
+
105
+ response = http.request(request)
106
+
107
+ unless response.is_a?(Net::HTTPOK)
108
+ msg = "MCP POST request failed: #{response.code} #{response.message}. Body: #{response.body[0..500]}"
109
+ Legate.logger.error(msg)
110
+ @last_error = msg
111
+ begin
112
+ error_details = JSON.parse(response.body, symbolize_names: true)
113
+ if error_details[:error]
114
+ raise RemoteToolError.new(error_details[:error][:message], error_details[:error][:code],
115
+ error_details[:error][:data])
116
+ end
117
+ rescue JSON::ParserError
118
+ # Body is not JSON
119
+ end
120
+ raise ConnectionError, msg
121
+ end
122
+
123
+ begin
124
+ response_hash = JSON.parse(response.body, symbolize_names: true)
125
+ Legate.logger.debug("<- [MCP Client POST Response] #{response_hash.inspect}")
126
+ response_hash
127
+ rescue JSON::ParserError => e
128
+ msg = "Failed to parse MCP JSON response from POST: #{e.message}. Body: #{response.body[0..500]}"
129
+ Legate.logger.error(msg)
130
+ @last_error = msg
131
+ raise ProtocolError, msg
132
+ end
133
+ rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError => e
134
+ @last_error = "Failed to send POST to #{@message_uri}: #{e.class} - #{e.message}"
135
+ Legate.logger.error(@last_error)
136
+ raise ConnectionError, @last_error
137
+ rescue Legate::Mcp::ProtocolError, Legate::Mcp::RemoteToolError
138
+ raise
139
+ rescue StandardError => e
140
+ @last_error = "Unexpected error during POST send_request: #{e.class} - #{e.message}"
141
+ Legate.logger.error("#{@last_error}\n#{e.backtrace.join("\n")}")
142
+ raise ConnectionError, @last_error
143
+ end
144
+ end
145
+
146
+ # Reads the next notification from the queue.
147
+ # @param timeout [Numeric, nil] Seconds to wait, 0 for non-blocking.
148
+ # @return [Hash, nil] Notification hash or nil if queue is empty/timeout occurs.
149
+ def read_notification(timeout = 0.1)
150
+ return nil unless connected?
151
+
152
+ begin
153
+ return @notification_queue.pop(true) if timeout == 0
154
+
155
+ @notification_queue.pop(timeout: timeout)
156
+ rescue ThreadError
157
+ nil
158
+ end
159
+ end
160
+
161
+ def next_request_id
162
+ @request_id_counter += 1
163
+ end
164
+
165
+ private
166
+
167
+ def connection_loop
168
+ attempt = 0
169
+ initial = true
170
+
171
+ until @disconnecting
172
+ begin
173
+ attempt += 1
174
+ run_sse_stream
175
+ break if @disconnecting
176
+
177
+ @connected = false
178
+ initial = false
179
+ attempt = 0
180
+ Legate.logger.info('SSE stream ended, will reconnect.')
181
+ # IOError covers EOFError, and Timeout::Error covers Net::Open/ReadTimeout,
182
+ # so the subclasses are omitted here to avoid shadowed (redundant) rescues.
183
+ rescue ConnectionError, IOError, Errno::ECONNREFUSED,
184
+ Errno::ECONNRESET, Errno::EHOSTUNREACH, SocketError,
185
+ Timeout::Error => e
186
+ break if @disconnecting
187
+
188
+ @connected = false
189
+ @last_error = e.message
190
+
191
+ if initial
192
+ signal_connect(:failed)
193
+ return
194
+ end
195
+
196
+ if attempt > MAX_RECONNECT_ATTEMPTS
197
+ Legate.logger.error("SSE: Max reconnect attempts (#{MAX_RECONNECT_ATTEMPTS}) reached: #{e.message}")
198
+ break
199
+ end
200
+
201
+ delay = [RECONNECT_BASE_DELAY * (2**(attempt - 1)), RECONNECT_MAX_DELAY].min
202
+ Legate.logger.warn("SSE reconnect #{attempt}/#{MAX_RECONNECT_ATTEMPTS} (#{e.class}). Retrying in #{delay}s...")
203
+ sleep(delay)
204
+ rescue StandardError => e
205
+ @connected = false
206
+ @last_error = "Unexpected SSE error: #{e.class} - #{e.message}"
207
+ Legate.logger.error("#{@last_error}\n#{e.backtrace&.first(5)&.join("\n")}")
208
+ signal_connect(:failed) if initial
209
+ break
210
+ end
211
+ end
212
+
213
+ @connected = false
214
+ end
215
+
216
+ def run_sse_stream
217
+ Net::HTTP.start(@sse_uri.hostname, @sse_uri.port,
218
+ use_ssl: @sse_uri.scheme == 'https',
219
+ open_timeout: 5, read_timeout: 30) do |http|
220
+ request = Net::HTTP::Get.new(@sse_uri.request_uri)
221
+ request['Accept'] = 'text/event-stream'
222
+ request['Cache-Control'] = 'no-cache'
223
+
224
+ http.request(request) do |response|
225
+ unless response.is_a?(Net::HTTPOK) && response['content-type']&.include?('text/event-stream')
226
+ raise ConnectionError,
227
+ "SSE endpoint returned #{response.code}: #{response['content-type']}"
228
+ end
229
+
230
+ Legate.logger.info("SSE connection established with #{@sse_uri}.")
231
+ @connected = true
232
+ signal_connect(:connected)
233
+
234
+ buffer = +''
235
+ response.read_body do |chunk|
236
+ buffer << chunk
237
+ while (line_end = buffer.index("\n\n"))
238
+ event_data = buffer.slice!(0, line_end + 2)
239
+ process_sse_event(event_data)
240
+ end
241
+ end
242
+ Legate.logger.info('SSE stream ended.')
243
+ end
244
+ end
245
+ rescue EOFError
246
+ Legate.logger.info('SSE stream closed by server.')
247
+ end
248
+
249
+ def signal_connect(status)
250
+ @connect_signal&.push(status)
251
+ @connect_signal = nil
252
+ end
253
+
254
+ def process_sse_event(event_string)
255
+ Legate.logger.debug("Processing SSE Event: #{event_string.inspect}")
256
+ data_buffer = +''
257
+ event_type = nil
258
+
259
+ event_string.each_line do |line|
260
+ line.chomp!
261
+ next if line.empty?
262
+ next if line.start_with?(':')
263
+
264
+ field, value = line.split(':', 2)
265
+ value&.strip!
266
+
267
+ case field
268
+ when 'event'
269
+ event_type = value
270
+ when 'data'
271
+ data_buffer << value << "\n"
272
+ when 'retry'
273
+ Legate.logger.debug("Received SSE retry suggestion: #{value}ms")
274
+ else
275
+ Legate.logger.warn("Ignoring unknown SSE field: #{field}")
276
+ end
277
+ end
278
+
279
+ return if data_buffer.empty?
280
+
281
+ begin
282
+ message = JSON.parse(data_buffer, symbolize_names: true)
283
+ Legate.logger.debug("Parsed SSE notification (event: #{event_type || 'message'}): #{message.inspect}")
284
+ @notification_queue << message
285
+ rescue JSON::ParserError => e
286
+ Legate.logger.error("Failed to parse JSON from SSE data: #{e.message}. Data: #{data_buffer.inspect}")
287
+ end
288
+ end
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,273 @@
1
+ # File: lib/legate/mcp/connection/stdio.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'open3'
5
+ require 'json'
6
+ require_relative '../../errors'
7
+
8
+ module Legate
9
+ module Mcp
10
+ module Connection
11
+ # Manages a connection to an MCP server via STDIO.
12
+ class Stdio
13
+ # How long to wait for process startup or initial output
14
+ PROCESS_START_TIMEOUT = 5 # seconds
15
+ # How long to wait when reading a response line
16
+ READ_TIMEOUT = 10 # seconds
17
+ # Max consecutive JSON parse errors before considering connection broken
18
+ PARSE_ERROR_THRESHOLD = 5
19
+
20
+ attr_reader :command, :args, :last_error
21
+
22
+ def initialize(command:, args: [])
23
+ @command = command
24
+ @args = args
25
+ @stdin = nil
26
+ @stdout = nil
27
+ @stderr = nil
28
+ @wait_thr = nil
29
+ @stderr_thread = nil
30
+ @connected = false
31
+ @request_id_counter = 0
32
+ @response_queue = Queue.new # <-- Back to response_queue
33
+ @notification_queue = Queue.new # <-- Back to notification_queue
34
+ @stdout_reader_thread = nil
35
+ @last_error = nil
36
+ @pid = nil
37
+ @consecutive_parse_errors = 0 # Initialize counter
38
+ end
39
+
40
+ def connected?
41
+ @connected && @wait_thr&.alive?
42
+ end
43
+
44
+ # Connects to the MCP server process.
45
+ # Launches the command and starts threads to monitor stdout/stderr.
46
+ # @raise [ConnectionError] if the process fails to start or terminates unexpectedly.
47
+ def connect
48
+ return true if connected?
49
+
50
+ Mcp.logger.info("Connecting via STDIO: #{@command} #{@args.join(' ')}")
51
+ @last_error = nil
52
+ stderr_pipe_read, stderr_pipe_write = IO.pipe
53
+
54
+ begin
55
+ # Use popen3 to capture stdin, stdout, stderr, and wait_thr
56
+ @stdin, @stdout, @stderr, @wait_thr = Open3.popen3(@command, *@args, err: stderr_pipe_write)
57
+ @pid = @wait_thr.pid
58
+ stderr_pipe_write.close # Close the write end in the parent
59
+
60
+ Mcp.logger.debug("MCP process started with PID: #{@pid}")
61
+
62
+ # Thread to read stderr and log/store errors
63
+ @stderr_thread = Thread.new do
64
+ stderr_pipe_read.each_line do |line|
65
+ Mcp.logger.error("[MCP Server STDERR] #{line.chomp}")
66
+ @last_error = line.chomp # Store last error line
67
+ end
68
+ rescue IOError => e
69
+ Mcp.logger.debug("Stderr pipe closed: #{e.message}")
70
+ ensure
71
+ stderr_pipe_read.close unless stderr_pipe_read.closed?
72
+ end
73
+
74
+ # Thread to continuously read stdout and parse JSON-RPC messages
75
+ @stdout_reader_thread = Thread.new do
76
+ Mcp.logger.debug("[Stdio Connection #{@pid}] stdout_reader_thread starting...")
77
+ begin
78
+ @stdout.each_line do |line|
79
+ # Handle potential encoding issues from subprocess output
80
+ line.force_encoding('UTF-8')
81
+ line.scrub!('') # Remove invalid bytes
82
+ line.strip! # Remove leading/trailing whitespace
83
+ next if line.empty? # Skip empty lines
84
+
85
+ Legate.logger.debug("<- [MCP Server STDOUT Raw] #{line}")
86
+
87
+ # Attempt to parse only if it looks like JSON
88
+ if line.start_with?('{') || line.start_with?('[')
89
+ begin
90
+ message = JSON.parse(line, symbolize_names: true)
91
+ Legate.logger.debug("[Stdio Connection #{@pid}] Received Parsed JSON:\n#{JSON.pretty_generate(message)}")
92
+ @consecutive_parse_errors = 0 # Reset on successful parse
93
+
94
+ # Route to correct queue based on ID presence/value
95
+ if message.key?(:id) && message[:id].nil? # MCP Notifications might have null id
96
+ @notification_queue << message
97
+ elsif message.key?(:id)
98
+ Legate.logger.debug("[Stdio Connection #{@pid}] Queuing response ID: #{message[:id]}")
99
+ @response_queue << message # Responses have non-null id
100
+ else # Assume notification for now
101
+ @notification_queue << message
102
+ Legate.logger.warn("Received MCP message without explicit id: #{message.inspect}")
103
+ end
104
+ rescue JSON::ParserError => e
105
+ Legate.logger.error("Failed to parse potential MCP JSON from stdout: #{e.message}. Line: #{line}")
106
+ @consecutive_parse_errors += 1
107
+ if @consecutive_parse_errors >= PARSE_ERROR_THRESHOLD # Use >= for clarity
108
+ Legate.logger.fatal("Too many consecutive JSON parse errors (#{PARSE_ERROR_THRESHOLD} reached). Assuming MCP connection broken.")
109
+ @connected = false # Mark connection as broken
110
+ @last_error = 'Too many consecutive JSON parse errors.'
111
+ break # Stop reading from stdout
112
+ end
113
+ end
114
+ else
115
+ # Log lines that don't look like JSON instead of trying to parse
116
+ Legate.logger.debug("Skipping non-JSON line from MCP STDOUT: #{line}")
117
+ # Do not increment parse error count for these lines
118
+ end
119
+ end
120
+ Legate.logger.info('MCP Server stdout stream ended.')
121
+ rescue IOError => e
122
+ Legate.logger.info("MCP Server stdout pipe closed: #{e.message}")
123
+ ensure
124
+ @connected = false # Mark as disconnected if stdout closes or loop breaks due to errors
125
+ Mcp.logger.debug("[Stdio Connection #{@pid}] stdout_reader_thread finished.")
126
+ end
127
+ end
128
+
129
+ @connected = true
130
+ Mcp.logger.info('MCP STDIO connection established.')
131
+ true
132
+ rescue Errno::ENOENT => e
133
+ @last_error = "Command not found: #{@command}"
134
+ Mcp.logger.error("#{@last_error} - #{e.message}")
135
+ raise ConnectionError, @last_error
136
+ rescue StandardError => e
137
+ @last_error = "Failed to start MCP process: #{e.message}"
138
+ Mcp.logger.error("#{@last_error}")
139
+ # Clean up if process started partially
140
+ disconnect(force: true)
141
+ raise ConnectionError, @last_error
142
+ end
143
+ end
144
+
145
+ # Sends a JSON-RPC request object to the server process.
146
+ # @param json_rpc_hash [Hash] The request hash (e.g., {jsonrpc: '2.0', method: '...', params: ..., id: ...})
147
+ # @raise [ConnectionError] if not connected.
148
+ def send_request(json_rpc_hash)
149
+ raise ConnectionError, 'Not connected' unless connected?
150
+
151
+ begin
152
+ request_json = json_rpc_hash.to_json
153
+ Mcp.logger.debug("-> [MCP Client STDIN] #{request_json}")
154
+ @stdin.puts(request_json)
155
+ @stdin.flush # Ensure data is sent immediately
156
+ rescue Errno::EPIPE => e
157
+ @connected = false
158
+ @last_error = "MCP process stdin pipe broke: #{e.message}"
159
+ Mcp.logger.error(@last_error)
160
+ raise ConnectionError, @last_error
161
+ rescue StandardError => e
162
+ @connected = false
163
+ @last_error = "Error writing to MCP process stdin: #{e.class} - #{e.message}"
164
+ Mcp.logger.error("#{@last_error}\n#{e.backtrace.join("\n")}")
165
+ raise ConnectionError, @last_error
166
+ end
167
+ end
168
+
169
+ # Reads the next available response or notification.
170
+ # This is a low-level method; typically use Client methods which match request/response.
171
+ # @param timeout [Numeric, nil] Seconds to wait for a message, nil to wait indefinitely.
172
+ # @return [Hash, nil] The parsed JSON-RPC message, or nil if timeout occurs.
173
+ # @raise [ConnectionError] if not connected or connection lost.
174
+ def read_message(timeout = READ_TIMEOUT)
175
+ raise ConnectionError, 'Not connected' unless connected?
176
+
177
+ # Check both queues, prioritize responses if available
178
+ begin
179
+ @response_queue.pop(true) # non_block = true
180
+ rescue ThreadError
181
+ # Response queue empty, check notifications
182
+ begin
183
+ @notification_queue.pop(true)
184
+ rescue ThreadError
185
+ # Both empty, wait with timeout if specified
186
+ return nil if timeout == 0 # Don't wait if timeout is 0
187
+
188
+ deadline = timeout ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout : nil
189
+ loop do
190
+ remaining = deadline ? deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) : 1.0
191
+ return nil if remaining <= 0
192
+
193
+ wait = [remaining, 0.5].min
194
+ msg = @response_queue.pop(timeout: wait)
195
+ return msg if msg
196
+
197
+ begin
198
+ return @notification_queue.pop(true)
199
+ rescue ThreadError
200
+ # notification queue empty too
201
+ end
202
+
203
+ raise ConnectionError, 'Connection lost while waiting for message' unless connected?
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ # Disconnects from the server process.
210
+ # Terminates the process and cleans up threads.
211
+ # @param force [Boolean] If true, use SIGKILL if SIGTERM fails.
212
+ # @param timeout [Numeric] Seconds to wait for graceful shutdown.
213
+ def disconnect(force: false, timeout: 5)
214
+ return unless @connected || @wait_thr # Only proceed if we have something to disconnect
215
+
216
+ Mcp.logger.info("Disconnecting from MCP STDIO process (PID: #{@pid})...")
217
+ @connected = false
218
+
219
+ # Close stdin to signal EOF to the process
220
+ @stdin&.close unless @stdin&.closed?
221
+
222
+ # Close stdout/stderr BEFORE killing reader threads so they
223
+ # unblock from IO.read and can exit cleanly
224
+ @stdout&.close unless @stdout&.closed?
225
+ @stderr&.close unless @stderr&.closed?
226
+
227
+ # Join reader threads with timeout, then force-kill if stuck
228
+ [@stdout_reader_thread, @stderr_thread].each do |thr|
229
+ next unless thr&.alive?
230
+
231
+ thr.join(2) || thr.kill
232
+ end
233
+
234
+ # Terminate the child process
235
+ if @wait_thr&.pid
236
+ pid = @wait_thr.pid
237
+ begin
238
+ Mcp.logger.debug("Sending SIGTERM to PID #{pid}...")
239
+ Process.kill('TERM', pid)
240
+ process_exited = @wait_thr.join(timeout)
241
+
242
+ unless process_exited
243
+ Mcp.logger.warn("MCP process PID #{pid} did not exit after SIGTERM and #{timeout}s timeout.")
244
+ if force
245
+ Mcp.logger.warn("Forcing shutdown with SIGKILL for PID #{pid}.")
246
+ Process.kill('KILL', pid)
247
+ @wait_thr.join(2)
248
+ end
249
+ end
250
+ Mcp.logger.info("MCP process PID #{pid} terminated. Status: #{@wait_thr.value}")
251
+ rescue Errno::ESRCH
252
+ Mcp.logger.info("MCP process PID #{pid} already exited.")
253
+ rescue StandardError => e
254
+ Mcp.logger.debug("Caught StandardError during termination: #{e.class}")
255
+ Mcp.logger.error("Error during process termination for PID #{pid}: #{e.message}")
256
+ end
257
+ end
258
+
259
+ @stdin = @stdout = @stderr = @wait_thr = @pid = nil
260
+ @response_queue.clear
261
+ @notification_queue.clear
262
+ Mcp.logger.info('MCP STDIO connection closed.')
263
+ end
264
+
265
+ # Generates the next unique request ID.
266
+ # @return [Integer]
267
+ def next_request_id
268
+ @request_id_counter += 1
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,103 @@
1
+ # File: lib/legate/mcp/connection_manager.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'client'
5
+ require_relative 'tool_wrapper'
6
+
7
+ module Legate
8
+ module Mcp
9
+ # Owns an agent's MCP client connections: connecting to configured servers,
10
+ # discovering and registering their tools into the agent's tool registry, and
11
+ # disconnecting. Extracted from Legate::Agent to keep MCP lifecycle out of the
12
+ # agent's core responsibilities.
13
+ class ConnectionManager
14
+ attr_reader :clients
15
+
16
+ # @param tool_registry [Legate::ToolRegistry] the agent's registry that MCP tools register into
17
+ # @param selected_tool_names [Array<Symbol>] tool names the agent selected (others are skipped)
18
+ # @param agent_name [Symbol, String] for log context
19
+ def initialize(tool_registry:, selected_tool_names:, agent_name:)
20
+ @tool_registry = tool_registry
21
+ @selected_tool_names = selected_tool_names
22
+ @agent_name = agent_name
23
+ @clients = []
24
+ end
25
+
26
+ # Connects to each configured MCP server and registers its selected tools.
27
+ # @param servers_config [Array<Hash>, nil] server configs from the definition
28
+ def connect(servers_config)
29
+ return if servers_config.nil? || servers_config.empty?
30
+
31
+ servers_config.each do |config|
32
+ # Transform keys to symbols for the client
33
+ symbolized_config = config.transform_keys(&:to_sym)
34
+ Legate.logger.info("Attempting to connect to MCP server: #{symbolized_config.inspect}")
35
+ begin
36
+ unless %w[stdio sse].include?(symbolized_config[:type])
37
+ Legate.logger.error("Unsupported MCP server type specified: #{symbolized_config[:type].inspect}. Skipping configuration: #{symbolized_config.inspect}")
38
+ next # Skip to the next server config
39
+ end
40
+
41
+ # Explicitly convert known string type values to symbols
42
+ if symbolized_config[:type] == 'stdio'
43
+ symbolized_config[:type] = :stdio
44
+ elsif symbolized_config[:type] == 'sse'
45
+ symbolized_config[:type] = :sse
46
+ end
47
+ # Pass the modified hash
48
+ client = Legate::Mcp::Client.new(symbolized_config)
49
+ client.connect # This performs handshake and gets capabilities
50
+ @clients << client
51
+ discover_and_register_tools(client)
52
+ rescue Legate::Mcp::ConnectionError, Legate::Mcp::ProtocolError => e # More specific MCP errors
53
+ Legate.logger.error("Failed to connect or handshake with MCP server #{config.inspect}: #{e.message}")
54
+ rescue Legate::Mcp::Error => e
55
+ Legate.logger.error("MCP-related error connecting to server #{config.inspect}: #{e.message}")
56
+ rescue StandardError => e
57
+ Legate.logger.error("Unexpected error connecting to MCP server #{config.inspect}: #{e.class} - #{e.message}")
58
+ end
59
+ end
60
+ end
61
+
62
+ # Disconnects all active MCP clients.
63
+ def disconnect
64
+ return if @clients.nil? || @clients.empty?
65
+
66
+ @clients.each do |client|
67
+ Legate.logger.info('Disconnecting MCP client...')
68
+ client.disconnect
69
+ rescue StandardError => e
70
+ Legate.logger.error("Error disconnecting MCP client: #{e.message}")
71
+ end
72
+ @clients.clear
73
+ end
74
+
75
+ private
76
+
77
+ # Discovers tools from a connected MCP client and registers the selected
78
+ # ones with the agent's registry.
79
+ # @param client [Legate::Mcp::Client]
80
+ def discover_and_register_tools(client)
81
+ Legate.logger.debug("[Agent E2E Debug] discover_and_register - @tool_registry ID: #{@tool_registry.object_id}")
82
+ begin
83
+ mcp_tool_schemas = client.list_tools
84
+ Legate.logger.debug("[Agent E2E Debug] list_tools returned: #{mcp_tool_schemas.inspect}")
85
+ Legate.logger.info("Discovered #{mcp_tool_schemas.count} tools from MCP server.")
86
+ mcp_tool_schemas.each do |schema|
87
+ tool_name_sym = schema[:name].to_sym
88
+ if @selected_tool_names.include?(tool_name_sym)
89
+ # Pass the agent's specific registry instance
90
+ Legate::Mcp::ToolWrapper.from_mcp_schema(schema, client, @tool_registry)
91
+ else
92
+ Legate.logger.debug("Skipping registration of MCP tool '#{tool_name_sym}' as it was not selected in agent definition.")
93
+ end
94
+ end
95
+ rescue Legate::Mcp::Error => e
96
+ Legate.logger.error("Failed to list tools from MCP server: #{e.message}")
97
+ rescue StandardError => e
98
+ Legate.logger.error("Unexpected error discovering MCP tools: #{e.class} - #{e.message}")
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end