Summary
The OpenClaw Unicity plugin loads and discovers tools through our Node.js MCP bridge, but six critical pathways are broken:
- Plugin tools never reach the LLM's tool list
- Plugin tool execution routes through
ServerManager which doesn't know about plugins
- Plugin config never reaches the bridge subprocess
- Hook forwarding (
before_agent_start) is dead code
- Inbound channel messages have no Rust-side handler
- Outbound channel message routing has no mechanism
This issue tracks the minimal changes needed to make the Unicity plugin (and any future MCP-backed plugin) fully functional end-to-end.
Sub-Issues (4 PRs)
| PR |
Issue |
Items |
Depends On |
| A1 |
#33 — Wire plugin tools into LLM tool list and execution dispatch |
1+2 |
— |
| A2 |
#34 — Pass plugin config to bridge via post-init notification |
3 |
— |
| B |
#35 — Forward hook events to plugins and collect agent context |
4 |
#33 |
| C |
#36 — Channel message routing: inbound notifications and outbound verification |
5+6 |
#33, #34, #35 |
Phase A (parallel):
├── #33 Items 1+2 (tool wiring)
└── #34 Item 3 (config passing)
Phase B (after #33):
└── #35 Item 4 (hook forwarding)
Phase C (after all above):
└── #36 Items 5+6 (channel messaging)
Design Decision: PluginRegistry on AgentRuntime vs ServerManager Registration
Considered: Registering plugin Peer<RoleClient> handles directly into ServerManager so plugin tools flow through the existing MCP tool list and dispatch path automatically.
Rejected because:
ToolCall::parse_name() uses split_once(':'). Tool name plugin:my-plugin:echo splits as server=plugin, tool=my-plugin:echo — wrong server lookup. Fixing this requires either dropping the plugin: prefix (name collision risk with MCP servers) or custom parse logic.
- Lifecycle dual-ownership: both
ServerManager::stop() and McpPlugin::unload() would try to close the same peer session.
ServerManager requires ServerConfig for each entry (restart policy, command, args) — plugins have their own lifecycle via Plugin::load()/unload().
Chosen approach: Add PluginRegistry as an optional field on AgentRuntime. Explicit, avoids naming conflicts, clean separation of plugin vs MCP server lifecycle.
Scope
- ~6 files modified, 0-1 new files
- Core changes in
runtime.rs and mcp_plugin.rs
- Bridge changes: 1-2 notification handlers in
astrid_bridge.mjs
- New dependency:
astrid-plugins added to astrid-runtime/Cargo.toml
- No new crates
Safety & Failure Modes
| Scenario |
Mitigation |
Plugin unloads while run_loop holds read lock on PluginRegistry |
Tool list is snapshotted before iteration. Execution of a just-unloaded tool returns "plugin tool not found" error to LLM — graceful degradation. |
| Plugin process killed mid-tool-call |
peer.call_tool() returns transport error, caught by existing ToolExecution error handling. |
| Config race (notification arrives before bridge handler ready) |
Bridge dispatch loop runs before handshake completes — notification handler is always ready. |
| Inbound message flood from plugin |
Bounded mpsc(256) with backpressure. Messages >1MB rejected at handler. |
Concurrent sessions trigger before_agent_start simultaneously |
Peer is thread-safe. send_notification() is concurrent-safe. No mutex needed. |
| Plugin tool arguments from LLM |
Flow through same PreToolCall hook as all other tools — existing security interceptor applies. |
Verification
- Unit tests: Extend
PluginRegistry tests to cover tool list merging; add AgentRuntime test with mock plugin
- Integration test: Load Unicity plugin via gateway, verify:
- Plugin tools appear in
list_tools RPC response
- Calling a plugin tool returns a result (not "server not found")
- Plugin config is non-empty in bridge (add debug log)
before_agent_start hook fires and context is prepended
- End-to-end: Send a message through CLI → agent → Unicity channel → verify round-trip
- Run:
cargo test --workspace -- --quiet to ensure no regressions
Summary
The OpenClaw Unicity plugin loads and discovers tools through our Node.js MCP bridge, but six critical pathways are broken:
ServerManagerwhich doesn't know about pluginsbefore_agent_start) is dead codeThis issue tracks the minimal changes needed to make the Unicity plugin (and any future MCP-backed plugin) fully functional end-to-end.
Sub-Issues (4 PRs)
Design Decision: PluginRegistry on AgentRuntime vs ServerManager Registration
Considered: Registering plugin
Peer<RoleClient>handles directly intoServerManagerso plugin tools flow through the existing MCP tool list and dispatch path automatically.Rejected because:
ToolCall::parse_name()usessplit_once(':'). Tool nameplugin:my-plugin:echosplits as server=plugin, tool=my-plugin:echo— wrong server lookup. Fixing this requires either dropping theplugin:prefix (name collision risk with MCP servers) or custom parse logic.ServerManager::stop()andMcpPlugin::unload()would try to close the same peer session.ServerManagerrequiresServerConfigfor each entry (restart policy, command, args) — plugins have their own lifecycle viaPlugin::load()/unload().Chosen approach: Add
PluginRegistryas an optional field onAgentRuntime. Explicit, avoids naming conflicts, clean separation of plugin vs MCP server lifecycle.Scope
runtime.rsandmcp_plugin.rsastrid_bridge.mjsastrid-pluginsadded toastrid-runtime/Cargo.tomlSafety & Failure Modes
run_loopholds read lock onPluginRegistrypeer.call_tool()returns transport error, caught by existingToolExecutionerror handling.mpsc(256)with backpressure. Messages >1MB rejected at handler.before_agent_startsimultaneouslyPeeris thread-safe.send_notification()is concurrent-safe. No mutex needed.PreToolCallhook as all other tools — existing security interceptor applies.Verification
PluginRegistrytests to cover tool list merging; addAgentRuntimetest with mock pluginlist_toolsRPC responsebefore_agent_starthook fires and context is prependedcargo test --workspace -- --quietto ensure no regressions