Summary
Types that derive Elicit generate elicit_checked(peer: Peer<RoleServer>) methods and are served via the #[elicit_tools] + #[tool_router] path, which produces a direct ServerHandler. The PluginRegistry plugin path expects a different handler signature (Arc<PluginContext> + CallToolRequestParams → BoxFuture<CallToolResult>).
There is currently no way to register a struct built with #[tool_router] as an ElicitPlugin in a PluginRegistry, making the two composition systems incompatible.
Use Case
In the destination library, all domain types derive Elicit. We want to expose them as a named plugin in a PluginRegistry:
PluginRegistry::new()
.register("destination", DestinationPlugin::new())
.serve(rmcp::transport::stdio()).await?;
Currently this requires either:
- Writing a manual
ElicitPlugin impl that bridges by extracting Peer from RequestContext (no precedent, fragile)
- Abandoning
PluginRegistry and using #[tool_router] standalone (loses composability with other plugins)
Proposed Solution
Add an adapter type — e.g. ServerHandlerPlugin or ToolRouterPlugin — that wraps any ServerHandler (produced by #[tool_router]) and implements ElicitPlugin:
pub struct ServerHandlerPlugin<H: ServerHandler> {
name: &'static str,
inner: H,
}
impl<H: ServerHandler + Send + Sync + 'static> ElicitPlugin for ServerHandlerPlugin<H> {
fn name(&self) -> &'static str { self.name }
fn list_tools(&self) -> Vec<Tool> { /* delegate to inner.list_tools() */ }
fn call_tool(&self, params, ctx) -> BoxFuture<...> {
/* delegate, bridging Peer extraction from RequestContext */
}
}
This would allow:
#[elicit_tools(CommonAddress, SpatialAddress, BusinessLicense)]
#[tool_router]
struct DestinationServer;
PluginRegistry::new()
.register("destination", ServerHandlerPlugin::new("destination", DestinationServer))
.register("http", elicit_reqwest::Plugin::new())
.serve(rmcp::transport::stdio()).await?;
Notes
- The key bridge needed is extracting
Peer<RoleServer> from RequestContext<RoleServer> so it can be passed to elicit_checked()
- Alternatively,
PluginRegistry could be extended with a register_handler(prefix, impl ServerHandler) method that wraps internally
- This would unlock composing any
#[tool_router]-based server as a plugin alongside shadow crates like elicit_reqwest
Summary
Types that derive
Elicitgenerateelicit_checked(peer: Peer<RoleServer>)methods and are served via the#[elicit_tools]+#[tool_router]path, which produces a directServerHandler. ThePluginRegistryplugin path expects a different handler signature (Arc<PluginContext> + CallToolRequestParams → BoxFuture<CallToolResult>).There is currently no way to register a struct built with
#[tool_router]as anElicitPluginin aPluginRegistry, making the two composition systems incompatible.Use Case
In the
destinationlibrary, all domain types deriveElicit. We want to expose them as a named plugin in aPluginRegistry:Currently this requires either:
ElicitPluginimpl that bridges by extractingPeerfromRequestContext(no precedent, fragile)PluginRegistryand using#[tool_router]standalone (loses composability with other plugins)Proposed Solution
Add an adapter type — e.g.
ServerHandlerPluginorToolRouterPlugin— that wraps anyServerHandler(produced by#[tool_router]) and implementsElicitPlugin:This would allow:
Notes
Peer<RoleServer>fromRequestContext<RoleServer>so it can be passed toelicit_checked()PluginRegistrycould be extended with aregister_handler(prefix, impl ServerHandler)method that wraps internally#[tool_router]-based server as a plugin alongside shadow crates likeelicit_reqwest