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,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)