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