robot_lab 0.2.8 → 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 (262) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +16 -18
  4. data/lib/robot_lab/ask_user.rb +3 -3
  5. data/lib/robot_lab/config/defaults.yml +5 -5
  6. data/lib/robot_lab/config.rb +2 -2
  7. data/lib/robot_lab/mcp/transports/streamable_http.rb +2 -4
  8. data/lib/robot_lab/memory.rb +16 -7
  9. data/lib/robot_lab/robot/hooking.rb +26 -0
  10. data/lib/robot_lab/robot/result_building.rb +119 -0
  11. data/lib/robot_lab/robot.rb +37 -120
  12. data/lib/robot_lab/run_config.rb +52 -20
  13. data/lib/robot_lab/tool.rb +7 -12
  14. data/lib/robot_lab/version.rb +1 -1
  15. data/lib/robot_lab.rb +3 -3
  16. metadata +15 -274
  17. data/.architecture/AGENTS.md +0 -32
  18. data/.architecture/config.yml +0 -8
  19. data/.architecture/members.yml +0 -60
  20. data/.architecture/reviews/feature-free-will.md +0 -490
  21. data/.architecture/reviews/overall-codebase.md +0 -427
  22. data/.claude/settings.local.json +0 -57
  23. data/.codex/config.toml +0 -2
  24. data/.envrc +0 -6
  25. data/.github/workflows/deploy-github-pages.yml +0 -52
  26. data/.irbrc +0 -6
  27. data/.loki +0 -65
  28. data/.quality/reek_baseline.txt +0 -43
  29. data/.rubocop.yml +0 -10
  30. data/Archspec.rb +0 -44
  31. data/CLAUDE.md +0 -139
  32. data/COMMITS.md +0 -196
  33. data/Rakefile +0 -193
  34. data/_typos.toml +0 -21
  35. data/agent2agent_review.md +0 -192
  36. data/agentf_improvements.md +0 -253
  37. data/agents.md +0 -14
  38. data/docs/api/core/index.md +0 -147
  39. data/docs/api/core/memory.md +0 -578
  40. data/docs/api/core/network.md +0 -553
  41. data/docs/api/core/result.md +0 -211
  42. data/docs/api/core/robot.md +0 -1667
  43. data/docs/api/core/state.md +0 -145
  44. data/docs/api/core/tool.md +0 -520
  45. data/docs/api/errors.md +0 -323
  46. data/docs/api/hooks.md +0 -469
  47. data/docs/api/index.md +0 -137
  48. data/docs/api/mcp/client.md +0 -387
  49. data/docs/api/mcp/index.md +0 -267
  50. data/docs/api/mcp/server.md +0 -252
  51. data/docs/api/mcp/transports.md +0 -339
  52. data/docs/api/messages/index.md +0 -126
  53. data/docs/api/messages/text-message.md +0 -170
  54. data/docs/api/messages/tool-call-message.md +0 -206
  55. data/docs/api/messages/tool-result-message.md +0 -242
  56. data/docs/api/messages/user-message.md +0 -232
  57. data/docs/api/skills.md +0 -423
  58. data/docs/api/streaming/context.md +0 -280
  59. data/docs/api/streaming/events.md +0 -185
  60. data/docs/api/streaming/index.md +0 -156
  61. data/docs/api/support.md +0 -846
  62. data/docs/architecture/core-concepts.md +0 -575
  63. data/docs/architecture/index.md +0 -252
  64. data/docs/architecture/message-flow.md +0 -366
  65. data/docs/architecture/network-orchestration.md +0 -478
  66. data/docs/architecture/robot-execution.md +0 -396
  67. data/docs/architecture/state-management.md +0 -410
  68. data/docs/assets/css/custom.css +0 -56
  69. data/docs/assets/images/robot_lab.jpg +0 -0
  70. data/docs/concepts.md +0 -641
  71. data/docs/examples/basic-chat.md +0 -258
  72. data/docs/examples/index.md +0 -425
  73. data/docs/examples/mcp-server.md +0 -403
  74. data/docs/examples/multi-robot-network.md +0 -476
  75. data/docs/examples/tool-usage.md +0 -468
  76. data/docs/getting-started/configuration.md +0 -645
  77. data/docs/getting-started/index.md +0 -56
  78. data/docs/getting-started/installation.md +0 -200
  79. data/docs/getting-started/quick-start.md +0 -293
  80. data/docs/guides/building-robots.md +0 -1187
  81. data/docs/guides/creating-networks.md +0 -736
  82. data/docs/guides/hooks.md +0 -1206
  83. data/docs/guides/index.md +0 -78
  84. data/docs/guides/knowledge.md +0 -219
  85. data/docs/guides/mcp-integration.md +0 -740
  86. data/docs/guides/memory.md +0 -458
  87. data/docs/guides/observability.md +0 -733
  88. data/docs/guides/streaming.md +0 -323
  89. data/docs/guides/using-tools.md +0 -780
  90. data/docs/index.md +0 -280
  91. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  92. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  93. data/examples/.envrc +0 -1
  94. data/examples/01_simple_robot.rb +0 -31
  95. data/examples/02_tools.rb +0 -109
  96. data/examples/03_network.rb +0 -118
  97. data/examples/04_mcp.rb +0 -215
  98. data/examples/05_streaming.rb +0 -137
  99. data/examples/06_prompt_templates.rb +0 -325
  100. data/examples/07_network_memory.rb +0 -313
  101. data/examples/08_llm_config.rb +0 -162
  102. data/examples/09_chaining.rb +0 -251
  103. data/examples/10_memory.rb +0 -315
  104. data/examples/11_network_introspection.rb +0 -239
  105. data/examples/12_message_bus.rb +0 -67
  106. data/examples/13_spawn.rb +0 -85
  107. data/examples/14_rusty_circuit/.envrc +0 -1
  108. data/examples/14_rusty_circuit/.gitignore +0 -1
  109. data/examples/14_rusty_circuit/README.md +0 -1
  110. data/examples/14_rusty_circuit/comic.rb +0 -149
  111. data/examples/14_rusty_circuit/display.rb +0 -217
  112. data/examples/14_rusty_circuit/heckler.rb +0 -65
  113. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  114. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  115. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  116. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  117. data/examples/14_rusty_circuit/scout.rb +0 -165
  118. data/examples/14_rusty_circuit/show.log +0 -234
  119. data/examples/15_memory_network_and_bus/.envrc +0 -1
  120. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  121. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  122. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  123. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  124. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  125. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -13
  126. data/examples/15_memory_network_and_bus/output/final_article.md +0 -13
  127. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  128. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  129. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  130. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -18
  131. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -13
  132. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  133. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  134. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  135. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  136. data/examples/16_writers_room/.envrc +0 -1
  137. data/examples/16_writers_room/display.rb +0 -158
  138. data/examples/16_writers_room/output/.gitignore +0 -4
  139. data/examples/16_writers_room/output/README.md +0 -69
  140. data/examples/16_writers_room/output/opus_001.md +0 -263
  141. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  142. data/examples/16_writers_room/output/opus_002.md +0 -245
  143. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  144. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  145. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  146. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  147. data/examples/16_writers_room/prompts/writer.md +0 -37
  148. data/examples/16_writers_room/room.rb +0 -186
  149. data/examples/16_writers_room/tools.rb +0 -173
  150. data/examples/16_writers_room/writer.rb +0 -121
  151. data/examples/16_writers_room/writers_room.rb +0 -256
  152. data/examples/17_skills.rb +0 -234
  153. data/examples/18_rails/.envrc +0 -3
  154. data/examples/18_rails/.gitignore +0 -5
  155. data/examples/18_rails/Gemfile +0 -11
  156. data/examples/18_rails/README.md +0 -67
  157. data/examples/18_rails/Rakefile +0 -4
  158. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  159. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  160. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  161. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  162. data/examples/18_rails/app/models/application_record.rb +0 -5
  163. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  164. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  165. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  166. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  167. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  168. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  169. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  170. data/examples/18_rails/bin/dev +0 -7
  171. data/examples/18_rails/bin/rails +0 -6
  172. data/examples/18_rails/bin/setup +0 -15
  173. data/examples/18_rails/config/application.rb +0 -33
  174. data/examples/18_rails/config/cable.yml +0 -2
  175. data/examples/18_rails/config/database.yml +0 -5
  176. data/examples/18_rails/config/environment.rb +0 -4
  177. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  178. data/examples/18_rails/config/routes.rb +0 -6
  179. data/examples/18_rails/config.ru +0 -4
  180. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  181. data/examples/19_token_tracking.rb +0 -138
  182. data/examples/20_circuit_breaker.rb +0 -147
  183. data/examples/21_learning_loop.rb +0 -181
  184. data/examples/22_context_compression.rb +0 -172
  185. data/examples/23_convergence.rb +0 -133
  186. data/examples/24_structured_delegation.rb +0 -151
  187. data/examples/25_history_search/conversation.jsonl +0 -30
  188. data/examples/25_history_search.rb +0 -133
  189. data/examples/26_document_store/api_versioning_adr.md +0 -52
  190. data/examples/26_document_store/incident_postmortem.md +0 -46
  191. data/examples/26_document_store/postgres_runbook.md +0 -49
  192. data/examples/26_document_store/redis_caching_guide.md +0 -48
  193. data/examples/26_document_store/sidekiq_guide.md +0 -51
  194. data/examples/26_document_store.rb +0 -140
  195. data/examples/27_incident_response/incident_response.rb +0 -261
  196. data/examples/28_mcp_discovery.rb +0 -113
  197. data/examples/29_ractor_tools.rb +0 -240
  198. data/examples/30_ractor_network.rb +0 -252
  199. data/examples/31_launch_assessment.rb +0 -246
  200. data/examples/32_newsletter_reader.rb +0 -193
  201. data/examples/33_stock_generator.rb +0 -80
  202. data/examples/33_stock_predictor.rb +0 -327
  203. data/examples/34_agentskills.rb +0 -76
  204. data/examples/35_hooks.rb +0 -266
  205. data/examples/README.md +0 -533
  206. data/examples/common.rb +0 -148
  207. data/examples/prompts/assistant.md +0 -5
  208. data/examples/prompts/audit_trail.md +0 -8
  209. data/examples/prompts/billing.md +0 -10
  210. data/examples/prompts/classifier.md +0 -7
  211. data/examples/prompts/comedian.md +0 -6
  212. data/examples/prompts/comedy_critic.md +0 -10
  213. data/examples/prompts/configurable.md +0 -9
  214. data/examples/prompts/dispatcher.md +0 -12
  215. data/examples/prompts/entity_extractor.md +0 -14
  216. data/examples/prompts/escalation.md +0 -42
  217. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  218. data/examples/prompts/frontmatter_named_test.md +0 -5
  219. data/examples/prompts/frontmatter_tools_test.md +0 -6
  220. data/examples/prompts/general.md +0 -7
  221. data/examples/prompts/github_assistant.md +0 -14
  222. data/examples/prompts/helper.md +0 -4
  223. data/examples/prompts/incident_responder.md +0 -18
  224. data/examples/prompts/keyword_extractor.md +0 -11
  225. data/examples/prompts/llm_config_demo.md +0 -20
  226. data/examples/prompts/order_support.md +0 -35
  227. data/examples/prompts/os_advocate.md +0 -13
  228. data/examples/prompts/os_chief.md +0 -13
  229. data/examples/prompts/os_editor.md +0 -13
  230. data/examples/prompts/parameterized_main_test.md +0 -6
  231. data/examples/prompts/pii_redactor.md +0 -7
  232. data/examples/prompts/product_support.md +0 -37
  233. data/examples/prompts/runbook_protocol.md +0 -12
  234. data/examples/prompts/sentiment_analyzer.md +0 -12
  235. data/examples/prompts/skill_a_test.md +0 -4
  236. data/examples/prompts/skill_b_test.md +0 -4
  237. data/examples/prompts/skill_config_test.md +0 -5
  238. data/examples/prompts/skill_cycle_a_test.md +0 -6
  239. data/examples/prompts/skill_cycle_b_test.md +0 -6
  240. data/examples/prompts/skill_description_test.md +0 -4
  241. data/examples/prompts/skill_leaf_test.md +0 -4
  242. data/examples/prompts/skill_nested_test.md +0 -6
  243. data/examples/prompts/skill_refs_main_test.md +0 -6
  244. data/examples/prompts/skill_self_ref_test.md +0 -6
  245. data/examples/prompts/skill_with_mcp_test.md +0 -9
  246. data/examples/prompts/skill_with_params_test.md +0 -6
  247. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  248. data/examples/prompts/skill_with_tools_test.md +0 -6
  249. data/examples/prompts/sre_compliance.md +0 -10
  250. data/examples/prompts/structured_output.md +0 -7
  251. data/examples/prompts/synthesizer.md +0 -17
  252. data/examples/prompts/technical.md +0 -10
  253. data/examples/prompts/template_with_skills_test.md +0 -6
  254. data/examples/prompts/triage.md +0 -22
  255. data/examples/ruboruby.md +0 -423
  256. data/examples/xyzzy.rb +0 -97
  257. data/logfile +0 -8
  258. data/mkdocs.yml +0 -209
  259. data/robot_concurrency.md +0 -38
  260. data/simple_acp_review.md +0 -298
  261. data/temp.md +0 -6
  262. data/tool_manifest_plan.md +0 -155
