Skip to content

feat: provide ServerHandler → ElicitPlugin adapter for composing Elicit-derived types into PluginRegistry #8

Description

@crumplecup

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:

  1. Writing a manual ElicitPlugin impl that bridges by extracting Peer from RequestContext (no precedent, fragile)
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions