Skip to main content

elicitation/plugin/
mod.rs

1//! Type-erased plugin interface for the elicitation tool registry.
2//!
3//! Each shadow crate (e.g., `elicit_reqwest`) provides a `Plugin` struct that
4//! implements [`ElicitPlugin`]. The [`PluginRegistry`](crate::PluginRegistry)
5//! collects these and serves them as a single MCP server.
6//!
7//! # Implementing a plugin
8//!
9//! **Simple path** — implement [`DescriptorPlugin`] and expose a slice of
10//! [`ToolDescriptor`]s built with [`make_descriptor`].  The blanket impl
11//! provides [`ElicitPlugin`] for free.
12//!
13//! **Full control** — implement [`ElicitPlugin`] directly.
14
15pub mod context;
16pub mod descriptor;
17pub mod descriptor_plugin;
18
19pub use context::{NoContext, PluginContext};
20pub use descriptor::{
21    PluginToolRegistration, ToolDescriptor, make_descriptor, make_descriptor_ctx,
22};
23pub use descriptor_plugin::DescriptorPlugin;
24// StatefulPlugin is defined in this module; re-exported at crate level from lib.rs.
25
26use std::borrow::Cow;
27use std::sync::Arc;
28
29use futures::future::BoxFuture;
30use rmcp::{
31    ErrorData,
32    model::{CallToolRequestParams, CallToolResult, Tool},
33    service::RequestContext,
34};
35
36use crate::rmcp::RoleServer;
37
38/// Type-erased interface for a shadow-crate tool plugin.
39///
40/// # Object Safety
41///
42/// This trait is object-safe: all async methods return `BoxFuture`.
43///
44/// Prefer implementing [`DescriptorPlugin`] over this trait directly unless
45/// you need custom dispatch logic.
46pub trait ElicitPlugin: Send + Sync + 'static {
47    /// Human-readable plugin name, used as the namespace prefix.
48    ///
49    /// E.g. `"http"` produces tools named `http__get`, `http__post`, etc.
50    fn name(&self) -> &'static str;
51
52    /// List all tools provided by this plugin (without namespace prefix).
53    fn list_tools(&self) -> Vec<Tool>;
54
55    /// Dispatch a tool call to this plugin.
56    ///
57    /// `params.name` will already have the namespace prefix stripped by
58    /// `PluginRegistry` before this is called.
59    fn call_tool<'a>(
60        &'a self,
61        params: CallToolRequestParams,
62        ctx: RequestContext<RoleServer>,
63    ) -> BoxFuture<'a, Result<CallToolResult, ErrorData>>;
64}
65
66/// A stateful plugin with a typed context.
67///
68/// Implement this trait (instead of [`ElicitPlugin`] directly) when your
69/// plugin needs server-side state — a DB pool, HTTP client, etc.  The context
70/// lives in an `Arc<Self::Context>` that is cloned into each tool handler.
71///
72/// A blanket impl of [`ElicitPlugin`] is provided for all `StatefulPlugin`
73/// types.  The context is type-erased at dispatch time using `Arc<dyn Any>`.
74///
75/// # Example
76///
77/// ```rust,no_run
78/// # use std::sync::Arc;
79/// # use elicitation::plugin::{StatefulPlugin, PluginContext, ToolDescriptor};
80/// # use rmcp::model::Tool;
81/// pub struct MyCtx { pub client: String }
82/// impl PluginContext for MyCtx {}
83///
84/// pub struct MyPlugin(Arc<MyCtx>);
85///
86/// impl StatefulPlugin for MyPlugin {
87///     type Context = MyCtx;
88///     fn name(&self) -> &'static str { "my" }
89///     fn list_tools(&self) -> Vec<Tool> { vec![] }
90///     fn tool_descriptors(&self) -> Vec<ToolDescriptor> { vec![] }
91///     fn context(&self) -> Arc<MyCtx> { self.0.clone() }
92/// }
93/// ```
94pub trait StatefulPlugin: Send + Sync + 'static {
95    /// The plugin's context type.
96    type Context: PluginContext;
97
98    /// Human-readable plugin name (namespace prefix).
99    fn name(&self) -> &'static str;
100
101    /// All tools provided by this plugin (MCP schema layer).
102    fn list_tools(&self) -> Vec<Tool>;
103
104    /// All descriptors provided by this plugin (handler layer).
105    ///
106    /// Named `tool_descriptors` to avoid collision with [`DescriptorPlugin::descriptors`].
107    fn tool_descriptors(&self) -> Vec<ToolDescriptor>;
108
109    /// Return a clone of the shared context `Arc`.
110    fn context(&self) -> Arc<Self::Context>;
111}
112
113impl<P: StatefulPlugin> ElicitPlugin for P {
114    fn name(&self) -> &'static str {
115        StatefulPlugin::name(self)
116    }
117
118    fn list_tools(&self) -> Vec<Tool> {
119        StatefulPlugin::list_tools(self)
120    }
121
122    fn call_tool<'a>(
123        &'a self,
124        params: CallToolRequestParams,
125        _ctx: RequestContext<RoleServer>,
126    ) -> BoxFuture<'a, Result<CallToolResult, ErrorData>> {
127        let bare = params
128            .name
129            .strip_prefix(&format!("{name}__", name = self.name()))
130            .map(|s| s.to_owned())
131            .unwrap_or_else(|| params.name.to_string());
132
133        let ctx: Arc<dyn std::any::Any + Send + Sync> = self.context();
134        let descriptors = self.tool_descriptors();
135
136        Box::pin(async move {
137            match descriptors.iter().find(|d| d.name == bare.as_str()) {
138                Some(descriptor) => descriptor.dispatch(ctx, params).await,
139                None => Err(ErrorData::invalid_params(
140                    format!("unknown tool: {bare}"),
141                    None,
142                )),
143            }
144        })
145    }
146}
147
148/// A type-erased, cheaply-cloneable plugin reference.
149pub type ArcPlugin = Arc<dyn ElicitPlugin>;
150///
151/// `"http"` + `"get"` → `"http__get"`.
152pub(crate) fn prefixed_name(prefix: &str, name: &str) -> Cow<'static, str> {
153    Cow::Owned(format!("{prefix}__{name}"))
154}
155
156/// Strip the namespace prefix from a tool name, returning the bare name.
157///
158/// `"http__get"` with prefix `"http"` → `"get"`.
159/// Returns `None` if the name does not start with `{prefix}__`.
160pub(crate) fn strip_prefix<'a>(prefix: &str, name: &'a str) -> Option<&'a str> {
161    let sep = format!("{prefix}__");
162    name.strip_prefix(sep.as_str())
163}