@@ -1,387 +0,0 @@
1
- # MCP Client
2
-
3
- Connects to MCP servers, discovers tools, and invokes them via the Model Context Protocol.
4
-
5
- ## Class: `RobotLab::MCP::Client`
6
-
7
- ```ruby
8
- client = RobotLab::MCP::Client.new(
9
- {
10
- name: "filesystem",
11
- transport: { type: "stdio", command: "mcp-server-filesystem", args: ["--root", "/data"] }
12
- }
13
- )
14
-
15
- client.connect
16
- tools = client.list_tools
17
- result = client.call_tool("readFile", { path: "/data/readme.txt" })
18
- client.disconnect
19
- ```
20
-
21
- ## Constructor
22
-
23
- ```ruby
24
- Client.new(server_or_config, poller: nil)
25
- ```
26
-
27
- `server_or_config` is **positional**. Passing the server keys directly —
28
- `Client.new(name: "fs", transport: {...})` — raises
29
- `ArgumentError: wrong number of arguments (given 0, expected 1)`. Wrap the
30
- configuration in braces.
31
-
32
- Accepts either a `Server` instance or a Hash configuration. When a Hash is provided, it is used to construct a `Server` internally.
33
-
34
- **Parameters:**
35
-
36
- | Name | Type | Default | Description |
37
- |------|------|---------|-------------|
38
- | `server_or_config` | `Server`, `Hash` | **required, positional** | Server instance or configuration hash |
39
- | `poller` | `MCP::ConnectionPoller`, `nil` | `nil` | Shared `IO.select` poller for multiplexing stdio transports (see [ConnectionPoller](#connectionpoller)) |
40
-
41
- **Hash Configuration Keys:**
42
-
43
- | Key | Type | Required | Description |
44
- |-----|------|----------|-------------|
45
- | `name` | `String` | Yes | Server identifier |
46
- | `transport` | `Hash` | Yes | Transport configuration (must include `type`) |
47
- | `timeout` | `Numeric` | No | Request timeout in seconds (default: 15). Propagated to the transport layer |
48
- | `description` | `String` | No | Human-readable summary used by `MCP::ServerDiscovery` |
49
-
50
- **Raises:** `ArgumentError` if the config is neither a `Server` nor a `Hash`.
51
-
52
- ## Attributes
53
-
54
- ### server
55
-
56
- ```ruby
57
- client.server # => RobotLab::MCP::Server
58
- ```
59
-
60
- The MCP server configuration object.
61
-
62
- ### transport
63
-
64
- ```ruby
65
- client.transport # => RobotLab::MCP::Transports::Base subclass | nil
66
- ```
67
-
68
- The transport instance created by `connect`. `nil` before the first successful
69
- `connect` and again after `disconnect`.
70
-
71
- ### connected
72
-
73
- ```ruby
74
- client.connected # => Boolean
75
- client.connected? # => Boolean
76
- ```
77
-
78
- Whether the client is currently connected to the server. `connected` is a plain
79
- reader over the same ivar that `connected?` returns; both are public.
80
-
81
- ## Methods
82
-
83
- ### connect
84
-
85
- ```ruby
86
- client.connect # => self
87
- ```
88
-
89
- Establish a connection to the MCP server. Creates the appropriate transport based on the server's transport type, then connects. If already connected, returns immediately. When a `poller:` was supplied, the client registers itself with the poller after the transport connects.
90
-
91
- Connection failures are logged as warnings and the client remains in a disconnected state (does not raise). Always check `connected?` afterwards.
92
-
93
- ### disconnect
94
-
95
- ```ruby
96
- client.disconnect # => self
97
- ```
98
-
99
- Close the connection to the MCP server. Unregisters from the poller (if any), closes the underlying transport, and resets `transport` to `nil`. If not connected, returns immediately.
100
-
101
- ### list_tools
102
-
103
- ```ruby
104
- client.list_tools # => Array<Hash>
105
- ```
106
-
107
- Discover available tools from the server. Returns an array of tool definition hashes.
108
-
109
- **Raises:** `MCPError` if not connected.
110
-
111
- ### call_tool
112
-
113
- ```ruby
114
- result = client.call_tool(name, arguments = {})
115
- ```
116
-
117
- Execute a tool on the server.
118
-
119
- **Parameters:**
120
-
121
- | Name | Type | Description |
122
- |------|------|-------------|
123
- | `name` | `String` | Tool name |
124
- | `arguments` | `Hash` | Tool arguments (default: `{}`) |
125
-
126
- **Returns:** Tool result content (from the `content` field of the response).
127
-
128
- **Raises:** `MCPError` if not connected.
129
-
130
- ### list_resources
131
-
132
- ```ruby
133
- client.list_resources # => Array<Hash>
134
- ```
135
-
136
- List available resources from the server.
137
-
138
- **Raises:** `MCPError` if not connected.
139
-
140
- ### read_resource
141
-
142
- ```ruby
143
- client.read_resource(uri) # => Object
144
- ```
145
-
146
- Read a resource by URI.
147
-
148
- **Parameters:**
149
-
150
- | Name | Type | Description |
151
- |------|------|-------------|
152
- | `uri` | `String` | Resource URI |
153
-
154
- **Raises:** `MCPError` if not connected.
155
-
156
- ### list_prompts
157
-
158
- ```ruby
159
- client.list_prompts # => Array<Hash>
160
- ```
161
-
162
- List available prompts from the server.
163
-
164
- **Raises:** `MCPError` if not connected.
165
-
166
- ### get_prompt
167
-
168
- ```ruby
169
- client.get_prompt(name, arguments = {}) # => Hash
170
- ```
171
-
172
- Get a prompt by name with optional arguments.
173
-
174
- **Parameters:**
175
-
176
- | Name | Type | Description |
177
- |------|------|-------------|
178
- | `name` | `String` | Prompt name |
179
- | `arguments` | `Hash` | Prompt arguments (default: `{}`) |
180
-
181
- **Raises:** `MCPError` if not connected.
182
-
183
- ### to_h
184
-
185
- ```ruby
186
- client.to_h # => Hash
187
- ```
188
-
189
- Converts the client to a hash representation containing server config and connection status:
190
-
191
- ```ruby
192
- { server: { name: "...", description: "...", transport: { ... }, timeout: 15 },
193
- connected: false }
194
- ```
195
-
196
- ## ConnectionPoller
197
-
198
- **Class:** `RobotLab::MCP::ConnectionPoller`
199
-
200
- By default each stdio client blocks on its own `@stdout.gets` inside a
201
- `Timeout.timeout`. `ConnectionPoller` replaces that with a single background
202
- thread running one `IO.select` across every registered stdio transport,
203
- dispatching each JSON-RPC response to the client that is waiting for it. This is
204
- useful when one robot talks to several local MCP servers.
205
-
206
- It is **opt-in**: nothing in `Robot` or `Network` creates one. `Robot`'s internal
207
- MCP setup calls `MCP::Client.new(server_config)` with no poller. You get a poller
208
- only by wiring it yourself.
209
-
210
- Async-based transports (SSE, WebSocket, StreamableHTTP) are unaffected — the
211
- poller silently ignores any client whose transport is not a live `Stdio`.
212
-
213
- ```ruby
214
- poller = RobotLab::MCP::ConnectionPoller.new.start
215
-
216
- client = RobotLab::MCP::Client.new(
217
- { name: "fs", transport: { type: "stdio", command: "mcp-server-fs" } },
218
- poller: poller
219
- )
220
-
221
- client.connect # registers the transport's stdout with the poller
222
- client.call_tool("readFile", { path: "/data/readme.txt" })
223
- client.disconnect # unregisters
224
-
225
- poller.stop
226
- ```
227
-
228
- ### Methods
229
-
230
- | Method | Description |
231
- |--------|-------------|
232
- | `start` | Start the polling thread (named `RobotLab::MCP::ConnectionPoller`). Idempotent; returns `self` |
233
- | `stop(timeout: 5)` | Stop the thread, cancelling every pending request with an `MCPError`. Waits up to `timeout` seconds for the thread to join. Returns `self` |
234
- | `register(client)` | Register a client. Non-stdio (or not-yet-connected) clients are silently ignored |
235
- | `unregister(client)` | Remove a client's IO from the select set |
236
- | `send_request(client, message, timeout:)` | Write the JSON-RPC message to the client's stdin and block until the poll loop dispatches the response. Raises `MCPError` on timeout or a broken pipe |
237
- | `running?` | Whether the polling thread is running |
238
-
239
- `POLL_INTERVAL` is `0.1` seconds — the `IO.select` timeout, and the sleep used
240
- when no clients are registered.
241
-
242
- `Client#request` routes through `poller.send_request` only when a poller is
243
- present **and** the transport is a `Transports::Stdio`; otherwise it calls
244
- `transport.send_request` directly.
245
-
246
- ## Transport Configuration
247
-
248
- The transport type is determined by the `type` key in the transport hash of the `Server` configuration.
249
-
250
- ### Stdio
251
-
252
- ```ruby
253
- client = RobotLab::MCP::Client.new(
254
- {
255
- name: "local",
256
- transport: {
257
- type: "stdio",
258
- command: "npx",
259
- args: ["@modelcontextprotocol/server-filesystem", "/path"]
260
- }
261
- }
262
- )
263
- ```
264
-
265
- ### WebSocket
266
-
267
- ```ruby
268
- client = RobotLab::MCP::Client.new(
269
- {
270
- name: "remote",
271
- transport: {
272
- type: "ws",
273
- url: "wss://mcp.example.com/ws"
274
- }
275
- }
276
- )
277
- ```
278
-
279
- ### SSE
280
-
281
- ```ruby
282
- client = RobotLab::MCP::Client.new(
283
- {
284
- name: "streaming",
285
- transport: {
286
- type: "sse",
287
- url: "https://mcp.example.com/sse"
288
- }
289
- }
290
- )
291
- ```
292
-
293
- ### Streamable HTTP
294
-
295
- ```ruby
296
- client = RobotLab::MCP::Client.new(
297
- {
298
- name: "http",
299
- transport: {
300
- type: "streamable-http",
301
- url: "https://mcp.example.com/mcp",
302
- session_id: "optional-session-id"
303
- }
304
- }
305
- )
306
- ```
307
-
308
- ## Examples
309
-
310
- ### Basic Usage
311
-
312
- ```ruby
313
- client = RobotLab::MCP::Client.new(
314
- { name: "github", transport: { type: "stdio", command: "mcp-server-github" } }
315
- )
316
-
317
- client.connect
318
- raise "could not connect" unless client.connected?
319
-
320
- # List available tools
321
- tools = client.list_tools
322
- tools.each { |t| puts "#{t[:name]}: #{t[:description]}" }
323
-
324
- # Call a tool
325
- result = client.call_tool("search_repositories", { query: "ruby mcp" })
326
- puts result
327
-
328
- client.disconnect
329
- ```
330
-
331
- ### From a Server Object
332
-
333
- ```ruby
334
- server = RobotLab::MCP::Server.new(
335
- name: "neon",
336
- transport: { type: "ws", url: "ws://localhost:8080" }
337
- )
338
-
339
- client = RobotLab::MCP::Client.new(server)
340
- client.connect
341
- ```
342
-
343
- ### In a Robot
344
-
345
- ```ruby
346
- robot = Robot.new(
347
- name: "assistant",
348
- system_prompt: "You help with file operations.",
349
- mcp: [
350
- { name: "fs", transport: { type: "stdio", command: "mcp-fs" } }
351
- ]
352
- )
353
-
354
- # `Robot#run` defaults to `mcp: :none, tools: :none`. Opt in on every run that
355
- # should reach the MCP servers: `mcp: :inherit` connects them, `tools: :inherit`
356
- # forwards the discovered tools to the model.
357
- result = robot.run("Read the contents of /data/config.yml", mcp: :inherit, tools: :inherit)
358
- puts result.last_text_content
359
- ```
360
-
361
- MCP connection failures inside a robot are logged and recorded in
362
- `robot.failed_mcp_server_names`; they never raise out of `run`.
363
-
364
- ### Error Handling
365
-
366
- `connect` swallows its own failures, so test `connected?` rather than rescuing
367
- around it:
368
-
369
- ```ruby
370
- client.connect
371
-
372
- begin
373
- raise RobotLab::MCPError, "not connected" unless client.connected?
374
-
375
- result = client.call_tool("unknown_tool", {})
376
- rescue RobotLab::MCPError => e
377
- puts "MCP error: #{e.message}"
378
- ensure
379
- client.disconnect
380
- end
381
- ```
382
-
383
- ## See Also
384
-
385
- - [MCP Overview](index.md)
386
- - [Server Configuration](server.md)
387
- - [Transports](transports.md)
@@ -1,267 +0,0 @@
1
- # MCP (Model Context Protocol)
2
-
3
- Integration with MCP servers for extended tool capabilities.
4
-
5
- ## Overview
6
-
7
- MCP allows robots to connect to external tool servers, extending their capabilities without modifying robot code. RobotLab provides an MCP client that communicates with MCP-compliant servers over multiple transport types.
8
-
9
- ```ruby
10
- robot = Robot.new(
11
- name: "developer",
12
- system_prompt: "You help with coding tasks.",
13
- mcp: [
14
- {
15
- name: "filesystem",
16
- transport: {
17
- type: "stdio",
18
- command: "npx",
19
- args: ["@modelcontextprotocol/server-filesystem", "/data"]
20
- }
21
- }
22
- ]
23
- )
24
-
25
- # MCP is opt-in per run — see "Connecting at Run Time" below.
26
- result = robot.run("What files are in /data?", mcp: :inherit, tools: :inherit)
27
- ```
28
-
29
- ## Components
30
-
31
- | Component | Description |
32
- |-----------|-------------|
33
- | [Client](client.md) | Connects to MCP servers, lists tools, calls tools |
34
- | [Server](server.md) | Server configuration data structure |
35
- | [Transports](transports.md) | Communication methods (stdio, WebSocket, SSE, HTTP) |
36
- | [ConnectionPoller](client.md#connectionpoller) | Optional shared `IO.select` loop multiplexing several stdio transports |
37
- | `MCP::ServerDiscovery` | Picks the relevant subset of configured servers for a message (see [Server Discovery](#server-discovery)) |
38
-
39
- ## Quick Start
40
-
41
- ### Using MCP with a Robot
42
-
43
- Pass MCP server configurations via the `mcp:` parameter when creating a robot:
44
-
45
- ```ruby
46
- robot = Robot.new(
47
- name: "assistant",
48
- template: :assistant,
49
- mcp: [
50
- { name: "github", transport: { type: "stdio", command: "mcp-server-github" } }
51
- ]
52
- )
53
-
54
- result = robot.run("List my open pull requests", mcp: :inherit, tools: :inherit)
55
- result.last_text_content
56
- ```
57
-
58
- ### Connecting at Run Time
59
-
60
- `Robot#run` defaults to `mcp: :none, tools: :none`. Those defaults mean "connect
61
- nothing and send zero tools for this turn", so a bare `robot.run("...")` reaches
62
- the LLM with **no** MCP servers connected and **no** tools attached, even when
63
- `mcp:` was supplied at build time.
64
-
65
- ```ruby
66
- robot.run("...") # no MCP, no tools
67
- robot.run("...", tools: :inherit) # attached local tools only
68
- robot.run("...", mcp: :inherit, tools: :inherit) # connect MCP servers AND send their tools
69
- ```
70
-
71
- `mcp: :inherit` triggers the connection attempt; `tools: :inherit` is what
72
- actually forwards the discovered MCP tools to the model. Both are needed.
73
-
74
- !!! warning "Build-time `:inherit` depends on whether there is a parent"
75
- `resolve_mcp_hierarchy` does not freeze the parent at construction — it
76
- recomputes it on **every run** as
77
- `network_config&.mcp || network_parent_config(network)&.mcp || RobotLab.config.mcp`,
78
- then resolves the build-time value against it, then the runtime value against
79
- *that*.
80
-
81
- - For a **standalone** robot the parent is the global default `:none`, so a
82
- build-time `mcp: :inherit` / `tools: :inherit` collapses to an allowlist
83
- that matches nothing. Give the standalone robot an explicit array at build
84
- time and pass `:inherit` at run time.
85
- - **Inside a network** whose `config:` sets `mcp:`/`tools:`, build-time
86
- `:inherit` is exactly how a robot opts in to the network's list — see
87
- [MCP in Networks](#mcp-in-networks) below.
88
-
89
- `robot.connect_mcp!` connects eagerly, but a later plain `run()` still sends no
90
- tools. Connection failures are logged and recorded in
91
- `robot.failed_mcp_server_names` — they are not raised.
92
-
93
- ### MCP in Networks
94
-
95
- A robot can inherit its MCP server list from the network's `config:`. This is one
96
- of only two fields (`mcp` and `tools`) that a network-level `RunConfig`
97
- propagates to member robots, and only when the robot opts in with `:inherit`:
98
-
99
- ```ruby
100
- network = RobotLab.create_network(
101
- name: "dev",
102
- config: RobotLab::RunConfig.new(
103
- mcp: [{ name: "github", transport: { type: "stdio", command: "mcp-server-github" } }]
104
- )
105
- ) do |n|
106
- n.task :assistant,
107
- Robot.new(name: "assistant", template: :assistant, mcp: :inherit),
108
- mcp: :inherit, tools: :inherit
109
- end
110
- ```
111
-
112
- Here the build-time `mcp: :inherit` on the robot is correct and necessary: the
113
- parent is resolved at run time from the network's `config:`, so `:inherit` picks
114
- up the network's server list rather than the global `:none`.
115
-
116
- `Network#task` has the same `mcp: :none, tools: :none` defaults as `Robot#run`,
117
- so the task must opt in as well — otherwise the inherited list collapses to `[]`.
118
-
119
- LLM fields (`model`, `temperature`, ...) and callbacks are **not** inherited from
120
- a network config; each robot reads those from its own configuration.
121
-
122
- ### Direct Client Usage
123
-
124
- `Client.new` takes the server (or config hash) as a **positional** argument:
125
-
126
- ```ruby
127
- client = RobotLab::MCP::Client.new(
128
- name: "filesystem",
129
- transport: { type: "stdio", command: "mcp-server-filesystem", args: ["--root", "/data"] }
130
- )
131
- # ArgumentError: wrong number of arguments (given 0, expected 1)
132
-
133
- client = RobotLab::MCP::Client.new(
134
- {
135
- name: "filesystem",
136
- transport: { type: "stdio", command: "mcp-server-filesystem", args: ["--root", "/data"] }
137
- }
138
- )
139
-
140
- client.connect
141
- tools = client.list_tools
142
- result = client.call_tool("readFile", { path: "/data/config.yml" })
143
- client.disconnect
144
- ```
145
-
146
- ### Server Discovery
147
-
148
- When a robot is built with `mcp_discovery: true`, the configured server list is
149
- filtered before connecting: `MCP::ServerDiscovery` scores each server's
150
- `name + description` against the user's message using term-frequency cosine
151
- similarity and connects only the servers scoring at or above the threshold
152
- (`DEFAULT_THRESHOLD` = 0.05).
153
-
154
- ```ruby
155
- robot = RobotLab.build(
156
- name: "assistant",
157
- mcp_discovery: true,
158
- mcp: [
159
- { name: "filesystem", description: "Read, write, and search local files",
160
- transport: { type: "stdio", command: "mcp-server-fs" } },
161
- { name: "brew", description: "Install and manage macOS packages via Homebrew",
162
- transport: { type: "stdio", command: "mcp-server-brew" } }
163
- ]
164
- )
165
-
166
- # Connects only the "brew" server for this message
167
- robot.run("install imagemagick", mcp: :inherit, tools: :inherit)
168
- ```
169
-
170
- Discovery falls back to the **full** server list when no server has a
171
- `description`, when the query is blank, when no server clears the threshold, or
172
- when the optional `classifier` gem is unavailable.
173
-
174
- ## Transport Types
175
-
176
- | Type | Config Key | Use Case |
177
- |------|------------|----------|
178
- | `stdio` | `"stdio"` | Local command/subprocess execution |
179
- | `websocket` | `"ws"` or `"websocket"` | Real-time bidirectional communication |
180
- | `sse` | `"sse"` | Server-sent events streaming |
181
- | `streamable-http` | `"streamable-http"` or `"http"` | HTTP request/response with session support |
182
-
183
- ## MCP Parameter Values
184
-
185
- The `mcp:` parameter on a Robot accepts three types of values:
186
-
187
- | Value | Meaning |
188
- |-------|---------|
189
- | `:none` | No MCP servers (explicitly disabled) |
190
- | `:inherit` | Resolve against the parent level (network config, then global config) |
191
- | `Array<Hash>` | Explicit list of server configurations |
192
-
193
- Each server configuration hash is passed straight to `MCP::Server.new`:
194
-
195
- | Key | Type | Required | Description |
196
- |-----|------|----------|-------------|
197
- | `name` | `String` | Yes | Unique server identifier |
198
- | `transport` | `Hash` | Yes | Transport configuration (must include `type`) |
199
- | `timeout` | `Numeric` | No | Request timeout in seconds (default: 15) |
200
- | `description` | `String` | No | Human-readable summary; the text `ServerDiscovery` scores against |
201
-
202
- `transport:` must be a **nested hash**. A flat `transport: "stdio"` with sibling
203
- `command:`/`args:` keys raises internally (`NoMethodError: undefined method
204
- 'transform_keys' for an instance of String`). `init_mcp_client` rescues it rather
205
- than re-raising, so the robot still builds — but with zero tools from that
206
- server. It is not silent: the failure is logged at `warn` through
207
- `RobotLab.config.logger` (`"Robot '<name>' error connecting to MCP server
208
- '<server>': ..."`) and the server is recorded in
209
- `robot.failed_mcp_server_names`. The same holds for an invalid transport type
210
- reached through the robot path.
211
-
212
- `MCP::Server#initialize` ends in `**_extra`, so any other key you add is
213
- accepted and silently discarded — a typo in `timeout` or `description` will not
214
- raise.
215
-
216
- ## Error Handling
217
-
218
- `RobotLab::MCPError` is raised when a request is made without an active
219
- connection, and by the transports for protocol/I-O failures.
220
-
221
- **`Client#connect` does not raise.** It rescues every `StandardError` — including
222
- a failed transport handshake and an unsupported transport type — logs a warning,
223
- and leaves the client disconnected. Check `client.connected?` after connecting:
224
-
225
- ```ruby
226
- client.connect
227
-
228
- unless client.connected?
229
- warn "MCP server unavailable"
230
- return
231
- end
232
-
233
- begin
234
- client.call_tool("unknown_tool", {})
235
- rescue RobotLab::MCPError => e
236
- puts "MCP error: #{e.message}"
237
- end
238
- ```
239
-
240
- An invalid transport type or a missing `command`/`url` raises `ArgumentError`
241
- from `MCP::Server.new` — and because `MCP::Client.new` builds the server, from
242
- `Client.new` too:
243
-
244
- ```ruby
245
- RobotLab::MCP::Server.new(name: "x", transport: { type: "bogus", command: "z" })
246
- # ArgumentError: Invalid transport type: bogus. Must be one of:
247
- # stdio, sse, ws, websocket, streamable-http, http
248
- ```
249
-
250
- That `ArgumentError` only reaches you when you construct the server or client
251
- **directly**. Through the robot path it is rescued like any other connect
252
- failure — `init_mcp_client` logs it at `warn` and adds the server to
253
- `failed_mcp_server_names`:
254
-
255
- ```ruby
256
- robot = RobotLab.build(name: "t", system_prompt: "hi",
257
- mcp: [{ name: "x", transport: { type: "bogus", command: "z" } }])
258
- robot.connect_mcp!
259
- # WARN -- : Robot 't' error connecting to MCP server 'x': Invalid transport type: bogus...
260
- robot.failed_mcp_server_names # => ["x"]
261
- ```
262
-
263
- ## See Also
264
-
265
- - [MCP Client](client.md)
266
- - [MCP Server](server.md)
267
- - [Transports](transports.md)