robot_lab 0.2.6 → 0.2.7
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.
- checksums.yaml +4 -4
- data/README.md +124 -64
- data/docs/api/core/index.md +41 -15
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +255 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +551 -61
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +110 -17
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +80 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +519 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +79 -31
- data/docs/architecture/index.md +86 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +145 -38
- data/docs/architecture/robot-execution.md +172 -90
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +284 -97
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +258 -50
- data/docs/guides/creating-networks.md +214 -30
- data/docs/guides/hooks.md +141 -54
- data/docs/guides/knowledge.md +35 -4
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +95 -47
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +237 -17
- data/docs/index.md +36 -4
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +12 -7
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +18 -8
- data/examples/README.md +199 -45
- data/examples/common.rb +79 -11
- data/examples/xyzzy.rb +8 -1
- data/lib/robot_lab/config.rb +10 -5
- data/lib/robot_lab/names.rb +402 -0
- data/lib/robot_lab/robot/agent_skill_matching.rb +1 -3
- data/lib/robot_lab/robot/bus_messaging.rb +16 -8
- data/lib/robot_lab/robot/template_rendering.rb +16 -3
- data/lib/robot_lab/robot.rb +23 -2
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +21 -15
- data/mkdocs.yml +6 -1
- metadata +7 -2
data/docs/api/index.md
CHANGED
|
@@ -8,10 +8,50 @@ The fundamental building blocks of RobotLab:
|
|
|
8
8
|
|
|
9
9
|
| Class | Description |
|
|
10
10
|
|-------|-------------|
|
|
11
|
-
| [Robot](core/robot.md) | LLM-powered agent with
|
|
12
|
-
| [
|
|
11
|
+
| [Robot](core/robot.md) | LLM-powered agent with templates, tools, memory, and MCP |
|
|
12
|
+
| [RobotResult](core/result.md) | Value object returned by every `robot.run()` |
|
|
13
|
+
| [Network](core/network.md) | Orchestrates multiple robots as a SimpleFlow pipeline |
|
|
13
14
|
| [Memory](core/memory.md) | Reactive key-value store for sharing data |
|
|
15
|
+
| [StateProxy](core/state.md) | Hash/method-access wrapper returned by `memory.data` |
|
|
14
16
|
| [Tool](core/tool.md) | Custom function robots can call |
|
|
17
|
+
| `RunConfig` | Shared LLM / tool / infrastructure configuration — see [Robot: RunConfig](core/robot.md#runconfig) |
|
|
18
|
+
|
|
19
|
+
There is **no** `RobotLab::State` class and no `RobotLab::NetworkRun` class. Runtime
|
|
20
|
+
state lives in `Memory`; `memory.data` returns a `StateProxy`.
|
|
21
|
+
|
|
22
|
+
## Hooks
|
|
23
|
+
|
|
24
|
+
The framework's extension seam — `Hooks.run` brackets every robot run, network
|
|
25
|
+
run, task, LLM generation, tool call, compaction, and `learn` call:
|
|
26
|
+
|
|
27
|
+
| Class | Description |
|
|
28
|
+
|-------|-------------|
|
|
29
|
+
| [Hook](hooks.md#robotlabhook) | Handler base class; subclasses implement hooks as class methods |
|
|
30
|
+
| [HookRegistry](hooks.md#robotlabhookregistry) | The store behind `RobotLab.hooks`, `network.hooks`, `robot.hooks` |
|
|
31
|
+
| [Hooks](hooks.md#robotlabhooks) | The dispatcher |
|
|
32
|
+
| [HookContext](hooks.md#robotlabhookcontext) | Base context, plus one subclass per family |
|
|
33
|
+
|
|
34
|
+
## Skills
|
|
35
|
+
|
|
36
|
+
`SKILL.md` bundles, the scripts they expose as tools, and the sandbox that
|
|
37
|
+
confines them:
|
|
38
|
+
|
|
39
|
+
| Class | Description |
|
|
40
|
+
|-------|-------------|
|
|
41
|
+
| [AgentSkill](skills.md#robotlabagentskill) | One skill bundle: instructions + `scripts/` |
|
|
42
|
+
| [AgentSkillCatalog](skills.md#robotlabagentskillcatalog) | Lazy registry over `~/.prompts/skills/` |
|
|
43
|
+
| [Capabilities](skills.md#robotlabcapabilities) | Declared vs. ceiling filesystem / network / timeout grant |
|
|
44
|
+
| [ScriptTool](skills.md#robotlabscripttool) | Wraps an executable script as a `Tool` |
|
|
45
|
+
| [Sandbox](skills.md#robotlabsandbox) | macOS Seatbelt confinement, or a passthrough |
|
|
46
|
+
|
|
47
|
+
## Support Classes
|
|
48
|
+
|
|
49
|
+
Everything else with a public surface — `Task`, `Runnable`, `ToolConfig`,
|
|
50
|
+
`ToolManifest`, `Budget::Ledger`, `DoomLoopDetector`, `HistoryCompressor`,
|
|
51
|
+
`Convergence`, `TextAnalysis`, `DelegationFuture`, `RobotMessage`, `BusPoller`,
|
|
52
|
+
`Waiter`, `Narrator`, `Config`, `MCP::ServerDiscovery`,
|
|
53
|
+
`MCP::ConnectionPoller`, and `Streaming::SequenceCounter` — is documented on the
|
|
54
|
+
[Support Classes](support.md) page.
|
|
15
55
|
|
|
16
56
|
## Messages
|
|
17
57
|
|
|
@@ -49,16 +89,49 @@ Real-time response streaming:
|
|
|
49
89
|
|
|
50
90
|
```ruby
|
|
51
91
|
# Configuration
|
|
52
|
-
RobotLab.config
|
|
53
|
-
RobotLab.
|
|
92
|
+
RobotLab.config # => Config instance
|
|
93
|
+
RobotLab.configure { |c| ... } # => yields the Config for mutation
|
|
94
|
+
RobotLab.reload_config! # => reload from all sources
|
|
54
95
|
|
|
55
96
|
# Building
|
|
56
|
-
RobotLab.build(name
|
|
57
|
-
|
|
58
|
-
RobotLab.
|
|
97
|
+
RobotLab.build(name: "robot", template: nil, system_prompt: nil, context: {},
|
|
98
|
+
enable_cache: true, bus: nil, skills: nil, config: nil, **options)
|
|
99
|
+
RobotLab.create_network(name:, concurrency: :auto, config: nil) { ... }
|
|
100
|
+
RobotLab.create_memory(data: {}, enable_cache: true, **options)
|
|
59
101
|
|
|
60
102
|
# Rendering a template to a String (not a robot) -- see Building Robots guide
|
|
61
103
|
RobotLab.render_template(name, **context) # => String
|
|
104
|
+
|
|
105
|
+
# Hooks -- see the Hooks API page
|
|
106
|
+
RobotLab.hooks # => HookRegistry
|
|
107
|
+
RobotLab.on(HandlerClass, context: nil)
|
|
108
|
+
RobotLab.clear_hooks! # => replaces the global registry with a fresh one
|
|
109
|
+
|
|
110
|
+
# Hook scope (internal plumbing; read by Tool#call)
|
|
111
|
+
RobotLab.with_hook_scope(registries, per_run_hooks) { ... }
|
|
112
|
+
RobotLab.current_hook_scope # => { registries:, per_run_hooks: } or nil
|
|
113
|
+
|
|
114
|
+
# Extensions
|
|
115
|
+
RobotLab.register_extension(name, mod)
|
|
116
|
+
RobotLab.extension_loaded?(:ractor) # => Boolean
|
|
117
|
+
RobotLab.extension(:ractor)
|
|
118
|
+
RobotLab.loaded_extensions # => Array<Symbol>
|
|
62
119
|
```
|
|
63
120
|
|
|
121
|
+
`with_hook_scope` publishes the active run's registries in a `Thread.current`
|
|
122
|
+
slot so a tool executing deep inside RubyLLM's tool loop resolves the same
|
|
123
|
+
registries — including the network's and any per-run `hooks:` — instead of
|
|
124
|
+
falling back to `[RobotLab.hooks, robot.hooks]`. It is thread-local, so it does
|
|
125
|
+
not reach a tool that runs on another thread. See
|
|
126
|
+
[Hooks: with_hook_scope](hooks.md#with_hook_scope-current_hook_scope).
|
|
127
|
+
|
|
128
|
+
!!! warning "Tools and MCP are opt-in per call"
|
|
129
|
+
`Robot#run` defaults to `tools: :none, mcp: :none`. A bare `robot.run("...")`
|
|
130
|
+
sends the LLM **zero** tools and connects **no** MCP servers, even when
|
|
131
|
+
`local_tools:`/`mcp:` were supplied at build time. Pass `tools: :inherit`
|
|
132
|
+
(and `mcp: :inherit`) at *run* time to use what is attached. For a standalone
|
|
133
|
+
robot, do not pass `tools: :inherit` at *build* time — it resolves to an
|
|
134
|
+
allowlist matching nothing. (Inside a network it is the required opt-in to a
|
|
135
|
+
network `config:` list.) See [Robot: Configuration Hierarchy](core/robot.md#configuration-hierarchy).
|
|
136
|
+
|
|
64
137
|
See individual class documentation for detailed method references.
|
data/docs/api/mcp/client.md
CHANGED
|
@@ -6,8 +6,10 @@ Connects to MCP servers, discovers tools, and invokes them via the Model Context
|
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
8
|
client = RobotLab::MCP::Client.new(
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
{
|
|
10
|
+
name: "filesystem",
|
|
11
|
+
transport: { type: "stdio", command: "mcp-server-filesystem", args: ["--root", "/data"] }
|
|
12
|
+
}
|
|
11
13
|
)
|
|
12
14
|
|
|
13
15
|
client.connect
|
|
@@ -19,16 +21,22 @@ client.disconnect
|
|
|
19
21
|
## Constructor
|
|
20
22
|
|
|
21
23
|
```ruby
|
|
22
|
-
Client.new(server_or_config)
|
|
24
|
+
Client.new(server_or_config, poller: nil)
|
|
23
25
|
```
|
|
24
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
|
+
|
|
25
32
|
Accepts either a `Server` instance or a Hash configuration. When a Hash is provided, it is used to construct a `Server` internally.
|
|
26
33
|
|
|
27
34
|
**Parameters:**
|
|
28
35
|
|
|
29
|
-
| Name | Type | Description |
|
|
30
|
-
|
|
31
|
-
| `server_or_config` | `Server`, `Hash` | Server instance or configuration hash |
|
|
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)) |
|
|
32
40
|
|
|
33
41
|
**Hash Configuration Keys:**
|
|
34
42
|
|
|
@@ -37,6 +45,7 @@ Accepts either a `Server` instance or a Hash configuration. When a Hash is provi
|
|
|
37
45
|
| `name` | `String` | Yes | Server identifier |
|
|
38
46
|
| `transport` | `Hash` | Yes | Transport configuration (must include `type`) |
|
|
39
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` |
|
|
40
49
|
|
|
41
50
|
**Raises:** `ArgumentError` if the config is neither a `Server` nor a `Hash`.
|
|
42
51
|
|
|
@@ -50,13 +59,24 @@ client.server # => RobotLab::MCP::Server
|
|
|
50
59
|
|
|
51
60
|
The MCP server configuration object.
|
|
52
61
|
|
|
53
|
-
###
|
|
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
|
|
54
72
|
|
|
55
73
|
```ruby
|
|
74
|
+
client.connected # => Boolean
|
|
56
75
|
client.connected? # => Boolean
|
|
57
76
|
```
|
|
58
77
|
|
|
59
|
-
Whether the client is currently connected to the server.
|
|
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.
|
|
60
80
|
|
|
61
81
|
## Methods
|
|
62
82
|
|
|
@@ -66,9 +86,9 @@ Whether the client is currently connected to the server.
|
|
|
66
86
|
client.connect # => self
|
|
67
87
|
```
|
|
68
88
|
|
|
69
|
-
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.
|
|
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.
|
|
70
90
|
|
|
71
|
-
Connection failures are logged as warnings and the client remains in a disconnected state (does not raise).
|
|
91
|
+
Connection failures are logged as warnings and the client remains in a disconnected state (does not raise). Always check `connected?` afterwards.
|
|
72
92
|
|
|
73
93
|
### disconnect
|
|
74
94
|
|
|
@@ -76,7 +96,7 @@ Connection failures are logged as warnings and the client remains in a disconnec
|
|
|
76
96
|
client.disconnect # => self
|
|
77
97
|
```
|
|
78
98
|
|
|
79
|
-
Close the connection to the MCP server.
|
|
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.
|
|
80
100
|
|
|
81
101
|
### list_tools
|
|
82
102
|
|
|
@@ -166,7 +186,62 @@ Get a prompt by name with optional arguments.
|
|
|
166
186
|
client.to_h # => Hash
|
|
167
187
|
```
|
|
168
188
|
|
|
169
|
-
Converts the client to a hash representation containing server config and connection status
|
|
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.
|
|
170
245
|
|
|
171
246
|
## Transport Configuration
|
|
172
247
|
|
|
@@ -176,11 +251,13 @@ The transport type is determined by the `type` key in the transport hash of the
|
|
|
176
251
|
|
|
177
252
|
```ruby
|
|
178
253
|
client = RobotLab::MCP::Client.new(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
254
|
+
{
|
|
255
|
+
name: "local",
|
|
256
|
+
transport: {
|
|
257
|
+
type: "stdio",
|
|
258
|
+
command: "npx",
|
|
259
|
+
args: ["@modelcontextprotocol/server-filesystem", "/path"]
|
|
260
|
+
}
|
|
184
261
|
}
|
|
185
262
|
)
|
|
186
263
|
```
|
|
@@ -189,10 +266,12 @@ client = RobotLab::MCP::Client.new(
|
|
|
189
266
|
|
|
190
267
|
```ruby
|
|
191
268
|
client = RobotLab::MCP::Client.new(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
269
|
+
{
|
|
270
|
+
name: "remote",
|
|
271
|
+
transport: {
|
|
272
|
+
type: "ws",
|
|
273
|
+
url: "wss://mcp.example.com/ws"
|
|
274
|
+
}
|
|
196
275
|
}
|
|
197
276
|
)
|
|
198
277
|
```
|
|
@@ -201,10 +280,12 @@ client = RobotLab::MCP::Client.new(
|
|
|
201
280
|
|
|
202
281
|
```ruby
|
|
203
282
|
client = RobotLab::MCP::Client.new(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
283
|
+
{
|
|
284
|
+
name: "streaming",
|
|
285
|
+
transport: {
|
|
286
|
+
type: "sse",
|
|
287
|
+
url: "https://mcp.example.com/sse"
|
|
288
|
+
}
|
|
208
289
|
}
|
|
209
290
|
)
|
|
210
291
|
```
|
|
@@ -213,11 +294,13 @@ client = RobotLab::MCP::Client.new(
|
|
|
213
294
|
|
|
214
295
|
```ruby
|
|
215
296
|
client = RobotLab::MCP::Client.new(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
297
|
+
{
|
|
298
|
+
name: "http",
|
|
299
|
+
transport: {
|
|
300
|
+
type: "streamable-http",
|
|
301
|
+
url: "https://mcp.example.com/mcp",
|
|
302
|
+
session_id: "optional-session-id"
|
|
303
|
+
}
|
|
221
304
|
}
|
|
222
305
|
)
|
|
223
306
|
```
|
|
@@ -228,11 +311,11 @@ client = RobotLab::MCP::Client.new(
|
|
|
228
311
|
|
|
229
312
|
```ruby
|
|
230
313
|
client = RobotLab::MCP::Client.new(
|
|
231
|
-
name: "github",
|
|
232
|
-
transport: { type: "stdio", command: "mcp-server-github" }
|
|
314
|
+
{ name: "github", transport: { type: "stdio", command: "mcp-server-github" } }
|
|
233
315
|
)
|
|
234
316
|
|
|
235
317
|
client.connect
|
|
318
|
+
raise "could not connect" unless client.connected?
|
|
236
319
|
|
|
237
320
|
# List available tools
|
|
238
321
|
tools = client.list_tools
|
|
@@ -268,16 +351,27 @@ robot = Robot.new(
|
|
|
268
351
|
]
|
|
269
352
|
)
|
|
270
353
|
|
|
271
|
-
#
|
|
272
|
-
|
|
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)
|
|
273
358
|
puts result.last_text_content
|
|
274
359
|
```
|
|
275
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
|
+
|
|
276
364
|
### Error Handling
|
|
277
365
|
|
|
366
|
+
`connect` swallows its own failures, so test `connected?` rather than rescuing
|
|
367
|
+
around it:
|
|
368
|
+
|
|
278
369
|
```ruby
|
|
370
|
+
client.connect
|
|
371
|
+
|
|
279
372
|
begin
|
|
280
|
-
client.
|
|
373
|
+
raise RobotLab::MCPError, "not connected" unless client.connected?
|
|
374
|
+
|
|
281
375
|
result = client.call_tool("unknown_tool", {})
|
|
282
376
|
rescue RobotLab::MCPError => e
|
|
283
377
|
puts "MCP error: #{e.message}"
|
data/docs/api/mcp/index.md
CHANGED
|
@@ -13,10 +13,17 @@ robot = Robot.new(
|
|
|
13
13
|
mcp: [
|
|
14
14
|
{
|
|
15
15
|
name: "filesystem",
|
|
16
|
-
transport: {
|
|
16
|
+
transport: {
|
|
17
|
+
type: "stdio",
|
|
18
|
+
command: "npx",
|
|
19
|
+
args: ["@modelcontextprotocol/server-filesystem", "/data"]
|
|
20
|
+
}
|
|
17
21
|
}
|
|
18
22
|
]
|
|
19
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)
|
|
20
27
|
```
|
|
21
28
|
|
|
22
29
|
## Components
|
|
@@ -26,6 +33,8 @@ robot = Robot.new(
|
|
|
26
33
|
| [Client](client.md) | Connects to MCP servers, lists tools, calls tools |
|
|
27
34
|
| [Server](server.md) | Server configuration data structure |
|
|
28
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)) |
|
|
29
38
|
|
|
30
39
|
## Quick Start
|
|
31
40
|
|
|
@@ -42,33 +51,91 @@ robot = Robot.new(
|
|
|
42
51
|
]
|
|
43
52
|
)
|
|
44
53
|
|
|
45
|
-
result = robot.run("List my open pull requests")
|
|
54
|
+
result = robot.run("List my open pull requests", mcp: :inherit, tools: :inherit)
|
|
46
55
|
result.last_text_content
|
|
47
56
|
```
|
|
48
57
|
|
|
49
|
-
###
|
|
58
|
+
### Connecting at Run Time
|
|
50
59
|
|
|
51
|
-
|
|
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.
|
|
52
64
|
|
|
53
65
|
```ruby
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
```
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
63
110
|
```
|
|
64
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
|
+
|
|
65
122
|
### Direct Client Usage
|
|
66
123
|
|
|
124
|
+
`Client.new` takes the server (or config hash) as a **positional** argument:
|
|
125
|
+
|
|
67
126
|
```ruby
|
|
68
127
|
client = RobotLab::MCP::Client.new(
|
|
69
128
|
name: "filesystem",
|
|
70
129
|
transport: { type: "stdio", command: "mcp-server-filesystem", args: ["--root", "/data"] }
|
|
71
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
|
+
)
|
|
72
139
|
|
|
73
140
|
client.connect
|
|
74
141
|
tools = client.list_tools
|
|
@@ -76,6 +143,34 @@ result = client.call_tool("readFile", { path: "/data/config.yml" })
|
|
|
76
143
|
client.disconnect
|
|
77
144
|
```
|
|
78
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
|
+
|
|
79
174
|
## Transport Types
|
|
80
175
|
|
|
81
176
|
| Type | Config Key | Use Case |
|
|
@@ -92,33 +187,79 @@ The `mcp:` parameter on a Robot accepts three types of values:
|
|
|
92
187
|
| Value | Meaning |
|
|
93
188
|
|-------|---------|
|
|
94
189
|
| `:none` | No MCP servers (explicitly disabled) |
|
|
95
|
-
| `:inherit` |
|
|
190
|
+
| `:inherit` | Resolve against the parent level (network config, then global config) |
|
|
96
191
|
| `Array<Hash>` | Explicit list of server configurations |
|
|
97
192
|
|
|
98
|
-
Each server configuration hash
|
|
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.
|
|
99
211
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
| `transport` | `Hash` | Transport configuration (must include `type`) |
|
|
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.
|
|
104
215
|
|
|
105
216
|
## Error Handling
|
|
106
217
|
|
|
107
|
-
|
|
218
|
+
`RobotLab::MCPError` is raised when a request is made without an active
|
|
219
|
+
connection, and by the transports for protocol/I-O failures.
|
|
108
220
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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:
|
|
112
224
|
|
|
113
225
|
```ruby
|
|
226
|
+
client.connect
|
|
227
|
+
|
|
228
|
+
unless client.connected?
|
|
229
|
+
warn "MCP server unavailable"
|
|
230
|
+
return
|
|
231
|
+
end
|
|
232
|
+
|
|
114
233
|
begin
|
|
115
|
-
client.connect
|
|
116
234
|
client.call_tool("unknown_tool", {})
|
|
117
235
|
rescue RobotLab::MCPError => e
|
|
118
236
|
puts "MCP error: #{e.message}"
|
|
119
237
|
end
|
|
120
238
|
```
|
|
121
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
|
+
|
|
122
263
|
## See Also
|
|
123
264
|
|
|
124
265
|
- [MCP Client](client.md)
|