robot_lab 0.3.0 → 0.5.0

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