robot_lab 0.3.0 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +1 -1
- data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
- data/lib/robot_lab/version.rb +1 -1
- metadata +3 -253
- data/.architecture/AGENTS.md +0 -32
- data/.architecture/config.yml +0 -8
- data/.architecture/members.yml +0 -60
- data/.architecture/reviews/feature-free-will.md +0 -490
- data/.architecture/reviews/overall-codebase.md +0 -427
- data/.claude/settings.local.json +0 -57
- data/.codex/config.toml +0 -2
- data/.envrc +0 -6
- data/.github/workflows/deploy-github-pages.yml +0 -52
- data/.irbrc +0 -6
- data/.loki +0 -65
- data/.quality/reek_baseline.txt +0 -43
- data/.rubocop.yml +0 -10
- data/Archspec.rb +0 -44
- data/CLAUDE.md +0 -141
- data/COMMITS.md +0 -196
- data/Rakefile +0 -193
- data/_typos.toml +0 -21
- data/agent2agent_review.md +0 -192
- data/agentf_improvements.md +0 -253
- data/agents.md +0 -14
- data/docs/api/core/index.md +0 -147
- data/docs/api/core/memory.md +0 -578
- data/docs/api/core/network.md +0 -553
- data/docs/api/core/result.md +0 -211
- data/docs/api/core/robot.md +0 -1667
- data/docs/api/core/state.md +0 -145
- data/docs/api/core/tool.md +0 -520
- data/docs/api/errors.md +0 -323
- data/docs/api/hooks.md +0 -469
- data/docs/api/index.md +0 -137
- data/docs/api/mcp/client.md +0 -387
- data/docs/api/mcp/index.md +0 -267
- data/docs/api/mcp/server.md +0 -252
- data/docs/api/mcp/transports.md +0 -339
- data/docs/api/messages/index.md +0 -126
- data/docs/api/messages/text-message.md +0 -170
- data/docs/api/messages/tool-call-message.md +0 -206
- data/docs/api/messages/tool-result-message.md +0 -242
- data/docs/api/messages/user-message.md +0 -232
- data/docs/api/skills.md +0 -423
- data/docs/api/streaming/context.md +0 -280
- data/docs/api/streaming/events.md +0 -185
- data/docs/api/streaming/index.md +0 -156
- data/docs/api/support.md +0 -846
- data/docs/architecture/core-concepts.md +0 -575
- data/docs/architecture/index.md +0 -252
- data/docs/architecture/message-flow.md +0 -366
- data/docs/architecture/network-orchestration.md +0 -478
- data/docs/architecture/robot-execution.md +0 -396
- data/docs/architecture/state-management.md +0 -410
- data/docs/assets/css/custom.css +0 -56
- data/docs/assets/images/robot_lab.jpg +0 -0
- data/docs/concepts.md +0 -641
- data/docs/examples/basic-chat.md +0 -258
- data/docs/examples/index.md +0 -425
- data/docs/examples/mcp-server.md +0 -403
- data/docs/examples/multi-robot-network.md +0 -476
- data/docs/examples/tool-usage.md +0 -468
- data/docs/getting-started/configuration.md +0 -645
- data/docs/getting-started/index.md +0 -56
- data/docs/getting-started/installation.md +0 -197
- data/docs/getting-started/quick-start.md +0 -293
- data/docs/guides/building-robots.md +0 -1187
- data/docs/guides/creating-networks.md +0 -736
- data/docs/guides/hooks.md +0 -1206
- data/docs/guides/index.md +0 -78
- data/docs/guides/knowledge.md +0 -219
- data/docs/guides/mcp-integration.md +0 -740
- data/docs/guides/memory.md +0 -458
- data/docs/guides/observability.md +0 -733
- data/docs/guides/streaming.md +0 -323
- data/docs/guides/using-tools.md +0 -780
- data/docs/index.md +0 -280
- data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
- data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
- data/examples/.envrc +0 -3
- data/examples/01_simple_robot.rb +0 -31
- data/examples/02_tools.rb +0 -109
- data/examples/03_network.rb +0 -118
- data/examples/04_mcp.rb +0 -217
- data/examples/05_streaming.rb +0 -137
- data/examples/06_prompt_templates.rb +0 -325
- data/examples/07_network_memory.rb +0 -313
- data/examples/08_llm_config.rb +0 -162
- data/examples/09_chaining.rb +0 -251
- data/examples/10_memory.rb +0 -315
- data/examples/11_network_introspection.rb +0 -239
- data/examples/12_message_bus.rb +0 -67
- data/examples/13_spawn.rb +0 -85
- data/examples/14_rusty_circuit/.envrc +0 -1
- data/examples/14_rusty_circuit/.gitignore +0 -1
- data/examples/14_rusty_circuit/README.md +0 -1
- data/examples/14_rusty_circuit/comic.rb +0 -149
- data/examples/14_rusty_circuit/display.rb +0 -217
- data/examples/14_rusty_circuit/heckler.rb +0 -65
- data/examples/14_rusty_circuit/open_mic.rb +0 -132
- data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
- data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
- data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
- data/examples/14_rusty_circuit/scout.rb +0 -165
- data/examples/14_rusty_circuit/show.log +0 -234
- data/examples/15_memory_network_and_bus/.envrc +0 -1
- data/examples/15_memory_network_and_bus/README.md +0 -66
- data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
- data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
- data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
- data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
- data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
- data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
- data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
- data/examples/15_memory_network_and_bus/output/memory.json +0 -13
- data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
- data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
- data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
- data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
- data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
- data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
- data/examples/16_writers_room/.envrc +0 -1
- data/examples/16_writers_room/display.rb +0 -158
- data/examples/16_writers_room/output/.gitignore +0 -4
- data/examples/16_writers_room/output/README.md +0 -69
- data/examples/16_writers_room/output/opus_001.md +0 -263
- data/examples/16_writers_room/output/opus_001_notes.log +0 -470
- data/examples/16_writers_room/output/opus_002.md +0 -245
- data/examples/16_writers_room/output/opus_002_notes.log +0 -546
- data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
- data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
- data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
- data/examples/16_writers_room/prompts/writer.md +0 -37
- data/examples/16_writers_room/room.rb +0 -186
- data/examples/16_writers_room/tools.rb +0 -173
- data/examples/16_writers_room/writer.rb +0 -121
- data/examples/16_writers_room/writers_room.rb +0 -256
- data/examples/17_skills.rb +0 -234
- data/examples/18_rails/.envrc +0 -3
- data/examples/18_rails/.gitignore +0 -5
- data/examples/18_rails/Gemfile +0 -11
- data/examples/18_rails/README.md +0 -67
- data/examples/18_rails/Rakefile +0 -4
- data/examples/18_rails/app/controllers/application_controller.rb +0 -4
- data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
- data/examples/18_rails/app/jobs/application_job.rb +0 -4
- data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
- data/examples/18_rails/app/models/application_record.rb +0 -5
- data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
- data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
- data/examples/18_rails/app/robots/chat_robot.rb +0 -25
- data/examples/18_rails/app/tools/time_tool.rb +0 -9
- data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
- data/examples/18_rails/app/views/chat/index.html.erb +0 -67
- data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
- data/examples/18_rails/bin/dev +0 -7
- data/examples/18_rails/bin/rails +0 -6
- data/examples/18_rails/bin/setup +0 -15
- data/examples/18_rails/config/application.rb +0 -33
- data/examples/18_rails/config/cable.yml +0 -2
- data/examples/18_rails/config/database.yml +0 -5
- data/examples/18_rails/config/environment.rb +0 -4
- data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
- data/examples/18_rails/config/routes.rb +0 -6
- data/examples/18_rails/config.ru +0 -4
- data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
- data/examples/19_token_tracking.rb +0 -138
- data/examples/20_circuit_breaker.rb +0 -147
- data/examples/21_learning_loop.rb +0 -181
- data/examples/22_context_compression.rb +0 -172
- data/examples/23_convergence.rb +0 -133
- data/examples/24_structured_delegation.rb +0 -151
- data/examples/25_history_search/conversation.jsonl +0 -30
- data/examples/25_history_search.rb +0 -133
- data/examples/26_document_store/api_versioning_adr.md +0 -52
- data/examples/26_document_store/incident_postmortem.md +0 -46
- data/examples/26_document_store/postgres_runbook.md +0 -49
- data/examples/26_document_store/redis_caching_guide.md +0 -48
- data/examples/26_document_store/sidekiq_guide.md +0 -51
- data/examples/26_document_store.rb +0 -140
- data/examples/27_incident_response/README.md +0 -65
- data/examples/27_incident_response/incident_response.rb +0 -261
- data/examples/28_mcp_discovery.rb +0 -113
- data/examples/29_ractor_tools.rb +0 -240
- data/examples/30_ractor_network.rb +0 -252
- data/examples/31_launch_assessment.rb +0 -246
- data/examples/32_newsletter_reader.rb +0 -193
- data/examples/33_stock_generator.rb +0 -80
- data/examples/33_stock_predictor.rb +0 -327
- data/examples/34_agentskills.rb +0 -76
- data/examples/35_hooks.rb +0 -266
- data/examples/README.md +0 -550
- data/examples/common.rb +0 -180
- data/examples/prompts/assistant.md +0 -5
- data/examples/prompts/audit_trail.md +0 -8
- data/examples/prompts/billing.md +0 -10
- data/examples/prompts/classifier.md +0 -7
- data/examples/prompts/comedian.md +0 -6
- data/examples/prompts/comedy_critic.md +0 -10
- data/examples/prompts/configurable.md +0 -9
- data/examples/prompts/dispatcher.md +0 -12
- data/examples/prompts/entity_extractor.md +0 -14
- data/examples/prompts/escalation.md +0 -42
- data/examples/prompts/frontmatter_mcp_test.md +0 -9
- data/examples/prompts/frontmatter_named_test.md +0 -5
- data/examples/prompts/frontmatter_tools_test.md +0 -6
- data/examples/prompts/general.md +0 -7
- data/examples/prompts/github_assistant.md +0 -14
- data/examples/prompts/helper.md +0 -4
- data/examples/prompts/incident_responder.md +0 -18
- data/examples/prompts/keyword_extractor.md +0 -11
- data/examples/prompts/llm_config_demo.md +0 -20
- data/examples/prompts/order_support.md +0 -35
- data/examples/prompts/os_advocate.md +0 -13
- data/examples/prompts/os_chief.md +0 -13
- data/examples/prompts/os_editor.md +0 -13
- data/examples/prompts/parameterized_main_test.md +0 -6
- data/examples/prompts/pii_redactor.md +0 -7
- data/examples/prompts/product_support.md +0 -37
- data/examples/prompts/runbook_protocol.md +0 -12
- data/examples/prompts/sentiment_analyzer.md +0 -12
- data/examples/prompts/skill_a_test.md +0 -4
- data/examples/prompts/skill_b_test.md +0 -4
- data/examples/prompts/skill_config_test.md +0 -5
- data/examples/prompts/skill_cycle_a_test.md +0 -6
- data/examples/prompts/skill_cycle_b_test.md +0 -6
- data/examples/prompts/skill_description_test.md +0 -4
- data/examples/prompts/skill_leaf_test.md +0 -4
- data/examples/prompts/skill_nested_test.md +0 -6
- data/examples/prompts/skill_refs_main_test.md +0 -6
- data/examples/prompts/skill_self_ref_test.md +0 -6
- data/examples/prompts/skill_with_mcp_test.md +0 -9
- data/examples/prompts/skill_with_params_test.md +0 -6
- data/examples/prompts/skill_with_robot_name_test.md +0 -5
- data/examples/prompts/skill_with_tools_test.md +0 -6
- data/examples/prompts/sre_compliance.md +0 -10
- data/examples/prompts/structured_output.md +0 -7
- data/examples/prompts/synthesizer.md +0 -17
- data/examples/prompts/technical.md +0 -10
- data/examples/prompts/template_with_skills_test.md +0 -6
- data/examples/prompts/triage.md +0 -22
- data/examples/ruboruby.md +0 -423
- data/examples/run_all.rb +0 -60
- data/examples/xyzzy.rb +0 -97
- data/logfile +0 -8
- data/mkdocs.yml +0 -209
- data/robot_concurrency.md +0 -38
- data/simple_acp_review.md +0 -298
- data/temp.md +0 -6
- data/tool_manifest_plan.md +0 -155
|
@@ -1,740 +0,0 @@
|
|
|
1
|
-
# MCP Integration
|
|
2
|
-
|
|
3
|
-
RobotLab supports the Model Context Protocol (MCP) for connecting to external tool servers.
|
|
4
|
-
|
|
5
|
-
## What is MCP?
|
|
6
|
-
|
|
7
|
-
MCP is a protocol that allows LLM applications to connect to external servers that provide tools, resources, and context. This enables:
|
|
8
|
-
|
|
9
|
-
- Reusable tool servers across applications
|
|
10
|
-
- Separation of tool logic from AI logic
|
|
11
|
-
- Dynamic tool discovery
|
|
12
|
-
|
|
13
|
-
## Configuring MCP Servers
|
|
14
|
-
|
|
15
|
-
### At Robot Level
|
|
16
|
-
|
|
17
|
-
Use the `mcp:` parameter on `RobotLab.build` to connect a robot to MCP servers:
|
|
18
|
-
|
|
19
|
-
```ruby
|
|
20
|
-
robot = RobotLab.build(
|
|
21
|
-
name: "coder",
|
|
22
|
-
template: :developer,
|
|
23
|
-
mcp: [
|
|
24
|
-
{
|
|
25
|
-
name: "filesystem",
|
|
26
|
-
transport: {
|
|
27
|
-
type: "stdio",
|
|
28
|
-
command: "mcp-server-filesystem",
|
|
29
|
-
args: ["--root", "/home/user/projects"]
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
name: "github",
|
|
34
|
-
transport: {
|
|
35
|
-
type: "stdio",
|
|
36
|
-
command: "mcp-server-github",
|
|
37
|
-
env: { "GITHUB_TOKEN" => ENV["GITHUB_TOKEN"] }
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
]
|
|
41
|
-
)
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### In Template Front Matter
|
|
45
|
-
|
|
46
|
-
MCP servers can be declared directly in a template's YAML front matter, making the template fully self-contained:
|
|
47
|
-
|
|
48
|
-
```markdown title="prompts/github_assistant.md"
|
|
49
|
-
---
|
|
50
|
-
description: GitHub assistant with MCP tool access
|
|
51
|
-
mcp:
|
|
52
|
-
- name: github
|
|
53
|
-
transport:
|
|
54
|
-
type: stdio
|
|
55
|
-
command: npx
|
|
56
|
-
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
57
|
-
---
|
|
58
|
-
You are a helpful GitHub assistant with access to GitHub tools via MCP.
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
```ruby
|
|
62
|
-
# MCP config comes from the template — no mcp: parameter needed
|
|
63
|
-
robot = RobotLab.build(template: :github_assistant)
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Constructor `mcp:` overrides frontmatter `mcp:` when provided.
|
|
67
|
-
|
|
68
|
-
> [!CAUTION]
|
|
69
|
-
> `transport:` must be a **nested mapping**, exactly as above. The flat form —
|
|
70
|
-
> `transport: stdio` with sibling `command:`/`args:` keys — does not raise:
|
|
71
|
-
> `Server` calls `transform_keys` on the transport value, a String raises
|
|
72
|
-
> `NoMethodError: undefined method 'transform_keys' for an instance of String`,
|
|
73
|
-
> and that exception is caught by MCP setup's rescue. The robot builds and runs
|
|
74
|
-
> with **zero tools** from that server. It is not silent, though: the failure is
|
|
75
|
-
> logged at `WARN` through `RobotLab.config.logger` (`$stdout` by default) and
|
|
76
|
-
> the server name is recorded in `robot.failed_mcp_server_names`.
|
|
77
|
-
|
|
78
|
-
### Hierarchical Configuration
|
|
79
|
-
|
|
80
|
-
The `mcp:` parameter supports three modes:
|
|
81
|
-
|
|
82
|
-
| Value | Behavior |
|
|
83
|
-
|-------|----------|
|
|
84
|
-
| `:none` | No MCP servers (default) |
|
|
85
|
-
| `:inherit` | Inherit from network or global config |
|
|
86
|
-
| `[...]` | Explicit array of server configurations |
|
|
87
|
-
|
|
88
|
-
```ruby
|
|
89
|
-
# Inherit from network/config
|
|
90
|
-
robot = RobotLab.build(
|
|
91
|
-
name: "reader",
|
|
92
|
-
system_prompt: "You help read files.",
|
|
93
|
-
mcp: :inherit
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
# Disable MCP explicitly
|
|
97
|
-
robot = RobotLab.build(
|
|
98
|
-
name: "calculator",
|
|
99
|
-
system_prompt: "You do math.",
|
|
100
|
-
mcp: :none
|
|
101
|
-
)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Resolution Order
|
|
105
|
-
|
|
106
|
-
MCP configuration resolves through a hierarchy: **runtime > robot build > network > global config**. Each level can override the previous:
|
|
107
|
-
|
|
108
|
-
```
|
|
109
|
-
Global (RobotLab.config.mcp)
|
|
110
|
-
-> Network (network config: / task config:)
|
|
111
|
-
-> Robot (mcp: :inherit | :none | [...])
|
|
112
|
-
-> Runtime (robot.run("msg", mcp: [...]), and task mcp: [...])
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
> [!NOTE]
|
|
116
|
-
> A task's `mcp:`/`tools:` are **not** a separate network tier — `Task` puts
|
|
117
|
-
> them into the run params, so they arrive as the **runtime** value for that
|
|
118
|
-
> robot's `run()`. Only a `config:` (on the network or on the task) acts as the
|
|
119
|
-
> parent level that `:inherit` resolves against.
|
|
120
|
-
|
|
121
|
-
## Timeout Configuration
|
|
122
|
-
|
|
123
|
-
Every server config accepts a `timeout:`. The default is 15 seconds
|
|
124
|
-
(`RobotLab::MCP::Server::DEFAULT_TIMEOUT`). Set it at the server level:
|
|
125
|
-
|
|
126
|
-
```ruby
|
|
127
|
-
robot = RobotLab.build(
|
|
128
|
-
name: "patient_bot",
|
|
129
|
-
system_prompt: "You help with slow operations.",
|
|
130
|
-
mcp: [
|
|
131
|
-
{
|
|
132
|
-
name: "heavy_server",
|
|
133
|
-
transport: { type: "stdio", command: "heavy-mcp-server" },
|
|
134
|
-
timeout: 60 # seconds
|
|
135
|
-
}
|
|
136
|
-
]
|
|
137
|
-
)
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Values >= 1000 are auto-converted from milliseconds to seconds. The minimum timeout is 1 second:
|
|
141
|
-
|
|
142
|
-
| Given | Stored |
|
|
143
|
-
|-------|--------|
|
|
144
|
-
| `nil` | `15` (the default) |
|
|
145
|
-
| `30` | `30.0` |
|
|
146
|
-
| `5000` | `5.0` (read as milliseconds) |
|
|
147
|
-
| `0.5` | `1` (floored to the 1-second minimum) |
|
|
148
|
-
|
|
149
|
-
> [!WARNING]
|
|
150
|
-
> **Only the Stdio transport actually enforces the timeout.** SSE, WebSocket,
|
|
151
|
-
> and StreamableHTTP store the value and never reference it — a remote MCP
|
|
152
|
-
> server that stops responding will hang the call indefinitely regardless of
|
|
153
|
-
> what you set here. Apply your own timeout around remote-transport calls if
|
|
154
|
-
> you need one.
|
|
155
|
-
|
|
156
|
-
## Transport Types
|
|
157
|
-
|
|
158
|
-
Valid `type:` values are exactly: `stdio`, `sse`, `ws`, `websocket`,
|
|
159
|
-
`streamable-http`, `http` (`RobotLab::MCP::Server::VALID_TRANSPORT_TYPES`).
|
|
160
|
-
|
|
161
|
-
An invalid type raises `ArgumentError: Invalid transport type: <type>. Must be
|
|
162
|
-
one of: stdio, sse, ws, websocket, streamable-http, http` — but **only if you
|
|
163
|
-
construct the server or client yourself**. Through the robot (`mcp:` on
|
|
164
|
-
`RobotLab.build`) the error is caught by the same rescue that handles any other
|
|
165
|
-
connect failure: it is logged at `WARN` and the server name is recorded in
|
|
166
|
-
`robot.failed_mcp_server_names`, and the robot carries on with zero tools from
|
|
167
|
-
that server. See [Connection Errors](#connection-errors).
|
|
168
|
-
|
|
169
|
-
```ruby
|
|
170
|
-
bad = { name: "oops", transport: { type: "streamable_http", url: "https://x" } }
|
|
171
|
-
|
|
172
|
-
RobotLab::MCP::Server.new(**bad) # => raises ArgumentError
|
|
173
|
-
|
|
174
|
-
robot = RobotLab.build(name: "r", system_prompt: "...", mcp: [bad])
|
|
175
|
-
robot.run("hi", mcp: :inherit, tools: :inherit)
|
|
176
|
-
# logs: WARN Robot 'r' error connecting to MCP server 'oops': Invalid transport type: ...
|
|
177
|
-
robot.failed_mcp_server_names # => ["oops"]
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### Stdio Transport
|
|
181
|
-
|
|
182
|
-
Communicate via stdin/stdout with a subprocess:
|
|
183
|
-
|
|
184
|
-
```ruby
|
|
185
|
-
{
|
|
186
|
-
name: "server_name",
|
|
187
|
-
transport: {
|
|
188
|
-
type: "stdio",
|
|
189
|
-
command: "mcp-server-command",
|
|
190
|
-
args: ["--option", "value"],
|
|
191
|
-
env: { "API_KEY" => ENV["API_KEY"] }
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### WebSocket Transport
|
|
197
|
-
|
|
198
|
-
Connect via WebSocket:
|
|
199
|
-
|
|
200
|
-
```ruby
|
|
201
|
-
{
|
|
202
|
-
name: "remote_server",
|
|
203
|
-
transport: {
|
|
204
|
-
type: "websocket",
|
|
205
|
-
url: "ws://localhost:8080/mcp"
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
!!! note "Dependency Required"
|
|
211
|
-
WebSocket transport requires the `async-websocket` gem. (It also reaches for
|
|
212
|
-
`Async::HTTP::Endpoint` without requiring it, so `async-http` must be loaded
|
|
213
|
-
as well.)
|
|
214
|
-
|
|
215
|
-
### SSE Transport
|
|
216
|
-
|
|
217
|
-
Server-Sent Events transport:
|
|
218
|
-
|
|
219
|
-
```ruby
|
|
220
|
-
{
|
|
221
|
-
name: "sse_server",
|
|
222
|
-
transport: {
|
|
223
|
-
type: "sse",
|
|
224
|
-
url: "http://localhost:8080/sse"
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
!!! note "Dependency Required"
|
|
230
|
-
SSE transport requires the `async-http` gem.
|
|
231
|
-
|
|
232
|
-
### HTTP Transport
|
|
233
|
-
|
|
234
|
-
Streamable HTTP transport with session support:
|
|
235
|
-
|
|
236
|
-
```ruby
|
|
237
|
-
{
|
|
238
|
-
name: "http_server",
|
|
239
|
-
transport: {
|
|
240
|
-
type: "streamable-http", # or "http" — NOT "streamable_http"
|
|
241
|
-
url: "https://api.example.com/mcp",
|
|
242
|
-
session_id: "optional_session_id",
|
|
243
|
-
auth_provider: -> { "Bearer #{fetch_token}" }
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
!!! note "Dependency Required"
|
|
249
|
-
Streamable HTTP transport requires the `async-http` gem.
|
|
250
|
-
|
|
251
|
-
> [!CAUTION]
|
|
252
|
-
> The type is spelled with a **hyphen**. `type: "streamable_http"` (underscore)
|
|
253
|
-
> raises `ArgumentError: Invalid transport type: streamable_http`.
|
|
254
|
-
|
|
255
|
-
> [!WARNING]
|
|
256
|
-
> The remote transports connect inside an un-awaited `Async` block and set
|
|
257
|
-
> their connected flag **before** the MCP initialize handshake. `client.connected?`
|
|
258
|
-
> therefore returns `true` even when the host is unreachable — do not treat it
|
|
259
|
-
> as proof the server answered. Stdio is the only transport that blocks on the
|
|
260
|
-
> handshake and reports a connect failure synchronously.
|
|
261
|
-
|
|
262
|
-
## Using MCP Tools
|
|
263
|
-
|
|
264
|
-
Once configured, MCP tools are discovered on connect and made available to the
|
|
265
|
-
robot. **Connecting is not automatic** — see the warning below:
|
|
266
|
-
|
|
267
|
-
```ruby
|
|
268
|
-
robot = RobotLab.build(
|
|
269
|
-
name: "helper",
|
|
270
|
-
system_prompt: <<~PROMPT,
|
|
271
|
-
You can help users with GitHub tasks.
|
|
272
|
-
Use available tools to search repositories, create issues, etc.
|
|
273
|
-
PROMPT
|
|
274
|
-
mcp: [
|
|
275
|
-
{ name: "github", transport: { type: "stdio", command: "mcp-server-github" } }
|
|
276
|
-
]
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
# mcp: :inherit connects the servers; tools: :inherit sends their tools to the LLM.
|
|
280
|
-
result = robot.run("Find repositories about machine learning", mcp: :inherit, tools: :inherit)
|
|
281
|
-
puts result.last_text_content
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
> [!CAUTION]
|
|
285
|
-
> **A plain `robot.run(message)` neither connects MCP servers nor sends any
|
|
286
|
-
> tools.** Both the `mcp:` and `tools:` parameters of `run` default to `:none`,
|
|
287
|
-
> which means "zero this turn" — the build-time `mcp:` list is simply not
|
|
288
|
-
> consulted. You need **both** flags:
|
|
289
|
-
>
|
|
290
|
-
> ```ruby
|
|
291
|
-
> robot.run("...") # no MCP connection, no tools
|
|
292
|
-
> robot.run("...", mcp: :inherit) # connects, but sends zero tools
|
|
293
|
-
> robot.run("...", mcp: :inherit, tools: :inherit) # connects AND sends its tools
|
|
294
|
-
> ```
|
|
295
|
-
|
|
296
|
-
## Filtering MCP Tools
|
|
297
|
-
|
|
298
|
-
Use the `tools:` parameter to restrict which tools (including MCP-discovered
|
|
299
|
-
tools) reach the LLM. It is a **name allowlist**, and it must be supplied at the
|
|
300
|
-
level that actually runs — the `run()` call (or the network `task`, which
|
|
301
|
-
forwards to `run()`):
|
|
302
|
-
|
|
303
|
-
```ruby
|
|
304
|
-
robot = RobotLab.build(
|
|
305
|
-
name: "reader",
|
|
306
|
-
system_prompt: "You help read and search files.",
|
|
307
|
-
mcp: [
|
|
308
|
-
{ name: "filesystem", transport: { type: "stdio", command: "mcp-server-fs" } }
|
|
309
|
-
]
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
robot.run("Summarise the README",
|
|
313
|
-
mcp: :inherit,
|
|
314
|
-
tools: %w[read_file search_files list_directory]) # only these
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
> [!WARNING]
|
|
318
|
-
> A build-time `tools: %w[...]` allowlist has no effect on its own, because the
|
|
319
|
-
> runtime default of `:none` sends zero tools regardless. And for a
|
|
320
|
-
> **standalone** robot, do not write `tools: :inherit` at build time as a
|
|
321
|
-
> workaround: there the parent level is the global `:none`, so it produces an
|
|
322
|
-
> allowlist of `["none"]` that matches nothing. Leave build-time `tools:` unset
|
|
323
|
-
> and pass the filter at run time.
|
|
324
|
-
>
|
|
325
|
-
> Inside a **network** whose `config:` supplies `tools:`/`mcp:`, this reverses:
|
|
326
|
-
> the parent level is the network's list, and build-time `:inherit` is what
|
|
327
|
-
> makes the robot pick it up. See
|
|
328
|
-
> [Network-Wide Tool and MCP Defaults](creating-networks.md#network-wide-tool-and-mcp-defaults).
|
|
329
|
-
|
|
330
|
-
## MCP in Networks
|
|
331
|
-
|
|
332
|
-
When running robots in a network, use per-task MCP configuration. Remember that
|
|
333
|
-
a task's `mcp:`/`tools:` become that robot's **runtime** values, so `tools:`
|
|
334
|
-
must be set too or the MCP tools will not be sent:
|
|
335
|
-
|
|
336
|
-
```ruby
|
|
337
|
-
network = RobotLab.create_network(name: "dev_pipeline") do
|
|
338
|
-
task :planner, planner_robot, depends_on: :none
|
|
339
|
-
task :coder, coder_robot,
|
|
340
|
-
mcp: [
|
|
341
|
-
{ name: "filesystem", transport: { type: "stdio", command: "mcp-server-fs" } }
|
|
342
|
-
],
|
|
343
|
-
tools: :inherit,
|
|
344
|
-
depends_on: [:planner]
|
|
345
|
-
task :reviewer, reviewer_robot, depends_on: [:coder]
|
|
346
|
-
end
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
## Common MCP Servers
|
|
350
|
-
|
|
351
|
-
### Filesystem
|
|
352
|
-
|
|
353
|
-
```ruby
|
|
354
|
-
{
|
|
355
|
-
name: "filesystem",
|
|
356
|
-
transport: {
|
|
357
|
-
type: "stdio",
|
|
358
|
-
command: "mcp-server-filesystem",
|
|
359
|
-
args: ["--root", "/path/to/files"]
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
Tools: `read_file`, `write_file`, `list_directory`, `search_files`
|
|
365
|
-
|
|
366
|
-
### GitHub
|
|
367
|
-
|
|
368
|
-
```ruby
|
|
369
|
-
{
|
|
370
|
-
name: "github",
|
|
371
|
-
transport: {
|
|
372
|
-
type: "stdio",
|
|
373
|
-
command: "mcp-server-github",
|
|
374
|
-
env: { "GITHUB_TOKEN" => ENV["GITHUB_TOKEN"] }
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
Tools: `search_repositories`, `create_issue`, `get_file_contents`, etc.
|
|
380
|
-
|
|
381
|
-
### Database
|
|
382
|
-
|
|
383
|
-
```ruby
|
|
384
|
-
{
|
|
385
|
-
name: "postgres",
|
|
386
|
-
transport: {
|
|
387
|
-
type: "stdio",
|
|
388
|
-
command: "mcp-server-postgres",
|
|
389
|
-
env: { "DATABASE_URL" => ENV["DATABASE_URL"] }
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
Tools: `query`, `list_tables`, `describe_table`
|
|
395
|
-
|
|
396
|
-
## MCP Server and Client Objects
|
|
397
|
-
|
|
398
|
-
For programmatic access, you can work with MCP objects directly:
|
|
399
|
-
|
|
400
|
-
```ruby
|
|
401
|
-
# Server configuration
|
|
402
|
-
server = RobotLab::MCP::Server.new(
|
|
403
|
-
name: "my_server",
|
|
404
|
-
transport: {
|
|
405
|
-
type: "stdio",
|
|
406
|
-
command: "my-mcp-server"
|
|
407
|
-
}
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
# Client connection
|
|
411
|
-
client = RobotLab::MCP::Client.new(server)
|
|
412
|
-
client.connect
|
|
413
|
-
|
|
414
|
-
client.connected? # => true
|
|
415
|
-
client.list_tools # => Array of tool definitions
|
|
416
|
-
client.call_tool("search", { query: "ruby" })
|
|
417
|
-
client.list_resources # => Array of resource definitions
|
|
418
|
-
client.disconnect
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
> [!NOTE]
|
|
422
|
-
> `MCP::Client#initialize(server_or_config, poller: nil)` takes its server as a
|
|
423
|
-
> **positional** argument — either a `Server` instance or a config Hash.
|
|
424
|
-
> `Client.new(name: "x", transport: {...})` raises `ArgumentError: wrong number
|
|
425
|
-
> of arguments (given 0, expected 1)`; wrap the hash in braces:
|
|
426
|
-
> `Client.new({ name: "x", transport: {...} })`.
|
|
427
|
-
|
|
428
|
-
## Connection Multiplexing
|
|
429
|
-
|
|
430
|
-
When a robot connects to several local (stdio) MCP servers, each client normally blocks independently while waiting for a response. `MCP::ConnectionPoller` replaces this with a single `IO.select` call across all registered stdout file descriptors, dispatching each response to the pending request for that client.
|
|
431
|
-
|
|
432
|
-
This is primarily useful in networks where many robots each have multiple stdio MCP servers. Async-based transports (SSE, WebSocket, StreamableHTTP) are unaffected — they already use the Async fiber scheduler.
|
|
433
|
-
|
|
434
|
-
```ruby
|
|
435
|
-
# Create a shared poller
|
|
436
|
-
poller = RobotLab::MCP::ConnectionPoller.new.start
|
|
437
|
-
|
|
438
|
-
# Pass the poller when building clients
|
|
439
|
-
client1 = RobotLab::MCP::Client.new(
|
|
440
|
-
{ name: "filesystem", transport: { type: "stdio", command: "mcp-server-fs" } },
|
|
441
|
-
poller: poller
|
|
442
|
-
)
|
|
443
|
-
client2 = RobotLab::MCP::Client.new(
|
|
444
|
-
{ name: "github", transport: { type: "stdio", command: "mcp-server-github" } },
|
|
445
|
-
poller: poller
|
|
446
|
-
)
|
|
447
|
-
|
|
448
|
-
client1.connect # registers with poller
|
|
449
|
-
client2.connect # registers with poller
|
|
450
|
-
|
|
451
|
-
# Both clients share the IO.select loop
|
|
452
|
-
client1.list_tools
|
|
453
|
-
client2.list_tools
|
|
454
|
-
|
|
455
|
-
poller.stop
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
Without a shared poller each client uses its own blocking `Timeout.timeout` call. With a poller, responses from any registered server wake the poller's select loop, which dispatches to the right waiting thread via a `Thread::Queue`.
|
|
459
|
-
|
|
460
|
-
!!! note
|
|
461
|
-
Only stdio clients are registered with the poller. SSE, WebSocket, and StreamableHTTP clients passed a `poller:` argument ignore it silently.
|
|
462
|
-
|
|
463
|
-
## Server Discovery
|
|
464
|
-
|
|
465
|
-
When a robot has many MCP servers configured, connecting to all of them upfront is wasteful — most servers will be irrelevant for any given user message. **Server Discovery** uses TF cosine similarity to select only the semantically relevant servers before the first `ensure_mcp_clients` call.
|
|
466
|
-
|
|
467
|
-
### Enabling Discovery
|
|
468
|
-
|
|
469
|
-
Add `description:` to each server config and set `mcp_discovery: true` on the robot:
|
|
470
|
-
|
|
471
|
-
```ruby
|
|
472
|
-
robot = RobotLab.build(
|
|
473
|
-
name: "assistant",
|
|
474
|
-
system_prompt: "You are a helpful assistant.",
|
|
475
|
-
mcp_discovery: true,
|
|
476
|
-
mcp: [
|
|
477
|
-
{
|
|
478
|
-
name: "filesystem",
|
|
479
|
-
description: "Read, write, and search local files and directories",
|
|
480
|
-
transport: { type: "stdio", command: "mcp-server-filesystem" }
|
|
481
|
-
},
|
|
482
|
-
{
|
|
483
|
-
name: "github",
|
|
484
|
-
description: "GitHub repos, issues, pull requests, code search",
|
|
485
|
-
transport: { type: "stdio", command: "mcp-server-github" }
|
|
486
|
-
},
|
|
487
|
-
{
|
|
488
|
-
name: "brew",
|
|
489
|
-
description: "Install, update, and manage macOS packages via Homebrew",
|
|
490
|
-
transport: { type: "stdio", command: "mcp-server-brew" }
|
|
491
|
-
}
|
|
492
|
-
]
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
# Discovery connects only :brew for this message — filesystem and github are skipped.
|
|
496
|
-
# mcp: :inherit is required; a plain run() resolves the MCP list to empty and
|
|
497
|
-
# discovery never runs.
|
|
498
|
-
robot.run("install imagemagick", mcp: :inherit, tools: :inherit)
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
### How It Works
|
|
502
|
-
|
|
503
|
-
`MCP::ServerDiscovery.select(query, from:, threshold:)` computes TF cosine similarity between the user's query and each server's topic text (`name + description`). Servers scoring at or above `DEFAULT_THRESHOLD` (0.05) are returned; the rest are excluded.
|
|
504
|
-
|
|
505
|
-
The threshold is intentionally low — server descriptions are short, so raw cosine scores are naturally small even for on-topic queries.
|
|
506
|
-
|
|
507
|
-
Discovery only applies on the **first** MCP-resolving `run()` call (before the robot is marked MCP-initialized). Once a set of servers is connected they remain connected for the robot's lifetime, preserving tool continuity across a conversation.
|
|
508
|
-
|
|
509
|
-
### Fallback Behaviour
|
|
510
|
-
|
|
511
|
-
All servers are returned unchanged when any of the following apply:
|
|
512
|
-
|
|
513
|
-
| Condition | Reason |
|
|
514
|
-
|-----------|--------|
|
|
515
|
-
| No server has a `description` field | Nothing to score against |
|
|
516
|
-
| `classifier` gem unavailable | Raises `DependencyError`, caught internally |
|
|
517
|
-
| Query is blank or nil | Nothing to compare |
|
|
518
|
-
| No server scores ≥ threshold | Rather fall back than leave the robot with no tools |
|
|
519
|
-
|
|
520
|
-
### Using the API Directly
|
|
521
|
-
|
|
522
|
-
```ruby
|
|
523
|
-
servers = [
|
|
524
|
-
{ name: "filesystem", description: "Read and write files",
|
|
525
|
-
transport: { type: "stdio", command: "mcp-server-fs" } },
|
|
526
|
-
{ name: "github", description: "GitHub repos and PRs",
|
|
527
|
-
transport: { type: "stdio", command: "mcp-server-github" } }
|
|
528
|
-
]
|
|
529
|
-
|
|
530
|
-
relevant = RobotLab::MCP::ServerDiscovery.select(
|
|
531
|
-
"search github repos",
|
|
532
|
-
from: servers,
|
|
533
|
-
threshold: 0.05 # optional, default
|
|
534
|
-
)
|
|
535
|
-
# => [{ name: "github", ... }]
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
> [!IMPORTANT]
|
|
539
|
-
> Scoring is lexical, not semantic — it compares word stems against each
|
|
540
|
-
> server's `description`. A query that happens to share no stems with **any**
|
|
541
|
-
> description scores 0.0 everywhere, trips the "nothing above threshold"
|
|
542
|
-
> fallback in the table above, and gets **all** servers back rather than none.
|
|
543
|
-
> With the two servers above:
|
|
544
|
-
>
|
|
545
|
-
> ```ruby
|
|
546
|
-
> select("search github repos", from: servers) # => ["github"]
|
|
547
|
-
> select("read and write files", from: servers) # => ["filesystem"]
|
|
548
|
-
> select("list open pull requests", from: servers) # => ["filesystem", "github"] <- fallback
|
|
549
|
-
> ```
|
|
550
|
-
>
|
|
551
|
-
> The last one selects everything because the description says "PRs", not "pull
|
|
552
|
-
> requests". Write descriptions using the words your prompts will actually use.
|
|
553
|
-
|
|
554
|
-
## Connection Resilience
|
|
555
|
-
|
|
556
|
-
### Eager Connection
|
|
557
|
-
|
|
558
|
-
`connect_mcp!` connects the robot's configured servers immediately, without
|
|
559
|
-
waiting for a `run()` that passes `mcp: :inherit`:
|
|
560
|
-
|
|
561
|
-
```ruby
|
|
562
|
-
robot = RobotLab.build(
|
|
563
|
-
name: "assistant",
|
|
564
|
-
system_prompt: "You help with tasks.",
|
|
565
|
-
mcp: [
|
|
566
|
-
{ name: "github", transport: { type: "stdio", command: "mcp-server-github" } },
|
|
567
|
-
{ name: "filesystem", transport: { type: "stdio", command: "mcp-server-fs" } }
|
|
568
|
-
]
|
|
569
|
-
)
|
|
570
|
-
|
|
571
|
-
robot.connect_mcp!
|
|
572
|
-
|
|
573
|
-
# Check which servers failed
|
|
574
|
-
if robot.failed_mcp_server_names.any?
|
|
575
|
-
puts "Failed to connect: #{robot.failed_mcp_server_names.join(', ')}"
|
|
576
|
-
end
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
> [!NOTE]
|
|
580
|
-
> `connect_mcp!` only opens the connections. A later plain `robot.run(message)`
|
|
581
|
-
> still sends the LLM **zero** tools, because `run`'s `tools:` defaults to
|
|
582
|
-
> `:none`. Eager connection and tool visibility are separate switches.
|
|
583
|
-
|
|
584
|
-
### Automatic Retry
|
|
585
|
-
|
|
586
|
-
Failed MCP servers are retried on subsequent `run()` calls **that resolve to a
|
|
587
|
-
non-empty MCP list** — i.e. runs that pass `mcp: :inherit` (or an explicit
|
|
588
|
-
array). If a server was down when the robot first connected, it is retried
|
|
589
|
-
transparently:
|
|
590
|
-
|
|
591
|
-
```ruby
|
|
592
|
-
robot.run("First message", mcp: :inherit, tools: :inherit) # github connects, filesystem fails
|
|
593
|
-
# ... filesystem comes back up ...
|
|
594
|
-
robot.run("Second message", mcp: :inherit, tools: :inherit) # filesystem retried and connects
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
### Injecting External MCP Clients
|
|
598
|
-
|
|
599
|
-
Host applications that manage MCP connections externally can inject pre-connected clients into a robot:
|
|
600
|
-
|
|
601
|
-
```ruby
|
|
602
|
-
robot.inject_mcp!(clients: my_clients, tools: my_tools)
|
|
603
|
-
```
|
|
604
|
-
|
|
605
|
-
This skips the normal connection process and marks the robot as MCP-initialized.
|
|
606
|
-
|
|
607
|
-
## Error Handling
|
|
608
|
-
|
|
609
|
-
### Connection Errors
|
|
610
|
-
|
|
611
|
-
MCP connection failures are **not raised**. They are logged as warnings and
|
|
612
|
-
recorded on the robot; the run continues without that server's tools, and one
|
|
613
|
-
failing server does not prevent the others from connecting. Inspect the result
|
|
614
|
-
rather than rescuing:
|
|
615
|
-
|
|
616
|
-
```ruby
|
|
617
|
-
result = robot.run("Search for repos", mcp: :inherit, tools: :inherit)
|
|
618
|
-
|
|
619
|
-
if robot.failed_mcp_server_names.any?
|
|
620
|
-
warn "MCP servers unavailable: #{robot.failed_mcp_server_names.join(', ')}"
|
|
621
|
-
end
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
### Timeout Errors
|
|
625
|
-
|
|
626
|
-
Stdio transports wrap all blocking I/O with a configurable timeout. On expiry
|
|
627
|
-
the transport raises `MCPError`. Note that the two messages differ, and neither
|
|
628
|
-
uses the server *name* you configured — the handshake message names the
|
|
629
|
-
**command**, and the per-request message names nothing at all:
|
|
630
|
-
|
|
631
|
-
```ruby
|
|
632
|
-
# Handshake timed out during connect:
|
|
633
|
-
# RobotLab::MCPError: MCP server 'heavy-mcp-server' did not respond within 15s
|
|
634
|
-
|
|
635
|
-
# A later request timed out:
|
|
636
|
-
# RobotLab::MCPError: MCP server did not respond within 15s
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
> [!IMPORTANT]
|
|
640
|
-
> `MCP::Client#connect` rescues **every** `StandardError`, logs
|
|
641
|
-
> `"MCP connection failed for <name>: ..."` at `:warn`, and returns `self` with
|
|
642
|
-
> `connected?` false. So a connect-time timeout never reaches your `begin/rescue`
|
|
643
|
-
> — check `client.connected?` (or `robot.failed_mcp_server_names`) instead.
|
|
644
|
-
> Per-request calls such as `list_tools` and `call_tool` **do** propagate
|
|
645
|
-
> `MCPError`.
|
|
646
|
-
|
|
647
|
-
SSE, WebSocket, and StreamableHTTP raise no timeout error at all — see the
|
|
648
|
-
warning under [Timeout Configuration](#timeout-configuration).
|
|
649
|
-
|
|
650
|
-
## Disconnecting
|
|
651
|
-
|
|
652
|
-
Robots can be manually disconnected from MCP servers:
|
|
653
|
-
|
|
654
|
-
```ruby
|
|
655
|
-
robot.disconnect # Disconnect all MCP clients
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
## Patterns
|
|
659
|
-
|
|
660
|
-
### Development vs Production
|
|
661
|
-
|
|
662
|
-
```ruby
|
|
663
|
-
mcp_config = if Rails.env.development?
|
|
664
|
-
[{ name: "local_fs", transport: { type: "stdio", command: "mcp-fs", args: ["--root", "."] } }]
|
|
665
|
-
else
|
|
666
|
-
[{ name: "s3", transport: { type: "stdio", command: "mcp-s3" } }]
|
|
667
|
-
end
|
|
668
|
-
|
|
669
|
-
robot = RobotLab.build(
|
|
670
|
-
name: "file_handler",
|
|
671
|
-
system_prompt: "You manage files.",
|
|
672
|
-
mcp: mcp_config
|
|
673
|
-
)
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
### Dynamic Server Selection
|
|
677
|
-
|
|
678
|
-
```ruby
|
|
679
|
-
def mcp_servers_for_user(user)
|
|
680
|
-
servers = []
|
|
681
|
-
servers << github_server if user.github_connected?
|
|
682
|
-
servers << slack_server if user.slack_connected?
|
|
683
|
-
servers
|
|
684
|
-
end
|
|
685
|
-
|
|
686
|
-
robot = RobotLab.build(
|
|
687
|
-
name: "assistant",
|
|
688
|
-
system_prompt: "You help the user with connected services.",
|
|
689
|
-
mcp: mcp_servers_for_user(current_user)
|
|
690
|
-
)
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
## Best Practices
|
|
694
|
-
|
|
695
|
-
### 1. Use Environment Variables for Credentials
|
|
696
|
-
|
|
697
|
-
```ruby
|
|
698
|
-
{
|
|
699
|
-
name: "github",
|
|
700
|
-
transport: {
|
|
701
|
-
type: "stdio",
|
|
702
|
-
command: "mcp-server-github",
|
|
703
|
-
env: {
|
|
704
|
-
"GITHUB_TOKEN" => ENV["GITHUB_TOKEN"],
|
|
705
|
-
"GITHUB_ORG" => ENV["GITHUB_ORG"]
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
### 2. Limit Tool Access
|
|
712
|
-
|
|
713
|
-
Restrict which MCP tools reach the LLM with a `tools:` allowlist on the run:
|
|
714
|
-
|
|
715
|
-
```ruby
|
|
716
|
-
robot = RobotLab.build(
|
|
717
|
-
name: "reader",
|
|
718
|
-
system_prompt: "You read and search files.",
|
|
719
|
-
mcp: [{ name: "fs", transport: { type: "stdio", command: "mcp-fs" } }]
|
|
720
|
-
)
|
|
721
|
-
|
|
722
|
-
robot.run("Find the config file",
|
|
723
|
-
mcp: :inherit,
|
|
724
|
-
tools: %w[read_file search_files]) # No write access
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
### 3. Use Appropriate Transports
|
|
728
|
-
|
|
729
|
-
| Transport | Best For |
|
|
730
|
-
|-----------|----------|
|
|
731
|
-
| `stdio` | Local servers, CLI tools. The only transport that enforces `timeout:` and reports connect failures synchronously. |
|
|
732
|
-
| `websocket` (or `ws`) | Persistent connections, bidirectional |
|
|
733
|
-
| `sse` | Server push, event streams |
|
|
734
|
-
| `streamable-http` (or `http`) | Remote APIs, session-based |
|
|
735
|
-
|
|
736
|
-
## Next Steps
|
|
737
|
-
|
|
738
|
-
- [Using Tools](using-tools.md) - Local tool patterns
|
|
739
|
-
- [Creating Networks](creating-networks.md) - Network configuration
|
|
740
|
-
- [API Reference: MCP](../api/mcp/index.md) - Complete MCP API
|