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,264 @@
1
+ # Using the Legate HttpClient Module
2
+
3
+ The `Legate::Tools::Base::HttpClient` module provides a standardized and robust way for custom Legate tools to make HTTP(S) requests. It's built on the `excon` gem, offering features like persistent connections, configurable timeouts, and middleware support (though used minimally by default).
4
+
5
+ This guide explains how to integrate and use the `HttpClient` in your tools.
6
+
7
+ ## 1. Including and Setting Up the HttpClient
8
+
9
+ To use the `HttpClient`, include the module in your tool class and call `setup_http_client` within your tool's `initialize` method.
10
+
11
+ ```ruby
12
+ require 'legate/tool'
13
+ require 'legate/tools/base/http_client' # Include the HttpClient module
14
+
15
+ class MyCustomHttpTool < Legate::Tool
16
+ include Legate::Tools::Base::HttpClient # Include the module
17
+
18
+ tool_description 'A tool that makes HTTP calls.'
19
+
20
+ def initialize(**options)
21
+ super(**options)
22
+
23
+ # Setup the HTTP client
24
+ setup_http_client(
25
+ base_url: 'https://api.example.com/v1/',
26
+ headers: { 'X-Custom-Default-Header' => 'MyValue' },
27
+ options: {
28
+ persistent: true,
29
+ connect_timeout: 5, # seconds
30
+ read_timeout: 10, # seconds
31
+ write_timeout: 10 # seconds
32
+ }
33
+ )
34
+ end
35
+
36
+ # ... tool methods ...
37
+ end
38
+ ```
39
+
40
+ ### `setup_http_client(base_url:, headers: {}, options: {})`
41
+
42
+ This method initializes and configures the underlying `Excon::Connection`.
43
+
44
+ * **`base_url:`** (String, required): The base URL for the API your tool will interact with (e.g., `https://api.example.com/v1/`). It must be a valid HTTP or HTTPS URL.
45
+ * **`headers:`** (Hash, optional): Default HTTP headers to be sent with every request made by this client instance. These can be overridden on a per-request basis.
46
+ * A default `User-Agent` header (e.g., `Legate-Ruby/0.1.0 Excon/0.104.0`) is automatically added unless you provide your own.
47
+ * **`options:`** (Hash, optional): Options passed directly to `Excon.new` for configuring the connection. Common options include:
48
+ * `persistent:` (Boolean): Whether to use persistent connections (default: `true`).
49
+ * `connect_timeout:`, `read_timeout:`, `write_timeout:` (Numeric): Connection, read, and write timeouts in seconds (defaults: `5`, `15`, `15` respectively).
50
+ * `ssl_verify_peer:` (Boolean): Whether to verify the SSL certificate (default: `true`).
51
+ * `proxy:` (String): Proxy server URL.
52
+ * `instrumentor:` Allows specifying a custom Excon instrumentor. By default, it uses an internal `QuietInstrumentor` (a subclass of `Excon::StandardInstrumentor`) that logs only errors, keeping normal request/response traffic out of the logs.
53
+
54
+ ## 2. Making HTTP Requests
55
+
56
+ Once set up, you can use the provided helper methods to make HTTP requests:
57
+
58
+ * `http_get(path, query: {}, headers: {}, options: {})`
59
+ * `http_head(path, query: {}, headers: {}, options: {})` - Returns headers only, no body (efficient for status checks)
60
+ * `http_post(path, body: nil, query: {}, headers: {}, options: {})`
61
+ * `http_put(path, body: nil, query: {}, headers: {}, options: {})`
62
+ * `http_delete(path, query: {}, headers: {}, options: {})`
63
+
64
+ ### Common Parameters
65
+
66
+ * **`path`** (String): The path for the request.
67
+ * If it's a relative path (e.g., `users`, `/users`), it will be joined with the `base_url` provided during setup.
68
+ * If it's an absolute URL (e.g., `https://another.api.com/data`), that URL will be used directly for the request, and a temporary Excon client will be used with the same connection options defined in `setup_http_client`.
69
+ * **`query:`** (Hash, optional): A hash of query parameters to be appended to the URL (e.g., `{ sort: 'name', limit: 10 }`).
70
+ * **`headers:`** (Hash, optional): Headers specific to this request. These will be merged with (and can override) the default headers set in `setup_http_client`.
71
+ * **`options:`** (Hash, optional): Excon request options specific to this request (e.g., to override timeouts for a single long-running request).
72
+ * **`body:`** (Object, optional, for `http_post`, `http_put`): The request body.
73
+ * If `body` is a `Hash`, it will typically be automatically encoded as a JSON string, and the `Content-Type` header will be set to `application/json; charset=utf-8` unless a `Content-Type` is explicitly provided in the `headers:` parameter for the request.
74
+ * If `body` is already a `String`, it will be sent as-is. You should ensure it's correctly encoded and set the appropriate `Content-Type` header if needed.
75
+
76
+ ### Example: Making a GET Request
77
+
78
+ ```ruby
79
+ def fetch_user_data(user_id)
80
+ response = http_get("users/#{user_id}", query: { include_details: true })
81
+ # Process the response (see "Handling Responses" below)
82
+ JSON.parse(response.body)
83
+ rescue Legate::ToolHttpError => e
84
+ Legate.logger.error "HTTP error fetching user #{user_id}: #{e.message}, Status: #{e.response&.status}"
85
+ # Handle specific statuses, e.g., e.response.status == 404
86
+ nil
87
+ rescue Legate::ToolError => e
88
+ Legate.logger.error "Tool error fetching user #{user_id}: #{e.message}"
89
+ nil
90
+ end
91
+ ```
92
+
93
+ ### Example: Making a HEAD Request
94
+
95
+ HEAD requests are useful for checking if a resource exists or getting metadata without downloading the full response body. This is efficient for status checks or verifying URLs.
96
+
97
+ ```ruby
98
+ def check_url_status(url)
99
+ # HEAD requests work with absolute URLs too
100
+ response = http_head(url)
101
+ {
102
+ status_code: response.status,
103
+ content_type: response.headers['Content-Type'],
104
+ content_length: response.headers['Content-Length'],
105
+ reachable: (200..399).cover?(response.status)
106
+ }
107
+ rescue Legate::ToolHttpError => e
108
+ # For a status checker, non-2xx responses may be valid results
109
+ # You can extract the response from the error
110
+ if e.response
111
+ { status_code: e.response.status, reachable: false }
112
+ else
113
+ { status_code: nil, reachable: false, error: e.message }
114
+ end
115
+ rescue Legate::ToolNetworkError, Legate::ToolTimeoutError => e
116
+ { status_code: nil, reachable: false, error: e.message }
117
+ end
118
+ ```
119
+
120
+ ## 3. Handling Responses
121
+
122
+ The request helper methods (`http_get`, `http_head`, etc.) return an `Excon::Response` object upon a successful request (typically HTTP 2xx status codes).
123
+
124
+ You can access various parts of the response:
125
+
126
+ * `response.status` (Integer): The HTTP status code.
127
+ * `response.body` (String): The response body. You may need to parse it (e.g., `JSON.parse(response.body)`).
128
+ * `response.headers` (Hash): A hash of response headers.
129
+
130
+ ```ruby
131
+ response = http_get('status')
132
+ if response.status == 200
133
+ Legate.logger.info "API Status: #{response.body}"
134
+ content_type = response.headers['Content-Type']
135
+ Legate.logger.info "Content-Type: #{content_type}"
136
+ else
137
+ Legate.logger.warn "Unexpected status: #{response.status}"
138
+ end
139
+ ```
140
+ If an HTTP error status (4xx or 5xx) is returned by the server, an `Legate::ToolHttpError` will be raised (see Error Handling).
141
+
142
+ ## 4. Error Handling
143
+
144
+ The `HttpClient` module wraps common `Excon::Error` exceptions into more specific `Legate::ToolError` subclasses. This provides a consistent error handling experience for tools.
145
+
146
+ Always include `begin...rescue` blocks when making HTTP calls.
147
+
148
+ * **`Legate::ToolTimeoutError`**: Raised for request timeouts (corresponds to `Excon::Error::Timeout`).
149
+ * **`Legate::ToolNetworkError`**: Raised for socket or underlying network issues (corresponds to `Excon::Error::Socket`).
150
+ * **`Legate::ToolCertificateError`**: Raised for SSL certificate errors (corresponds to `Excon::Error::CertificateError`).
151
+ * **`Legate::ToolHttpError`**: Raised for HTTP status codes in the 4xx and 5xx ranges (corresponds to `Excon::Error::HTTPStatusError`).
152
+ * You can access the `Excon::Response` object via `e.response` on this error (e.g., `e.response.status`, `e.response.body`).
153
+ * **`Legate::ToolError`**: A general error class used for other Excon errors or issues within the HttpClient module itself (e.g., failed JSON encoding, invalid base URL).
154
+
155
+ When an `Legate::ToolError` is raised due to an underlying `Excon::Error`, the original Excon error is preserved in the `cause` attribute of the Legate error. This is useful for debugging.
156
+
157
+ ```ruby
158
+ begin
159
+ response = http_post('submit_data', body: { key: 'value' })
160
+ # ... process response ...
161
+ rescue Legate::ToolTimeoutError => e
162
+ Legate.logger.error "Request timed out: #{e.message}"
163
+ # Optionally inspect e.cause if needed
164
+ rescue Legate::ToolHttpError => e
165
+ Legate.logger.error "HTTP Error: #{e.message} (Status: #{e.response&.status})"
166
+ if e.response&.status == 401
167
+ Legate.logger.warn "Authentication required or token expired."
168
+ # Potentially trigger re-authentication logic
169
+ end
170
+ Legate.logger.debug "Response body from error: #{e.response&.body}"
171
+ rescue Legate::ToolNetworkError => e
172
+ Legate.logger.error "Network error: #{e.message}"
173
+ rescue Legate::ToolError => e # Catch-all for other HttpClient or generic Legate tool errors
174
+ Legate.logger.error "A tool error occurred: #{e.message}"
175
+ Legate.logger.debug "Original cause: #{e.cause.inspect}" if e.cause
176
+ end
177
+ ```
178
+
179
+ ## 5. Authentication
180
+
181
+ The `HttpClient` module itself is unopinionated about authentication mechanisms. It **does not** manage authentication state (like tokens) or complex authentication flows (like OAuth2).
182
+
183
+ Your tool is responsible for:
184
+ 1. Determining the required authentication method (e.g., API key, Bearer token).
185
+ 2. Retrieving the necessary credentials. This might come from the tool's configuration, the `ToolContext` (session state, possibly using the Legate's credential management features), or an interactive flow.
186
+ 3. Injecting the credentials into the HTTP request, typically via the `headers:` parameter of the request helper methods.
187
+
188
+ ### Example: Bearer Token Authentication
189
+
190
+ ```ruby
191
+ # Assume 'token' is retrieved by the tool
192
+ auth_headers = { 'Authorization' => "Bearer #{token}" }
193
+ response = http_get('/protected/resource', headers: auth_headers)
194
+ ```
195
+
196
+ ### Example: API Key in Header
197
+
198
+ ```ruby
199
+ # Assume 'api_key' is retrieved by the tool
200
+ auth_headers = { 'X-Api-Key' => api_key }
201
+ response = http_get('/data', headers: auth_headers)
202
+ ```
203
+
204
+ Refer to the Legate's authentication documentation for details on managing and retrieving credentials within tools.
205
+
206
+ ## 6. Full Example
207
+
208
+ Here's a simple tool that fetches a random cat fact using the `HttpClient`.
209
+
210
+ ```ruby
211
+ require 'legate/tool'
212
+ require 'legate/tools/base/http_client'
213
+ require 'json'
214
+
215
+ module Legate
216
+ module Tools
217
+ # Tool name :cat_fact_tool is inferred from the class name.
218
+ class CatFactTool < Legate::Tool
219
+ include Legate::Tools::Base::HttpClient
220
+
221
+ tool_description 'Fetches a random cat fact.'
222
+ # No parameters needed for this tool.
223
+
224
+ def initialize(**options)
225
+ super(**options)
226
+ setup_http_client(
227
+ base_url: 'https://catfact.ninja/',
228
+ options: { read_timeout: 5 } # Short timeout for this API
229
+ )
230
+ end
231
+
232
+ private
233
+
234
+ # perform_execution takes two positional args (params, context) and must
235
+ # return a status Hash: { status: :success, result: ... } or
236
+ # { status: :error, error_message: ... }.
237
+ def perform_execution(_params, _context)
238
+ Legate.logger.info 'Fetching a cat fact...'
239
+ response = http_get('fact') # Path is relative to base_url
240
+ parsed_body = JSON.parse(response.body)
241
+ fact = parsed_body['fact']
242
+ Legate.logger.info "Retrieved cat fact: #{fact}"
243
+ { status: :success, result: fact }
244
+ rescue Legate::ToolHttpError => e
245
+ Legate.logger.error "HTTP error fetching cat fact: #{e.message}, Status: #{e.response&.status}"
246
+ { status: :error, error_message: "HTTP #{e.response&.status} - #{e.message}" }
247
+ rescue Legate::ToolTimeoutError => e
248
+ Legate.logger.error "Timeout fetching cat fact: #{e.message}"
249
+ { status: :error, error_message: 'Request timed out.' }
250
+ rescue Legate::ToolError => e
251
+ Legate.logger.error "Tool error fetching cat fact: #{e.message}"
252
+ { status: :error, error_message: e.message }
253
+ rescue JSON::ParserError => e
254
+ Legate.logger.error "Failed to parse cat fact response: #{e.message}"
255
+ { status: :error, error_message: 'Could not parse response from cat fact API.' }
256
+ end
257
+ end
258
+ end
259
+ end
260
+ ```
261
+
262
+ > **Note:** Rather than returning the result hash, a tool may also raise a `Legate::ToolError` subclass (e.g., on a failed HTTP request) and let the Legate runtime convert it into a standard error event. The built-in `Legate::Tools::CatFacts` tool uses that approach.
263
+
264
+ This guide should help you effectively use the `Legate::Tools::Base::HttpClient` module to build powerful, network-enabled tools within the Legate framework.
@@ -0,0 +1,137 @@
1
+ # LLM Providers
2
+
3
+ Legate's planner talks to a Large Language Model through a small, pluggable adapter interface (`Legate::LLM::Adapter`). **Gemini is the default**, but you can point Legate at any provider — a hosted API, a local model, or your own implementation — without changing your agents.
4
+
5
+ ## The adapter interface
6
+
7
+ An adapter is any object that responds to three methods:
8
+
9
+ ```ruby
10
+ class MyAdapter < Legate::LLM::Adapter
11
+ # Whether the adapter can make calls (e.g. an API key is present).
12
+ def available?; true; end
13
+
14
+ # The resolved model id, or nil if unavailable.
15
+ def model_name; 'my-model'; end
16
+
17
+ # Generate a completion for a single prompt.
18
+ # @param prompt [String]
19
+ # @param json [Boolean] request raw-JSON output where the provider supports it
20
+ # @return [String, nil] the model's text output (nil if unavailable)
21
+ def generate(prompt, json: false)
22
+ # ... call your provider, return the text ...
23
+ end
24
+ end
25
+ ```
26
+
27
+ The planner calls `generate(prompt, json: true)` and parses a JSON plan out of the returned text, so any provider that can return text (ideally JSON-constrained) works. An adapter can additionally implement `supports_structured_output?` + accept a `schema:` to **guarantee** valid plan JSON via the provider's native structured output — `Legate::LLM::Gemini` does this (Gemini `responseSchema`), so plans on Gemini are schema-constrained rather than parsed out of prose.
28
+
29
+ ## Built-in adapters
30
+
31
+ ### Gemini (default)
32
+
33
+ Used automatically when no other provider is configured. Requires `GOOGLE_API_KEY` (or `GEMINI_API_KEY`).
34
+
35
+ ```ruby
36
+ Legate::LLM::Gemini.new(model: 'gemini-3.5-flash', api_key: ENV['GOOGLE_API_KEY'])
37
+ ```
38
+
39
+ ### Ollama (local)
40
+
41
+ Talks to a local [Ollama](https://ollama.com) server over HTTP — **no API key, no cost, fully local**. Configure the host with the `:host` option or the `OLLAMA_HOST` env var (default `http://localhost:11434`).
42
+
43
+ ```ruby
44
+ Legate::LLM::Ollama.new(model: 'llama3') # http://localhost:11434
45
+ Legate::LLM::Ollama.new(model: 'qwen2.5', host: 'http://gpu-box:11434', read_timeout: 180)
46
+ ```
47
+
48
+ It requests JSON-constrained output (Ollama's `"format": "json"`) when the planner asks for a plan.
49
+
50
+ ## Selecting a provider for every agent
51
+
52
+ Set a factory once at boot. It receives `model:`, `api_key:`, and `logger:` keyword arguments and returns an adapter:
53
+
54
+ ```ruby
55
+ # Use a local Ollama model everywhere instead of Gemini
56
+ Legate::LLM.default_adapter_factory = lambda do |model:, **|
57
+ Legate::LLM::Ollama.new(model: model)
58
+ end
59
+ ```
60
+
61
+ When unset (the default), Legate uses the Gemini adapter. The per-agent `model_name` is passed through to your factory as `model:`, so you can still vary the model per agent.
62
+
63
+ ## Overriding for a single planner
64
+
65
+ If you construct a planner directly, you can inject an adapter for just that instance (this takes precedence over the global factory):
66
+
67
+ ```ruby
68
+ adapter = Legate::LLM::Ollama.new(model: 'llama3')
69
+ planner = Legate::Planner.new(agent: my_agent, llm_adapter: adapter)
70
+ ```
71
+
72
+ > Most users won't construct planners by hand — agents build their own. The `default_adapter_factory` above is the usual way to choose a provider.
73
+
74
+ ## Writing a custom adapter
75
+
76
+ To support a provider Legate doesn't ship (OpenAI, Anthropic, a gateway, a mock for tests), implement the three interface methods and wire it through the factory:
77
+
78
+ ```ruby
79
+ class OpenAIAdapter < Legate::LLM::Adapter
80
+ def initialize(model:, api_key: ENV['OPENAI_API_KEY'], logger: nil, **)
81
+ @model = model
82
+ @api_key = api_key
83
+ @logger = logger || Legate.logger
84
+ end
85
+
86
+ def available?
87
+ !@api_key.to_s.empty?
88
+ end
89
+
90
+ def model_name
91
+ available? ? @model : nil
92
+ end
93
+
94
+ def generate(prompt, json: false)
95
+ # POST to your provider, return the assistant's text.
96
+ # Pass a JSON-mode / response-format flag when json is true.
97
+ end
98
+ end
99
+
100
+ Legate::LLM.default_adapter_factory = lambda do |model:, **|
101
+ OpenAIAdapter.new(model: model)
102
+ end
103
+ ```
104
+
105
+ That's the whole contract — `available?`, `model_name`, `generate(prompt, json:)`.
106
+
107
+ ## Optional: native function calling
108
+
109
+ [Agentic (`:react`) agents](agentic_agents) pick their next action one step at a
110
+ time. By default the planner does this by prompting for a JSON action and parsing
111
+ it — works with any adapter. An adapter can opt into the provider's **native
112
+ tool-calling API** instead (more reliable: the tool name and arguments come back
113
+ typed, not parsed out of prose) by implementing two more methods:
114
+
115
+ ```ruby
116
+ def supports_function_calling?
117
+ true
118
+ end
119
+
120
+ # @param tools [Array<Hash>] each { name:, description:, parameters: <JSON Schema> }
121
+ # @return [Hash] { kind: :tool, name:, arguments:, thought: } or { kind: :final, text:, thought: }
122
+ def generate_with_tools(prompt, tools:)
123
+ # Call your provider's function-calling endpoint with the tool schemas,
124
+ # then return the structured choice in the neutral shape above.
125
+ end
126
+ ```
127
+
128
+ `Legate::LLM::Gemini` implements both, so agentic agents on Gemini use native
129
+ function calling automatically. Adapters that don't (Ollama, the default custom
130
+ adapter) inherit `supports_function_calling? => false` and stay on the JSON path —
131
+ no action needed. This affects only the agentic next-action decision; the
132
+ multi-step planner still uses `generate`.
133
+
134
+ ## See also
135
+
136
+ - [Agentic Agents](agentic_agents) — the ReAct loop these adapters reason through.
137
+ - [Legate Planner](../core_concepts/legate_planner) — how the planner turns a model response into an executable plan.
@@ -0,0 +1,232 @@
1
+ # Using Legate as an MCP Client
2
+
3
+ This guide explains how to configure and use an `Legate::Agent` to connect to external Model Context Protocol (MCP) servers and utilize the tools they provide. This allows your Legate agents to leverage a wider ecosystem of capabilities beyond their natively defined tools.
4
+
5
+ ## Architecture Overview: Legate as MCP Client
6
+
7
+ The following diagram illustrates how an Legate Agent interacts with an external MCP server:
8
+
9
+ ```mermaid
10
+ graph LR
11
+ subgraph Legate Agent Process
12
+ A[Legate::Agent]
13
+ Client[Legate::Mcp::Client]
14
+ Conn["Legate::Mcp::Connection::Stdio / ::Sse"]
15
+ WrapperTool["Legate::Mcp::ToolWrapper"]
16
+ Planner[Legate::Planner]
17
+
18
+ A -- Configures & Manages --> Client
19
+ Client -- Uses --> Conn
20
+ Planner -- Considers --> WrapperTool
21
+ A -- Executes --> WrapperTool
22
+ WrapperTool -- Delegates Call --> Client
23
+ end
24
+
25
+ subgraph External MCP Server Process
26
+ MCP_Srv[External MCP Server]
27
+ MCP_Tool[Actual External Tool]
28
+ end
29
+
30
+ Conn -- JSON-RPC --> MCP_Srv
31
+ MCP_Srv -- Executes --> MCP_Tool
32
+ MCP_Srv -- JSON-RPC Response --> Conn
33
+
34
+ style A fill:#ccf,stroke:#333,stroke-width:2px
35
+ style Client fill:#ccf,stroke:#333,stroke-width:2px
36
+ style WrapperTool fill:#ccf,stroke:#333,stroke-width:2px
37
+ style MCP_Srv fill:#f9f,stroke:#333,stroke-width:2px
38
+ ```
39
+
40
+ **Key Components:**
41
+
42
+ * **`Legate::Agent`:** The main Legate agent instance.
43
+ * **`Legate::Mcp::Client`:** Manages the connection and communication with a specific MCP server.
44
+ * **`Legate::Mcp::Connection::Stdio` / `Legate::Mcp::Connection::Sse`:** Handle the low-level transport (STDIO or SSE). The `Legate::Mcp::ConnectionManager` owns the connection lifecycle (connecting, discovering tools, and disconnecting).
45
+ * **`Legate::Mcp::ToolWrapper`:** A dynamically created proxy `Legate::Tool` that represents an external MCP tool within the Legate agent.
46
+ * **External MCP Server:** The third-party server providing tools via MCP.
47
+
48
+ ## 1. Configuration
49
+
50
+ > ### ⚠️ Security: MCP server configs are trusted input
51
+ >
52
+ > Connecting to an MCP server means **running code you trust**, exactly like adding a dependency to your project:
53
+ >
54
+ > - **`:stdio` servers launch a local subprocess** from the configured `command`/`args`. Whoever can set an agent's `mcp_servers` can run arbitrary local commands — that's the whole point of stdio MCP, not a flaw. Treat an MCP config like a `Gemfile` entry.
55
+ > - **`:sse`/remote servers are *not* SSRF-restricted.** MCP servers legitimately run on `localhost` or inside your private network, so Legate does **not** block private/loopback/metadata addresses for MCP URLs (unlike the outbound webhook tool, which does). A malicious MCP URL could reach internal services.
56
+ >
57
+ > The trust boundary is therefore **who can supply an agent definition's `mcp_servers`**. In code you control this is a non-issue. If you let *untrusted users* create or edit agent definitions (e.g. by exposing the bundled web UI on a public network), they can run commands and reach internal hosts on your server. **Do not expose the web UI to untrusted networks** (see the README's "Security model" section), and only configure MCP servers you would be comfortable adding as a dependency.
58
+
59
+ To enable an agent to act as an MCP client, you configure it with details of the MCP servers it should connect to.
60
+
61
+ ### 1.1. `mcp_servers` on the Agent Definition
62
+
63
+ MCP server connections are configured on the agent's `Legate::AgentDefinition` via the `mcp_servers` DSL method. Build the definition, then pass it to `Legate::Agent.new(definition:)`:
64
+
65
+ ```ruby
66
+ require 'legate'
67
+ require 'legate/mcp' # Ensure MCP modules are loaded
68
+
69
+ # Example: Configuration for an MCP server running via STDIO
70
+ stdio_server_config = {
71
+ type: :stdio, # or "stdio"
72
+ command: 'npx', # The command to run the server
73
+ args: ['@modelcontextprotocol/server-filesystem', '--stdio', '/path/to/accessible/directory']
74
+ }
75
+
76
+ # Example: Configuration for an MCP server via SSE (HTTP Server-Sent Events)
77
+ sse_server_config = {
78
+ type: :sse, # or "sse"
79
+ url: 'http://localhost:9292/mcp', # Base URL for the MCP server (SSE endpoint often /sse, messages often /messages)
80
+ # Optional: token: 'your-auth-token' # If the server requires bearer token authentication
81
+ }
82
+
83
+ mcp_client_definition = Legate::AgentDefinition.new.define do |a|
84
+ a.name :mcp_client_agent
85
+ a.description 'An agent that uses external MCP tools.'
86
+ a.instruction 'Use native and external MCP tools to help the user.'
87
+
88
+ a.use_tool :my_native_tool # Optional: native Legate tools
89
+ a.use_tool :read_file # Selected MCP tool (see section 1.2)
90
+ a.use_tool :list_directory # Selected MCP tool (see section 1.2)
91
+
92
+ # Array of MCP server configs (or pass them as separate arguments)
93
+ a.mcp_servers stdio_server_config, sse_server_config
94
+ end
95
+
96
+ my_agent = Legate::Agent.new(definition: mcp_client_definition)
97
+ ```
98
+
99
+ **Each server configuration hash requires:**
100
+
101
+ * `type`: Symbol or String. `:stdio` or `:sse`.
102
+ * **For `:stdio`:**
103
+ * `command`: String. The executable to run the server (e.g., `npx`, `python`, `bundle exec ruby`).
104
+ * `args`: Array of Strings. Arguments for the command.
105
+ * **For `:sse`:**
106
+ * `url`: String. The base URL of the MCP server. The client will attempt to connect to standard MCP sub-paths like `/sse` for events and `/messages` for calls.
107
+ * `token` (Optional): String. A bearer token for authentication if the server requires it.
108
+
109
+ ### 1.2. Selecting MCP Tools with `use_tool` (Crucial for V1)
110
+
111
+ Due to the dynamic nature of tool discovery and potential for naming conflicts or overwhelming numbers of tools, **it is currently essential to specify which tools from the MCP server(s) the agent should actually register and use.**
112
+
113
+ You do this with the same `a.use_tool :tool_name` DSL you use for native tools. When the agent starts, the `Legate::Mcp::ConnectionManager` discovers the server's tools and only registers those whose names match a `use_tool` selection:
114
+
115
+ ```ruby
116
+ definition = Legate::AgentDefinition.new.define do |a|
117
+ a.name :mcp_client_agent
118
+ a.instruction '...'
119
+ a.mcp_servers some_mcp_server_config
120
+ a.use_tool :tool_name_from_mcp_server1
121
+ a.use_tool :another_tool_from_mcp_server2
122
+ end
123
+ ```
124
+
125
+ * `use_tool` takes a Symbol; call it once per tool.
126
+ * These names **must exactly match** the tool names as exposed by the MCP server. You might need to inspect the MCP server's `tools/list` response or its documentation to get the correct names.
127
+ * If none of the agent's selected tool names match a server's tools, the agent will connect to the MCP server and perform the handshake but **will not register any tools** from that server.
128
+ * The Legate planner will only "see" and be able to use MCP tools that were selected via `use_tool` and successfully registered.
129
+
130
+ ## 2. How it Works
131
+
132
+ 1. **Agent Initialization (`Legate::Agent.new(definition:)`)**:
133
+ * The `mcp_servers` configurations are read from the definition.
134
+ 2. **Agent Start (`agent.start`)**:
135
+ * The agent's `Legate::Mcp::ConnectionManager` processes each configuration in the definition's `mcp_servers`:
136
+ * A connection is established using the specified `type` (`Legate::Mcp::Connection::Stdio` or `Legate::Mcp::Connection::Sse`). This includes the MCP `initialize` handshake.
137
+ * If the connection is successful, the manager calls `tools/list` on the MCP server.
138
+ * For each tool schema received from the server whose name matches one of the agent's `use_tool` selections:
139
+ * `Legate::Mcp::ToolWrapper.from_mcp_schema` is called. This method:
140
+ * Converts the MCP tool's JSON Schema (for `inputSchema`) into the Legate parameter format. (See [Schema Conversion Details](../advanced/mcp_schema_conversion))
141
+ * Dynamically creates an anonymous `Legate::Tool` subclass that acts as a proxy.
142
+ * Registers this proxy tool with the agent's specific `ToolRegistry`.
143
+ * The agent is now aware of both its native tools and the selected, wrapped MCP tools.
144
+ 3. **Planning (`agent.run_task`)**:
145
+ * The `Legate::Planner` considers all tools available in the agent's `ToolRegistry`, including the wrapped MCP tools.
146
+ 4. **Execution**:
147
+ * If the planner selects a wrapped MCP tool:
148
+ * The agent calls the wrapper tool's `execute` method.
149
+ * The `ToolWrapper` instance, in its `perform_execution` method:
150
+ * Translates the Legate parameters into the JSON structure expected by the external tool.
151
+ * Uses its associated `Legate::Mcp::Client` instance to send a `tools/call` request to the MCP server.
152
+ * Receives the response from the MCP server.
153
+ * Maps the MCP result or error back into the standard Legate status hash (e.g., `{status: :success, result: ...}` or `{status: :error, error_details: ...}`).
154
+ 5. **Agent Stop (`agent.stop`)**:
155
+ * The agent calls `disconnect` on all active `Legate::Mcp::Client` instances, terminating connections (e.g., stopping STDIO processes, closing SSE connections).
156
+
157
+ ## 3. Error Handling
158
+
159
+ * **Connection Errors**: If an `Legate::Mcp::Client` fails to connect during `agent.start` (e.g., STDIO command not found, SSE server unreachable, MCP handshake fails), an error is logged. That specific MCP server and its tools will be unavailable to the agent. The agent will typically continue starting with its native tools and any other successfully connected MCP servers.
160
+ * **Tool Execution Errors**:
161
+ * **Communication Errors (`Legate::Mcp::ConnectionError`, `Legate::Mcp::ProtocolError`)**: These indicate issues with the communication channel or MCP protocol itself (e.g., invalid JSON-RPC, timeout). The `ToolWrapper` catches these and returns an Legate `:error` status hash (e.g., `{status: :error, error_message: "MCP Communication Error: ...", error_details: {mcp_error_type: 'ConnectionError'}}`).
162
+ * **Remote Tool Errors (`Legate::Mcp::RemoteToolError`)**: These occur if the external MCP server successfully executed the tool, but the tool *itself* reported an error (e.g., via an MCP error object in the `tools/call` response). The `ToolWrapper` converts this into an Legate `:error` status hash, often including the original error message, code, and data from the MCP server in the `error_details` field.
163
+ * **Agent Behavior**: If a plan step involving an external MCP tool fails, the agent's plan execution typically halts, and the final agent response will reflect the error status hash, similar to failures with native tools.
164
+
165
+ ## 4. Example Snippet
166
+
167
+ ```ruby
168
+ # (Ensure Legate.configure and MCP server setup as per above)
169
+
170
+ # Define a native tool for comparison.
171
+ # The tool name :native_tool is inferred from the class name.
172
+ class NativeTool < Legate::Tool
173
+ tool_description 'A simple native tool.'
174
+
175
+ private
176
+
177
+ def perform_execution(_params, _context)
178
+ { status: :success, result: 'Native tool executed!' }
179
+ end
180
+ end
181
+ Legate::GlobalToolManager.register_tool(NativeTool)
182
+
183
+ # Configure the MCP server (e.g., filesystem server)
184
+ # Ensure '/tmp/mcp_test_dir' exists and is accessible by the npx command.
185
+ # Create a file: echo "Hello from MCP!" > /tmp/mcp_test_dir/test.txt
186
+ mcp_server_config = {
187
+ type: :stdio,
188
+ command: 'npx',
189
+ args: ['@modelcontextprotocol/server-filesystem', '--stdio', '/tmp/mcp_test_dir']
190
+ }
191
+
192
+ definition = Legate::AgentDefinition.new.define do |a|
193
+ a.name :mcp_client_agent
194
+ a.instruction 'Use native and filesystem tools to help the user.'
195
+ a.use_tool :native_tool
196
+ a.use_tool :read_file # Assuming server-filesystem exposes 'read_file'
197
+ a.mcp_servers mcp_server_config
198
+ end
199
+
200
+ agent = Legate::Agent.new(definition: definition)
201
+
202
+ agent.start
203
+
204
+ Legate.logger.info("Agent Tools Available: #{agent.available_tools_metadata.map { |t| t[:name] }}")
205
+
206
+ # Example task
207
+ session_service = Legate::SessionService::InMemory.new
208
+ session_id = session_service.create_session(app_name: agent.name, user_id: 'test').id
209
+
210
+ # This prompt assumes the LLM/planner knows to use 'read_file' for such a request
211
+ user_input = "Can you read the file named 'test.txt' for me?"
212
+
213
+ final_event = agent.run_task(
214
+ session_id: session_id,
215
+ user_input: user_input,
216
+ session_service: session_service
217
+ )
218
+
219
+ Legate.logger.info("Agent Task Result: #{final_event.content.inspect}")
220
+
221
+ agent.stop
222
+ ```
223
+ *For a more complete, runnable example, see `examples/14_mcp_client.rb` in the `legate` repository.*
224
+
225
+ ## 5. Security Considerations
226
+
227
+ * **STDIO Connections**: Assume a trusted local environment. The commands specified are executed on the system where `legate` is running.
228
+ * **SSE Connections**:
229
+ * Use HTTPS (`https://...`) for the MCP server URL in production to protect data in transit.
230
+ * If the server requires authentication, provide the token via the `token` parameter in the SSE configuration. Manage this token securely.
231
+ * **Trust**: By configuring an agent to connect to an MCP server, you are trusting that server and the tools it exposes. Be cautious about connecting to untrusted MCP servers, as they can execute code or access resources based on the tools they provide.
232
+ * **Selected Tools**: The `use_tool` selection mechanism provides a layer of control, ensuring that only explicitly approved tools from an MCP server are made available to the agent.