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}