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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +345 -0
- data/bin/legate +13 -0
- data/examples/00_quickstart.rb +51 -0
- data/examples/01_simple_agent.rb +105 -0
- data/examples/02_multi_tool_agent.rb +140 -0
- data/examples/03_custom_tool.rb +93 -0
- data/examples/04_agent_instructions.rb +84 -0
- data/examples/05_state_and_sessions.rb +91 -0
- data/examples/06_callbacks.rb +186 -0
- data/examples/07_async_jobs.rb +112 -0
- data/examples/08_loop_agent.rb +197 -0
- data/examples/09_sequential_workflow.rb +40 -0
- data/examples/10_parallel_workflow.rb +34 -0
- data/examples/11_agent_delegation.rb +24 -0
- data/examples/12_http_client_tool.rb +156 -0
- data/examples/13_authentication.rb +220 -0
- data/examples/14_mcp_client.rb +154 -0
- data/examples/15_mcp_server.rb +79 -0
- data/examples/16_webhooks.rb +91 -0
- data/examples/README_sequential_agents.md +164 -0
- data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
- data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
- data/examples/advanced/auth/excon_middleware.rb +317 -0
- data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
- data/examples/advanced/auth/fiber_auth_example.rb +281 -0
- data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
- data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
- data/examples/advanced/auth/oauth2_auth.rb +419 -0
- data/examples/advanced/auth/oidc_auth.rb +514 -0
- data/examples/advanced/auth/openweather_api.rb +251 -0
- data/examples/advanced/auth/openweather_tool.rb +153 -0
- data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
- data/examples/advanced/auth/service_account.rb +135 -0
- data/examples/advanced/auth/test_with_httpbin.rb +202 -0
- data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
- data/examples/advanced/callback_monitoring.rb +679 -0
- data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
- data/examples/advanced/mas/loop_workflow.rb +28 -0
- data/examples/advanced/mas/mock_planner.rb +77 -0
- data/examples/advanced/mas/proper_delegation_example.rb +276 -0
- data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
- data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
- data/examples/advanced/mcp/mcp_server_async.rb +76 -0
- data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
- data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
- data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
- data/examples/advanced/random_calculator.rb +104 -0
- data/examples/advanced/sleep_agent.rb +153 -0
- data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
- data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
- data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
- data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
- data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
- data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
- data/examples/tools/oauth2_example.rb +136 -0
- data/examples/tools/sleepy_tool.rb +42 -0
- data/lib/legate/activity_log.rb +71 -0
- data/lib/legate/agent.rb +959 -0
- data/lib/legate/agent_code_generator.rb +185 -0
- data/lib/legate/agent_definition.rb +812 -0
- data/lib/legate/agentic/decision.rb +49 -0
- data/lib/legate/agentic/loop.rb +134 -0
- data/lib/legate/agentic.rb +5 -0
- data/lib/legate/agents/loop_agent.rb +248 -0
- data/lib/legate/agents/parallel_agent.rb +163 -0
- data/lib/legate/agents/sequential_agent.rb +190 -0
- data/lib/legate/agents.rb +14 -0
- data/lib/legate/auth/config.rb +148 -0
- data/lib/legate/auth/coordinator.rb +218 -0
- data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
- data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
- data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
- data/lib/legate/auth/credential.rb +157 -0
- data/lib/legate/auth/encryption.rb +108 -0
- data/lib/legate/auth/error.rb +94 -0
- data/lib/legate/auth/exchanged_credential.rb +180 -0
- data/lib/legate/auth/excon_middleware.rb +285 -0
- data/lib/legate/auth/http_client_utils.rb +364 -0
- data/lib/legate/auth/manager.rb +531 -0
- data/lib/legate/auth/manager_store.rb +394 -0
- data/lib/legate/auth/middleware_factory.rb +290 -0
- data/lib/legate/auth/runner.rb +279 -0
- data/lib/legate/auth/scheme.rb +125 -0
- data/lib/legate/auth/schemes/api_key.rb +212 -0
- data/lib/legate/auth/schemes/google_service_account.rb +108 -0
- data/lib/legate/auth/schemes/http_bearer.rb +98 -0
- data/lib/legate/auth/schemes/oauth2.rb +396 -0
- data/lib/legate/auth/schemes/openid_connect.rb +346 -0
- data/lib/legate/auth/schemes/service_account.rb +388 -0
- data/lib/legate/auth/schemes.rb +40 -0
- data/lib/legate/auth/token_manager.rb +362 -0
- data/lib/legate/auth/token_store.rb +86 -0
- data/lib/legate/auth/tool_context_extension.rb +97 -0
- data/lib/legate/auth/tool_integration.rb +188 -0
- data/lib/legate/auth/url_guard.rb +81 -0
- data/lib/legate/auth.rb +453 -0
- data/lib/legate/callbacks/callback_context.rb +71 -0
- data/lib/legate/cli/agent_commands.rb +950 -0
- data/lib/legate/cli/auth_commands.rb +520 -0
- data/lib/legate/cli/base_command.rb +24 -0
- data/lib/legate/cli/deployment_commands.rb +934 -0
- data/lib/legate/cli/output_helper.rb +108 -0
- data/lib/legate/cli/session_commands.rb +138 -0
- data/lib/legate/cli/skaffold_commands.rb +223 -0
- data/lib/legate/cli/tool_commands.rb +261 -0
- data/lib/legate/cli/web_commands.rb +182 -0
- data/lib/legate/cli.rb +40 -0
- data/lib/legate/configuration/webhooks.rb +113 -0
- data/lib/legate/configuration.rb +39 -0
- data/lib/legate/definition_store.rb +23 -0
- data/lib/legate/errors.rb +118 -0
- data/lib/legate/event.rb +161 -0
- data/lib/legate/gemini_ai_beta_patch.rb +39 -0
- data/lib/legate/generators/agent_generator.rb +412 -0
- data/lib/legate/generators/code_validator.rb +48 -0
- data/lib/legate/generators/legate/install_generator.rb +35 -0
- data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
- data/lib/legate/generators/legate/templates/initializer.rb +18 -0
- data/lib/legate/generators/runtime_tool_loader.rb +76 -0
- data/lib/legate/generators/tool_generator.rb +408 -0
- data/lib/legate/generators.rb +11 -0
- data/lib/legate/global_definition_registry.rb +506 -0
- data/lib/legate/global_tool_manager.rb +135 -0
- data/lib/legate/llm/adapter.rb +69 -0
- data/lib/legate/llm/gemini.rb +172 -0
- data/lib/legate/llm/ollama.rb +80 -0
- data/lib/legate/llm.rb +34 -0
- data/lib/legate/mcp/client.rb +320 -0
- data/lib/legate/mcp/connection/sse.rb +292 -0
- data/lib/legate/mcp/connection/stdio.rb +273 -0
- data/lib/legate/mcp/connection_manager.rb +103 -0
- data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
- data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
- data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
- data/lib/legate/mcp/tool_wrapper.rb +138 -0
- data/lib/legate/mcp/util/schema_converter.rb +134 -0
- data/lib/legate/mcp.rb +23 -0
- data/lib/legate/plan_executor.rb +375 -0
- data/lib/legate/planner.rb +839 -0
- data/lib/legate/rails/railtie.rb +43 -0
- data/lib/legate/rails.rb +9 -0
- data/lib/legate/redaction.rb +32 -0
- data/lib/legate/session.rb +299 -0
- data/lib/legate/session_service/active_record.rb +300 -0
- data/lib/legate/session_service/base.rb +68 -0
- data/lib/legate/session_service/event_broadcast.rb +74 -0
- data/lib/legate/session_service/in_memory.rb +188 -0
- data/lib/legate/tool/metadata_dsl.rb +122 -0
- data/lib/legate/tool.rb +276 -0
- data/lib/legate/tool_code_generator.rb +103 -0
- data/lib/legate/tool_context.rb +350 -0
- data/lib/legate/tool_loader.rb +39 -0
- data/lib/legate/tool_registry.rb +73 -0
- data/lib/legate/tool_result.rb +61 -0
- data/lib/legate/tools/agent_tool.rb +187 -0
- data/lib/legate/tools/base/http_client.rb +319 -0
- data/lib/legate/tools/base/safe_url.rb +56 -0
- data/lib/legate/tools/base_async_job_tool.rb +91 -0
- data/lib/legate/tools/calculator.rb +89 -0
- data/lib/legate/tools/cat_facts.rb +81 -0
- data/lib/legate/tools/check_job_status_tool.rb +48 -0
- data/lib/legate/tools/current_time_tool.rb +64 -0
- data/lib/legate/tools/echo.rb +43 -0
- data/lib/legate/tools/http_request_tool.rb +105 -0
- data/lib/legate/tools/random_number_tool.rb +64 -0
- data/lib/legate/tools/read_webpage_tool.rb +92 -0
- data/lib/legate/tools/sleepy_tool.rb +74 -0
- data/lib/legate/tools/webhook_tool.rb +146 -0
- data/lib/legate/version.rb +5 -0
- data/lib/legate/web/app.rb +984 -0
- data/lib/legate/web/public/css/main.css +4980 -0
- data/lib/legate/web/public/images/favicon-256.png +0 -0
- data/lib/legate/web/public/images/favicon-32.png +0 -0
- data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
- data/lib/legate/web/public/images/legate-logo-light.png +0 -0
- data/lib/legate/web/public/js/legate.js +616 -0
- data/lib/legate/web/public/styles/main.scss +4402 -0
- data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
- data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
- data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
- data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
- data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
- data/lib/legate/web/routes/api_routes.rb +56 -0
- data/lib/legate/web/routes/authentication_routes.rb +1541 -0
- data/lib/legate/web/routes/core_routes.rb +111 -0
- data/lib/legate/web/routes/documentation_routes.rb +220 -0
- data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
- data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
- data/lib/legate/web/sass_compiler.rb +73 -0
- data/lib/legate/web/views/_active_session_info.slim +25 -0
- data/lib/legate/web/views/_activity_list.slim +55 -0
- data/lib/legate/web/views/_agent_card.slim +56 -0
- data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
- data/lib/legate/web/views/_agent_status_controls.slim +71 -0
- data/lib/legate/web/views/_agent_tool_table.slim +74 -0
- data/lib/legate/web/views/_chat_message.slim +95 -0
- data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
- data/lib/legate/web/views/_display_agent_description.slim +11 -0
- data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
- data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
- data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
- data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
- data/lib/legate/web/views/_display_agent_model.slim +17 -0
- data/lib/legate/web/views/_display_agent_name.slim +42 -0
- data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
- data/lib/legate/web/views/_display_agent_type.slim +65 -0
- data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
- data/lib/legate/web/views/_edit_agent_description.slim +16 -0
- data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
- data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
- data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
- data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
- data/lib/legate/web/views/_edit_agent_model.slim +23 -0
- data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
- data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
- data/lib/legate/web/views/_edit_agent_type.slim +67 -0
- data/lib/legate/web/views/_session_error.slim +4 -0
- data/lib/legate/web/views/_skeleton.slim +69 -0
- data/lib/legate/web/views/_tool_card.slim +9 -0
- data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
- data/lib/legate/web/views/agent.slim +436 -0
- data/lib/legate/web/views/agent_auth.slim +562 -0
- data/lib/legate/web/views/agents.slim +369 -0
- data/lib/legate/web/views/auth.slim +112 -0
- data/lib/legate/web/views/auth_credential_detail.slim +327 -0
- data/lib/legate/web/views/auth_credentials.slim +261 -0
- data/lib/legate/web/views/auth_debug.slim +94 -0
- data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
- data/lib/legate/web/views/auth_mapping_new.slim +123 -0
- data/lib/legate/web/views/auth_mappings.slim +120 -0
- data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
- data/lib/legate/web/views/auth_schemes.slim +259 -0
- data/lib/legate/web/views/auth_test.slim +418 -0
- data/lib/legate/web/views/chat.slim +192 -0
- data/lib/legate/web/views/docs_index.slim +105 -0
- data/lib/legate/web/views/docs_show.slim +105 -0
- data/lib/legate/web/views/error_404.slim +5 -0
- data/lib/legate/web/views/index.slim +148 -0
- data/lib/legate/web/views/layout.slim +144 -0
- data/lib/legate/web/views/tool_detail.slim +87 -0
- data/lib/legate/web/views/tools.slim +50 -0
- data/lib/legate/web/webhook_listener.rb +367 -0
- data/lib/legate/web.rb +9 -0
- data/lib/legate.rb +220 -0
- data/public/docs/advanced/callbacks.md +828 -0
- data/public/docs/advanced/mcp_schema_conversion.md +59 -0
- data/public/docs/authentication/api_reference/config.md +210 -0
- data/public/docs/authentication/api_reference/credential.md +246 -0
- data/public/docs/authentication/api_reference/encryption.md +218 -0
- data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
- data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
- data/public/docs/authentication/api_reference/index.md +30 -0
- data/public/docs/authentication/api_reference/scheme.md +250 -0
- data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
- data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
- data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
- data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
- data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
- data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
- data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
- data/public/docs/authentication/api_reference/token_manager.md +221 -0
- data/public/docs/authentication/api_reference/token_store.md +146 -0
- data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
- data/public/docs/authentication/guides/api_key.md +190 -0
- data/public/docs/authentication/guides/bearer.md +172 -0
- data/public/docs/authentication/guides/configuration.md +255 -0
- data/public/docs/authentication/guides/custom_flow.md +523 -0
- data/public/docs/authentication/guides/index.md +24 -0
- data/public/docs/authentication/guides/migration.md +435 -0
- data/public/docs/authentication/guides/oauth2.md +252 -0
- data/public/docs/authentication/guides/oidc.md +241 -0
- data/public/docs/authentication/guides/overview.md +155 -0
- data/public/docs/authentication/guides/secure_storage.md +301 -0
- data/public/docs/authentication/guides/service_account.md +228 -0
- data/public/docs/authentication/guides/token_lifecycle.md +295 -0
- data/public/docs/authentication/guides/web_ui_integration.md +504 -0
- data/public/docs/authentication/index.md +58 -0
- data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
- data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
- data/public/docs/authentication/troubleshooting/index.md +11 -0
- data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
- data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
- data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
- data/public/docs/cli/legate_cli_usage.md +363 -0
- data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
- data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
- data/public/docs/core_concepts/legate_configuration.md +116 -0
- data/public/docs/core_concepts/legate_definition_store.md +102 -0
- data/public/docs/core_concepts/legate_planner.md +94 -0
- data/public/docs/core_concepts/legate_session_service.md +104 -0
- data/public/docs/error_handling/legate_error_handling.md +122 -0
- data/public/docs/examples.md +199 -0
- data/public/docs/getting_started.md +111 -0
- data/public/docs/guides/agentic_agents.md +137 -0
- data/public/docs/guides/ai_code_generators.md +437 -0
- data/public/docs/guides/auto_loading.md +326 -0
- data/public/docs/guides/configuring_agent_webhooks.md +219 -0
- data/public/docs/guides/http_client_usage.md +264 -0
- data/public/docs/guides/llm_providers.md +137 -0
- data/public/docs/guides/mcp_client_integration.md +232 -0
- data/public/docs/guides/mcp_server_exposure.md +206 -0
- data/public/docs/guides/rails_integration.md +128 -0
- data/public/docs/guides/sending_outbound_webhooks.md +227 -0
- data/public/docs/guides/streaming.md +112 -0
- data/public/docs/guides/webhooks.md +288 -0
- data/public/docs/introduction.md +51 -0
- data/public/docs/multi_agent_systems/advanced_features.md +57 -0
- data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
- data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
- data/public/docs/multi_agent_systems/state_management.md +47 -0
- data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
- data/public/docs/tools/legate_built_in_tools.md +332 -0
- data/public/docs/tools/legate_tools_and_registry.md +263 -0
- data/public/docs/web_ui/legate_web_ui.md +137 -0
- 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.
|