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.
Files changed (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +1 -1
  4. data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
  5. data/lib/robot_lab/version.rb +1 -1
  6. metadata +3 -253
  7. data/.architecture/AGENTS.md +0 -32
  8. data/.architecture/config.yml +0 -8
  9. data/.architecture/members.yml +0 -60
  10. data/.architecture/reviews/feature-free-will.md +0 -490
  11. data/.architecture/reviews/overall-codebase.md +0 -427
  12. data/.claude/settings.local.json +0 -57
  13. data/.codex/config.toml +0 -2
  14. data/.envrc +0 -6
  15. data/.github/workflows/deploy-github-pages.yml +0 -52
  16. data/.irbrc +0 -6
  17. data/.loki +0 -65
  18. data/.quality/reek_baseline.txt +0 -43
  19. data/.rubocop.yml +0 -10
  20. data/Archspec.rb +0 -44
  21. data/CLAUDE.md +0 -141
  22. data/COMMITS.md +0 -196
  23. data/Rakefile +0 -193
  24. data/_typos.toml +0 -21
  25. data/agent2agent_review.md +0 -192
  26. data/agentf_improvements.md +0 -253
  27. data/agents.md +0 -14
  28. data/docs/api/core/index.md +0 -147
  29. data/docs/api/core/memory.md +0 -578
  30. data/docs/api/core/network.md +0 -553
  31. data/docs/api/core/result.md +0 -211
  32. data/docs/api/core/robot.md +0 -1667
  33. data/docs/api/core/state.md +0 -145
  34. data/docs/api/core/tool.md +0 -520
  35. data/docs/api/errors.md +0 -323
  36. data/docs/api/hooks.md +0 -469
  37. data/docs/api/index.md +0 -137
  38. data/docs/api/mcp/client.md +0 -387
  39. data/docs/api/mcp/index.md +0 -267
  40. data/docs/api/mcp/server.md +0 -252
  41. data/docs/api/mcp/transports.md +0 -339
  42. data/docs/api/messages/index.md +0 -126
  43. data/docs/api/messages/text-message.md +0 -170
  44. data/docs/api/messages/tool-call-message.md +0 -206
  45. data/docs/api/messages/tool-result-message.md +0 -242
  46. data/docs/api/messages/user-message.md +0 -232
  47. data/docs/api/skills.md +0 -423
  48. data/docs/api/streaming/context.md +0 -280
  49. data/docs/api/streaming/events.md +0 -185
  50. data/docs/api/streaming/index.md +0 -156
  51. data/docs/api/support.md +0 -846
  52. data/docs/architecture/core-concepts.md +0 -575
  53. data/docs/architecture/index.md +0 -252
  54. data/docs/architecture/message-flow.md +0 -366
  55. data/docs/architecture/network-orchestration.md +0 -478
  56. data/docs/architecture/robot-execution.md +0 -396
  57. data/docs/architecture/state-management.md +0 -410
  58. data/docs/assets/css/custom.css +0 -56
  59. data/docs/assets/images/robot_lab.jpg +0 -0
  60. data/docs/concepts.md +0 -641
  61. data/docs/examples/basic-chat.md +0 -258
  62. data/docs/examples/index.md +0 -425
  63. data/docs/examples/mcp-server.md +0 -403
  64. data/docs/examples/multi-robot-network.md +0 -476
  65. data/docs/examples/tool-usage.md +0 -468
  66. data/docs/getting-started/configuration.md +0 -645
  67. data/docs/getting-started/index.md +0 -56
  68. data/docs/getting-started/installation.md +0 -197
  69. data/docs/getting-started/quick-start.md +0 -293
  70. data/docs/guides/building-robots.md +0 -1187
  71. data/docs/guides/creating-networks.md +0 -736
  72. data/docs/guides/hooks.md +0 -1206
  73. data/docs/guides/index.md +0 -78
  74. data/docs/guides/knowledge.md +0 -219
  75. data/docs/guides/mcp-integration.md +0 -740
  76. data/docs/guides/memory.md +0 -458
  77. data/docs/guides/observability.md +0 -733
  78. data/docs/guides/streaming.md +0 -323
  79. data/docs/guides/using-tools.md +0 -780
  80. data/docs/index.md +0 -280
  81. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  82. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  83. data/examples/.envrc +0 -3
  84. data/examples/01_simple_robot.rb +0 -31
  85. data/examples/02_tools.rb +0 -109
  86. data/examples/03_network.rb +0 -118
  87. data/examples/04_mcp.rb +0 -217
  88. data/examples/05_streaming.rb +0 -137
  89. data/examples/06_prompt_templates.rb +0 -325
  90. data/examples/07_network_memory.rb +0 -313
  91. data/examples/08_llm_config.rb +0 -162
  92. data/examples/09_chaining.rb +0 -251
  93. data/examples/10_memory.rb +0 -315
  94. data/examples/11_network_introspection.rb +0 -239
  95. data/examples/12_message_bus.rb +0 -67
  96. data/examples/13_spawn.rb +0 -85
  97. data/examples/14_rusty_circuit/.envrc +0 -1
  98. data/examples/14_rusty_circuit/.gitignore +0 -1
  99. data/examples/14_rusty_circuit/README.md +0 -1
  100. data/examples/14_rusty_circuit/comic.rb +0 -149
  101. data/examples/14_rusty_circuit/display.rb +0 -217
  102. data/examples/14_rusty_circuit/heckler.rb +0 -65
  103. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  104. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  105. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  106. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  107. data/examples/14_rusty_circuit/scout.rb +0 -165
  108. data/examples/14_rusty_circuit/show.log +0 -234
  109. data/examples/15_memory_network_and_bus/.envrc +0 -1
  110. data/examples/15_memory_network_and_bus/README.md +0 -66
  111. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  112. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  113. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  114. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  115. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  116. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  117. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  118. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  119. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  120. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  121. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  122. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  123. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  124. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  125. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  126. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  127. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  128. data/examples/16_writers_room/.envrc +0 -1
  129. data/examples/16_writers_room/display.rb +0 -158
  130. data/examples/16_writers_room/output/.gitignore +0 -4
  131. data/examples/16_writers_room/output/README.md +0 -69
  132. data/examples/16_writers_room/output/opus_001.md +0 -263
  133. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  134. data/examples/16_writers_room/output/opus_002.md +0 -245
  135. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  136. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  137. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  138. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  139. data/examples/16_writers_room/prompts/writer.md +0 -37
  140. data/examples/16_writers_room/room.rb +0 -186
  141. data/examples/16_writers_room/tools.rb +0 -173
  142. data/examples/16_writers_room/writer.rb +0 -121
  143. data/examples/16_writers_room/writers_room.rb +0 -256
  144. data/examples/17_skills.rb +0 -234
  145. data/examples/18_rails/.envrc +0 -3
  146. data/examples/18_rails/.gitignore +0 -5
  147. data/examples/18_rails/Gemfile +0 -11
  148. data/examples/18_rails/README.md +0 -67
  149. data/examples/18_rails/Rakefile +0 -4
  150. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  151. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  152. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  153. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  154. data/examples/18_rails/app/models/application_record.rb +0 -5
  155. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  156. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  157. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  158. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  159. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  160. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  161. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  162. data/examples/18_rails/bin/dev +0 -7
  163. data/examples/18_rails/bin/rails +0 -6
  164. data/examples/18_rails/bin/setup +0 -15
  165. data/examples/18_rails/config/application.rb +0 -33
  166. data/examples/18_rails/config/cable.yml +0 -2
  167. data/examples/18_rails/config/database.yml +0 -5
  168. data/examples/18_rails/config/environment.rb +0 -4
  169. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  170. data/examples/18_rails/config/routes.rb +0 -6
  171. data/examples/18_rails/config.ru +0 -4
  172. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  173. data/examples/19_token_tracking.rb +0 -138
  174. data/examples/20_circuit_breaker.rb +0 -147
  175. data/examples/21_learning_loop.rb +0 -181
  176. data/examples/22_context_compression.rb +0 -172
  177. data/examples/23_convergence.rb +0 -133
  178. data/examples/24_structured_delegation.rb +0 -151
  179. data/examples/25_history_search/conversation.jsonl +0 -30
  180. data/examples/25_history_search.rb +0 -133
  181. data/examples/26_document_store/api_versioning_adr.md +0 -52
  182. data/examples/26_document_store/incident_postmortem.md +0 -46
  183. data/examples/26_document_store/postgres_runbook.md +0 -49
  184. data/examples/26_document_store/redis_caching_guide.md +0 -48
  185. data/examples/26_document_store/sidekiq_guide.md +0 -51
  186. data/examples/26_document_store.rb +0 -140
  187. data/examples/27_incident_response/README.md +0 -65
  188. data/examples/27_incident_response/incident_response.rb +0 -261
  189. data/examples/28_mcp_discovery.rb +0 -113
  190. data/examples/29_ractor_tools.rb +0 -240
  191. data/examples/30_ractor_network.rb +0 -252
  192. data/examples/31_launch_assessment.rb +0 -246
  193. data/examples/32_newsletter_reader.rb +0 -193
  194. data/examples/33_stock_generator.rb +0 -80
  195. data/examples/33_stock_predictor.rb +0 -327
  196. data/examples/34_agentskills.rb +0 -76
  197. data/examples/35_hooks.rb +0 -266
  198. data/examples/README.md +0 -550
  199. data/examples/common.rb +0 -180
  200. data/examples/prompts/assistant.md +0 -5
  201. data/examples/prompts/audit_trail.md +0 -8
  202. data/examples/prompts/billing.md +0 -10
  203. data/examples/prompts/classifier.md +0 -7
  204. data/examples/prompts/comedian.md +0 -6
  205. data/examples/prompts/comedy_critic.md +0 -10
  206. data/examples/prompts/configurable.md +0 -9
  207. data/examples/prompts/dispatcher.md +0 -12
  208. data/examples/prompts/entity_extractor.md +0 -14
  209. data/examples/prompts/escalation.md +0 -42
  210. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  211. data/examples/prompts/frontmatter_named_test.md +0 -5
  212. data/examples/prompts/frontmatter_tools_test.md +0 -6
  213. data/examples/prompts/general.md +0 -7
  214. data/examples/prompts/github_assistant.md +0 -14
  215. data/examples/prompts/helper.md +0 -4
  216. data/examples/prompts/incident_responder.md +0 -18
  217. data/examples/prompts/keyword_extractor.md +0 -11
  218. data/examples/prompts/llm_config_demo.md +0 -20
  219. data/examples/prompts/order_support.md +0 -35
  220. data/examples/prompts/os_advocate.md +0 -13
  221. data/examples/prompts/os_chief.md +0 -13
  222. data/examples/prompts/os_editor.md +0 -13
  223. data/examples/prompts/parameterized_main_test.md +0 -6
  224. data/examples/prompts/pii_redactor.md +0 -7
  225. data/examples/prompts/product_support.md +0 -37
  226. data/examples/prompts/runbook_protocol.md +0 -12
  227. data/examples/prompts/sentiment_analyzer.md +0 -12
  228. data/examples/prompts/skill_a_test.md +0 -4
  229. data/examples/prompts/skill_b_test.md +0 -4
  230. data/examples/prompts/skill_config_test.md +0 -5
  231. data/examples/prompts/skill_cycle_a_test.md +0 -6
  232. data/examples/prompts/skill_cycle_b_test.md +0 -6
  233. data/examples/prompts/skill_description_test.md +0 -4
  234. data/examples/prompts/skill_leaf_test.md +0 -4
  235. data/examples/prompts/skill_nested_test.md +0 -6
  236. data/examples/prompts/skill_refs_main_test.md +0 -6
  237. data/examples/prompts/skill_self_ref_test.md +0 -6
  238. data/examples/prompts/skill_with_mcp_test.md +0 -9
  239. data/examples/prompts/skill_with_params_test.md +0 -6
  240. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  241. data/examples/prompts/skill_with_tools_test.md +0 -6
  242. data/examples/prompts/sre_compliance.md +0 -10
  243. data/examples/prompts/structured_output.md +0 -7
  244. data/examples/prompts/synthesizer.md +0 -17
  245. data/examples/prompts/technical.md +0 -10
  246. data/examples/prompts/template_with_skills_test.md +0 -6
  247. data/examples/prompts/triage.md +0 -22
  248. data/examples/ruboruby.md +0 -423
  249. data/examples/run_all.rb +0 -60
  250. data/examples/xyzzy.rb +0 -97
  251. data/logfile +0 -8
  252. data/mkdocs.yml +0 -209
  253. data/robot_concurrency.md +0 -38
  254. data/simple_acp_review.md +0 -298
  255. data/temp.md +0 -6
  256. 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