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,252 +0,0 @@
1
- # MCP Server Configuration
2
-
3
- Data structure for MCP server connection configuration.
4
-
5
- ## Class: `RobotLab::MCP::Server`
6
-
7
- `Server` is a configuration object that defines how to connect to an MCP server. It holds the server name and transport settings, and validates the configuration on construction.
8
-
9
- This is **not** an MCP server implementation -- it is the configuration used by `MCP::Client` to establish a connection to an external MCP server.
10
-
11
- ```ruby
12
- server = RobotLab::MCP::Server.new(
13
- name: "filesystem",
14
- description: "Read, write, and search local files and directories",
15
- transport: { type: "stdio", command: "mcp-server-filesystem", args: ["--root", "/data"] }
16
- )
17
-
18
- # With custom timeout
19
- server = RobotLab::MCP::Server.new(
20
- name: "slow_server",
21
- transport: { type: "stdio", command: "heavy-mcp-server" },
22
- timeout: 30
23
- )
24
- ```
25
-
26
- ## Constructor
27
-
28
- ```ruby
29
- Server.new(name:, transport:, timeout: nil, description: nil, **_extra)
30
- ```
31
-
32
- **Parameters:**
33
-
34
- | Name | Type | Default | Description |
35
- |------|------|---------|-------------|
36
- | `name` | `String` | **required** | Unique server identifier |
37
- | `transport` | `Hash` | **required** | Transport configuration (must include `type`) |
38
- | `timeout` | `Numeric`, `nil` | `15` | Request timeout in seconds. Values >= 1000 are auto-converted from milliseconds. Minimum 1 second |
39
- | `description` | `String`, `nil` | `""` | Human-readable summary of what the server does; scored by `MCP::ServerDiscovery` |
40
-
41
- Any other keyword is absorbed by `**_extra` and silently discarded, so a
42
- misspelled key raises nothing.
43
-
44
- **Raises:** `ArgumentError` if:
45
- - The transport type is not one of the valid types
46
- - A stdio transport is missing the `:command` key
47
- - A network transport (ws, websocket, sse, streamable-http, http) is missing the `:url` key
48
-
49
- ## Constants
50
-
51
- ```ruby
52
- RobotLab::MCP::Server::VALID_TRANSPORT_TYPES
53
- # => ["stdio", "sse", "ws", "websocket", "streamable-http", "http"]
54
-
55
- RobotLab::MCP::Server::DEFAULT_TIMEOUT
56
- # => 15 (seconds)
57
- ```
58
-
59
- ## Attributes
60
-
61
- ### name
62
-
63
- ```ruby
64
- server.name # => String
65
- ```
66
-
67
- The server identifier string.
68
-
69
- ### description
70
-
71
- ```ruby
72
- server.description # => String
73
- ```
74
-
75
- Human-readable summary of the server's capabilities. Always a String — a `nil`
76
- or omitted `description:` becomes `""`. Used by `MCP::ServerDiscovery`, which
77
- scores `"#{name} #{description}"` against the user's message when a robot is
78
- built with `mcp_discovery: true`. Discovery falls back to the full server list
79
- when no server in the list has a non-empty description.
80
-
81
- ### transport
82
-
83
- ```ruby
84
- server.transport # => Hash
85
- ```
86
-
87
- The normalized transport configuration hash (keys are symbols, type is downcased).
88
-
89
- ### timeout
90
-
91
- ```ruby
92
- server.timeout # => Numeric
93
- ```
94
-
95
- Request timeout in seconds. Defaults to `DEFAULT_TIMEOUT` (15). Values >= 1000 passed to the constructor are auto-converted from milliseconds to seconds. The minimum is 1 second.
96
-
97
- ## Methods
98
-
99
- ### transport_type
100
-
101
- ```ruby
102
- server.transport_type # => String
103
- ```
104
-
105
- Returns the transport type string (e.g., `"stdio"`, `"ws"`, `"sse"`).
106
-
107
- ### to_h
108
-
109
- ```ruby
110
- server.to_h
111
- # => { name: "y", description: "", transport: { type: "sse", url: "http://x" }, timeout: 30.0 }
112
- ```
113
-
114
- Converts the server configuration to a hash representation. The hash is **not**
115
- compacted — all four keys are always present, and `description` is `""` when it
116
- was never set. Note that `timeout` is normalized to a Float unless it defaulted
117
- (`DEFAULT_TIMEOUT` is the Integer `15`).
118
-
119
- ## Transport Configuration Options
120
-
121
- ### Stdio Transport
122
-
123
- For local MCP servers running as subprocesses:
124
-
125
- ```ruby
126
- Server.new(
127
- name: "filesystem",
128
- transport: {
129
- type: "stdio",
130
- command: "mcp-server-filesystem", # Required
131
- args: ["--root", "/data"], # Optional
132
- env: { "DEBUG" => "true" } # Optional
133
- }
134
- )
135
- ```
136
-
137
- | Key | Type | Required | Description |
138
- |-----|------|----------|-------------|
139
- | `type` | `String` | Yes | Must be `"stdio"` |
140
- | `command` | `String` | Yes | Executable command |
141
- | `args` | `Array<String>` | No | Command arguments |
142
- | `env` | `Hash` | No | Environment variables |
143
-
144
- ### WebSocket Transport
145
-
146
- For bidirectional real-time communication:
147
-
148
- ```ruby
149
- Server.new(
150
- name: "neon",
151
- transport: {
152
- type: "ws",
153
- url: "ws://localhost:8080" # Required
154
- }
155
- )
156
- ```
157
-
158
- | Key | Type | Required | Description |
159
- |-----|------|----------|-------------|
160
- | `type` | `String` | Yes | `"ws"` or `"websocket"` |
161
- | `url` | `String` | Yes | WebSocket endpoint URL |
162
-
163
- ### SSE Transport
164
-
165
- For server-sent events streaming:
166
-
167
- ```ruby
168
- Server.new(
169
- name: "streaming",
170
- transport: {
171
- type: "sse",
172
- url: "http://localhost:8080/sse" # Required
173
- }
174
- )
175
- ```
176
-
177
- | Key | Type | Required | Description |
178
- |-----|------|----------|-------------|
179
- | `type` | `String` | Yes | Must be `"sse"` |
180
- | `url` | `String` | Yes | SSE endpoint URL |
181
-
182
- ### Streamable HTTP Transport
183
-
184
- For HTTP-based communication with session management:
185
-
186
- ```ruby
187
- Server.new(
188
- name: "api",
189
- transport: {
190
- type: "streamable-http",
191
- url: "https://server.smithery.ai/neon/mcp", # Required
192
- session_id: "abc123", # Optional
193
- auth_provider: -> { "Bearer #{token}" } # Optional
194
- }
195
- )
196
- ```
197
-
198
- | Key | Type | Required | Description |
199
- |-----|------|----------|-------------|
200
- | `type` | `String` | Yes | `"streamable-http"` or `"http"` |
201
- | `url` | `String` | Yes | HTTP endpoint URL |
202
- | `session_id` | `String` | No | Session identifier |
203
- | `auth_provider` | `Proc` | No | Authentication callback returning auth header value |
204
-
205
- ## Examples
206
-
207
- ### Multiple Server Configurations
208
-
209
- ```ruby
210
- servers = [
211
- {
212
- name: "filesystem",
213
- transport: { type: "stdio", command: "mcp-server-filesystem", args: ["/data"] }
214
- },
215
- {
216
- name: "github",
217
- transport: { type: "stdio", command: "mcp-server-github" }
218
- },
219
- {
220
- name: "database",
221
- transport: { type: "ws", url: "ws://localhost:9090" }
222
- }
223
- ]
224
-
225
- # Pass directly to a robot
226
- robot = Robot.new(
227
- name: "dev_assistant",
228
- system_prompt: "You help with development tasks.",
229
- mcp: servers
230
- )
231
-
232
- # `run` defaults to `mcp: :none, tools: :none` — opt in per run
233
- robot.run("What changed in the repo?", mcp: :inherit, tools: :inherit)
234
- ```
235
-
236
- ### Creating a Client from a Server
237
-
238
- ```ruby
239
- server = RobotLab::MCP::Server.new(
240
- name: "tools",
241
- transport: { type: "ws", url: "ws://localhost:8080" }
242
- )
243
-
244
- client = RobotLab::MCP::Client.new(server)
245
- client.connect
246
- ```
247
-
248
- ## See Also
249
-
250
- - [MCP Overview](index.md)
251
- - [MCP Client](client.md)
252
- - [Transports](transports.md)
@@ -1,339 +0,0 @@
1
- # MCP Transports
2
-
3
- Communication methods for MCP client-server connections.
4
-
5
- ## Overview
6
-
7
- Transports handle the low-level communication between `MCP::Client` and external MCP servers. All transports implement the same interface defined by `Transports::Base`, using JSON-RPC 2.0 for message exchange and the MCP protocol version `2024-11-05` for initialization.
8
-
9
- RobotLab provides four built-in transport types:
10
-
11
- | Transport | Class | Use Case | Status |
12
- |-----------|-------|----------|--------|
13
- | Stdio | `Transports::Stdio` | Local subprocess servers | Fully working |
14
- | WebSocket | `Transports::WebSocket` | Real-time bidirectional | **Broken** — see below |
15
- | SSE | `Transports::SSE` | Server-sent events | Working, but `connect` reports success unconditionally |
16
- | Streamable HTTP | `Transports::StreamableHTTP` | HTTP with session support | Working, but `connect` reports success unconditionally |
17
-
18
- > **Read this before using a non-stdio transport.** Only `Stdio` performs its
19
- > connection and MCP handshake synchronously. The other three wrap that work in
20
- > an `Async do ... end` block whose result is never awaited, so any error raised
21
- > inside — including a refused TCP connection — is discarded. `connect` returns
22
- > `self` regardless. The specific consequences are documented per transport
23
- > below.
24
-
25
- ## Base Interface
26
-
27
- All transports inherit from `RobotLab::MCP::Transports::Base` and implement:
28
-
29
- ```ruby
30
- class RobotLab::MCP::Transports::Base
31
- DEFAULT_TIMEOUT = 15 # seconds
32
-
33
- attr_reader :config # => Hash (symbolized keys, :timeout removed)
34
- attr_reader :timeout # => Numeric (seconds, extracted from config)
35
-
36
- def connect # Establish connection, returns self
37
- def send_request(message) # Send JSON-RPC message, returns Hash response
38
- def close # Close connection, returns self
39
- def connected? # Returns Boolean
40
- end
41
- ```
42
-
43
- The `timeout` is extracted from the config hash during initialization (and removed from `config`). If not provided, it defaults to `DEFAULT_TIMEOUT` (15 seconds). The timeout is propagated from `MCP::Server` through `MCP::Client` to the transport.
44
-
45
- **Only `Stdio` enforces the timeout.** `SSE`, `WebSocket`, and `StreamableHTTP`
46
- store `@timeout` and expose it through the `timeout` reader, but never reference
47
- it — their requests are not time-bounded by this value. If you need a bound on a
48
- non-stdio transport, wrap the call yourself, or route the client through an
49
- `MCP::ConnectionPoller` (stdio only).
50
-
51
- ## Stdio Transport
52
-
53
- **Class:** `RobotLab::MCP::Transports::Stdio`
54
-
55
- Spawns a subprocess and communicates via stdin/stdout using JSON-RPC messages (one per line). Automatically sends MCP `initialize` and `notifications/initialized` on connect. All blocking I/O is wrapped with `Timeout.timeout` so a missing or hung server cannot block the caller forever.
56
-
57
- ### Configuration
58
-
59
- ```ruby
60
- {
61
- type: "stdio",
62
- command: "mcp-server-filesystem", # Required: executable command
63
- args: ["--root", "/data"], # Optional: command arguments
64
- env: { "DEBUG" => "true" }, # Optional: environment variables
65
- timeout: 10 # Optional: request timeout in seconds (default: 15)
66
- }
67
- ```
68
-
69
- | Key | Type | Required | Description |
70
- |-----|------|----------|-------------|
71
- | `command` | `String` | Yes | Executable command to spawn |
72
- | `args` | `Array<String>` | No | Command arguments |
73
- | `env` | `Hash` | No | Environment variables (merged with current env) |
74
- | `timeout` | `Numeric` | No | Request timeout in seconds (default: 15) |
75
-
76
- ### Behavior
77
-
78
- - Uses `Open3.popen3` to spawn the subprocess
79
- - Verifies the process actually started (raises `MCPError` if it exits immediately)
80
- - Writes JSON-RPC messages to stdin (one per line)
81
- - Reads responses from stdout, skipping notifications (messages without `id`)
82
- - All blocking reads are wrapped with `Timeout.timeout` — raises `MCPError` if the server does not respond within the timeout period
83
- - `connected?` returns `true` when the subprocess is alive
84
- - `close` calls `cleanup_process` to reliably close stdin, stdout, stderr and kill the subprocess
85
- - Handles `Errno::ENOENT` (command not found), `Errno::EPIPE` / `IOError` (broken pipe / connection lost), and `Timeout::Error` (hung server) with clear error messages
86
-
87
- ### Example
88
-
89
- ```ruby
90
- transport = RobotLab::MCP::Transports::Stdio.new(
91
- command: "mcp-server-filesystem",
92
- args: ["--root", "/data"],
93
- env: { "DEBUG" => "true" },
94
- timeout: 10
95
- )
96
-
97
- transport.connect
98
- response = transport.send_request({ jsonrpc: "2.0", id: 1, method: "tools/list" })
99
- transport.close
100
- ```
101
-
102
- ## WebSocket Transport
103
-
104
- **Class:** `RobotLab::MCP::Transports::WebSocket`
105
-
106
- Intended to use `async-websocket` for non-blocking bidirectional communication.
107
-
108
- > **This transport does not currently work.** `connect` calls
109
- > `Async::HTTP::Endpoint.parse`, but only requires `async` and
110
- > `async/websocket/client` — the `Async::HTTP` namespace is never loaded, so the
111
- > call raises `NameError`. That happens inside an un-awaited `Async` block, so
112
- > the error is swallowed: `connect` returns `self`, no `MCPError` is raised, and
113
- > `connected?` stays `false`. Every subsequent `send_request` then raises
114
- > `MCPError, "Not connected"`.
115
- >
116
- > Verified: `WebSocket.new(url: "ws://127.0.0.1:9/ws").connect.connected?` is
117
- > `false` with no exception surfacing.
118
- >
119
- > The rescue on `connect` only catches `LoadError`, which is raised if
120
- > `async-websocket` is missing. Because of the `Async::HTTP::Endpoint` call, this
121
- > transport also needs `async-http` even once `async-websocket` is installed —
122
- > both are declared as runtime dependencies of the gem.
123
-
124
- ### Configuration
125
-
126
- ```ruby
127
- {
128
- type: "ws", # or "websocket"
129
- url: "wss://mcp.example.com/ws" # Required: WebSocket endpoint
130
- }
131
- ```
132
-
133
- | Key | Type | Required | Description |
134
- |-----|------|----------|-------------|
135
- | `url` | `String` | Yes | WebSocket endpoint URL |
136
-
137
- ### Behavior
138
-
139
- - Intends to use `Async::WebSocket::Client.connect` within an `Async` block, then
140
- send the MCP `initialize` handshake
141
- - In practice the block raises `NameError` on `Async::HTTP::Endpoint` before the
142
- connection is created, and the un-awaited block discards it
143
- - `send_request` sends JSON-RPC messages as JSON strings and reads the response
144
- inside an awaited `Async` block — but it raises `MCPError, "Not connected"`
145
- because `@connected` was never set
146
- - `connect` raises `MCPError` only for `LoadError` (missing `async-websocket`);
147
- it does not raise for a connection failure
148
- - `close` is a no-op while `@connected` is `false`
149
-
150
- ### Example
151
-
152
- ```ruby
153
- transport = RobotLab::MCP::Transports::WebSocket.new(
154
- url: "ws://localhost:8080"
155
- )
156
-
157
- transport.connect
158
- transport.connected? # => false, even against a live server
159
-
160
- # Raises MCPError: "Not connected"
161
- transport.send_request({ jsonrpc: "2.0", id: 1, method: "tools/list" })
162
- ```
163
-
164
- ## SSE Transport
165
-
166
- **Class:** `RobotLab::MCP::Transports::SSE`
167
-
168
- Uses `async-http` for HTTP-based communication. Sends requests via HTTP POST and reads responses. Requires the `async-http` gem.
169
-
170
- ### Configuration
171
-
172
- ```ruby
173
- {
174
- type: "sse",
175
- url: "http://localhost:8080/sse" # Required: SSE endpoint
176
- }
177
- ```
178
-
179
- | Key | Type | Required | Description |
180
- |-----|------|----------|-------------|
181
- | `url` | `String` | Yes | SSE/HTTP endpoint URL |
182
-
183
- ### Behavior
184
-
185
- - Creates an `Async::HTTP::Client` on connect, then sends the MCP `initialize`
186
- handshake
187
- - Sends JSON-RPC messages via HTTP POST with `Content-Type: application/json`
188
- - Reads and parses JSON response body
189
- - Raises `MCPError` if the `async-http` gem is not installed (`LoadError` only)
190
- - The `timeout` from the server config is stored but never applied
191
-
192
- > **`connect` always reports success.** `@connected = true` is assigned *before*
193
- > `send_initialize` runs, and the whole sequence is inside an un-awaited `Async`
194
- > block. Against an unreachable host, `connect` returns `self`, `connected?`
195
- > returns `true`, and the handshake failure is never surfaced. The first real
196
- > `send_request` is where the failure appears.
197
- >
198
- > Verified: `SSE.new(url: "http://127.0.0.1:9/sse").connect.connected?` is `true`.
199
-
200
- ### Example
201
-
202
- ```ruby
203
- transport = RobotLab::MCP::Transports::SSE.new(
204
- url: "http://localhost:8080/sse"
205
- )
206
-
207
- transport.connect
208
- # connected? is true here whether or not the server exists
209
-
210
- response = transport.send_request({ jsonrpc: "2.0", id: 1, method: "tools/list" })
211
- transport.close
212
- ```
213
-
214
- ## Streamable HTTP Transport
215
-
216
- **Class:** `RobotLab::MCP::Transports::StreamableHTTP`
217
-
218
- HTTP-based transport with session management and optional authentication. Supports session IDs for maintaining server-side state across requests. Requires the `async-http` gem.
219
-
220
- ### Configuration
221
-
222
- ```ruby
223
- {
224
- type: "streamable-http", # or "http"
225
- url: "https://server.smithery.ai/neon/mcp", # Required: HTTP endpoint
226
- session_id: "abc123", # Optional: session identifier
227
- auth_provider: -> { "Bearer #{token}" } # Optional: auth callback
228
- }
229
- ```
230
-
231
- | Key | Type | Required | Description |
232
- |-----|------|----------|-------------|
233
- | `url` | `String` | Yes | HTTP endpoint URL |
234
- | `session_id` | `String` | No | Pre-existing session identifier |
235
- | `auth_provider` | `Proc` | No | Callback returning Authorization header value |
236
-
237
- ### Behavior
238
-
239
- - Creates an `Async::HTTP::Client` on connect
240
- - Sends MCP `initialize` on connect; if no session ID was provided, extracts it from the server response (`serverInfo.sessionId`)
241
- - Sends `X-Session-ID` header with each request when a session ID is available
242
- - Calls `auth_provider` for each request to populate the `Authorization` header
243
- - Exposes `session_id` reader for accessing the current session ID
244
- - Raises `MCPError` if the `async-http` gem is not installed (`LoadError` only)
245
- - The `timeout` from the server config is stored but never applied
246
-
247
- > **`connect` always reports success**, for the same reason as SSE:
248
- > `@connected = true` precedes `send_initialize`, and the enclosing `Async` block
249
- > is never awaited. `connected?` returns `true` against an unreachable host, and
250
- > `session_id` stays at whatever you configured (`nil` if you configured nothing)
251
- > because the handshake result was discarded.
252
- >
253
- > Verified: `StreamableHTTP.new(url: "http://127.0.0.1:9/mcp").connect` yields
254
- > `connected? == true`, `session_id == nil`.
255
-
256
- ### Example
257
-
258
- ```ruby
259
- transport = RobotLab::MCP::Transports::StreamableHTTP.new(
260
- url: "https://server.smithery.ai/neon/mcp",
261
- auth_provider: -> { "Bearer #{ENV['MCP_TOKEN']}" }
262
- )
263
-
264
- transport.connect
265
- puts transport.session_id # => pre-configured value, or nil until a request lands
266
-
267
- response = transport.send_request({ jsonrpc: "2.0", id: 1, method: "tools/list" })
268
- transport.close
269
- ```
270
-
271
- ## Connection Lifecycle
272
-
273
- All transports expose the same four-step lifecycle:
274
-
275
- 1. **Create** -- instantiate with configuration hash
276
- 2. **Connect** -- establish connection and perform MCP protocol initialization
277
- 3. **Request/Response** -- send JSON-RPC requests, receive responses
278
- 4. **Close** -- tear down connection and release resources
279
-
280
- Step 2 behaves differently per transport:
281
-
282
- | Transport | `connect` is synchronous | Errors surface from `connect` | `connected?` reflects reality |
283
- |-----------|--------------------------|-------------------------------|-------------------------------|
284
- | `Stdio` | Yes | Yes (`MCPError`) | Yes — also checks the process is alive |
285
- | `SSE` | No (un-awaited `Async`) | No | No — always `true` after `connect` |
286
- | `StreamableHTTP` | No (un-awaited `Async`) | No | No — always `true` after `connect` |
287
- | `WebSocket` | No (un-awaited `Async`) | No | No — always `false` after `connect` |
288
-
289
- Only `Stdio` gives you a trustworthy answer at connect time. For the other three,
290
- treat the first `send_request` as the real connection test.
291
-
292
- Each transport builds the same MCP `initialize` message during connect:
293
-
294
- ```json
295
- {
296
- "jsonrpc": "2.0",
297
- "id": 0,
298
- "method": "initialize",
299
- "params": {
300
- "protocolVersion": "2024-11-05",
301
- "capabilities": {},
302
- "clientInfo": {
303
- "name": "RobotLab",
304
- "version": "<current version>"
305
- }
306
- }
307
- }
308
- ```
309
-
310
- ## Error Handling
311
-
312
- All transports raise `RobotLab::MCPError` for connection and communication failures:
313
-
314
- ```ruby
315
- begin
316
- transport.connect
317
- transport.send_request(message)
318
- rescue RobotLab::MCPError => e
319
- puts "Transport error: #{e.message}"
320
- ensure
321
- transport.close
322
- end
323
- ```
324
-
325
- Specific error cases:
326
- - **Not connected** -- calling `send_request` before `connect` raises `MCPError` (all transports)
327
- - **Missing gem** -- WebSocket, SSE, and HTTP transports raise `MCPError` with a `LoadError` message if the required gem (`async-websocket` / `async-http`) is not installed. This is the *only* error `connect` re-raises on those three
328
- - **Connection refused / unreachable host** -- **not** reported by SSE, WebSocket, or StreamableHTTP `connect`; the error is discarded with the un-awaited `Async` block
329
- - **No response** -- Stdio transport raises `MCPError` if the subprocess produces no output (EOF on stdout)
330
- - **Command not found** -- Stdio transport raises `MCPError` with the original `Errno::ENOENT` message
331
- - **Timeout** -- Stdio transport raises `MCPError` if the server does not respond within the configured timeout. No other transport enforces a timeout
332
- - **Broken pipe** -- Stdio transport raises `MCPError` and marks itself disconnected on `Errno::EPIPE` or `IOError`
333
- - **Immediate exit** -- Stdio transport raises `MCPError` if the server process exits immediately after spawn
334
-
335
- ## See Also
336
-
337
- - [MCP Overview](index.md)
338
- - [MCP Client](client.md)
339
- - [Server Configuration](server.md)