Skip to main content

dbmcp_server/
server.rs

1//! Shared MCP server wrapper and metadata.
2//!
3//! Provides the cloneable, type-erased [`Server`] consumed by the
4//! stdio and HTTP transports, plus [`server_info`] used by per-backend
5//! [`ServerHandler`] implementations.
6
7use std::fmt;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use rmcp::model::{
12    CallToolRequestParams, CallToolResponse, Implementation, ListToolsResult, PaginatedRequestParams,
13    ServerCapabilities, ServerConfig, Tool,
14};
15use rmcp::service::RequestContext;
16use rmcp::{ErrorData, RoleServer, ServerHandler};
17
18use crate::tool::ErrorContext;
19
20/// Hardcoded product name matching the root binary crate.
21const NAME: &str = "dbmcp";
22
23/// The current version, derived from the workspace `Cargo.toml` at compile time.
24const VERSION: &str = env!("CARGO_PKG_VERSION");
25
26/// Human-readable title for the MCP server.
27const TITLE: &str = "Database MCP Server";
28
29/// Website URL, derived from the workspace `Cargo.toml` at compile time.
30const HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
31
32/// Cloneable, type-erased MCP server.
33///
34/// Wraps any backend adapter behind an [`Arc`] using the private
35/// [`DynHandler`] trait for type erasure. All database backends produce the same concrete
36/// type, eliminating the need for enum dispatch. The same `Arc` is held
37/// a second time as a [`Shutdown`] view so transports can drain pools
38/// via [`Server::shutdown`].
39#[derive(Clone)]
40pub struct Server {
41    handler: Arc<dyn DynHandler>,
42    shutdown: Arc<dyn Shutdown>,
43    error_context: Option<ErrorContext>,
44}
45
46/// Boxed `Send` future returned by [`DynHandler`] request methods.
47type HandlerFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, ErrorData>> + Send + 'a>>;
48
49/// Object-safe view of the [`ServerHandler`] methods backends override.
50///
51/// [`ServerHandler`] returns `impl Future`, so it cannot be a trait object.
52/// Every method not listed here keeps rmcp's default on [`Server`], which
53/// matches what each backend inherits today.
54trait DynHandler: Send + Sync {
55    fn get_info(&self) -> ServerConfig;
56
57    fn call_tool(
58        &self,
59        request: CallToolRequestParams,
60        context: RequestContext<RoleServer>,
61    ) -> HandlerFuture<'_, CallToolResponse>;
62
63    fn list_tools(
64        &self,
65        request: Option<PaginatedRequestParams>,
66        context: RequestContext<RoleServer>,
67    ) -> HandlerFuture<'_, ListToolsResult>;
68
69    fn get_tool(&self, name: &str) -> Option<Tool>;
70}
71
72impl<H: ServerHandler> DynHandler for H {
73    fn get_info(&self) -> ServerConfig {
74        ServerHandler::get_info(self)
75    }
76
77    fn call_tool(
78        &self,
79        request: CallToolRequestParams,
80        context: RequestContext<RoleServer>,
81    ) -> HandlerFuture<'_, CallToolResponse> {
82        Box::pin(ServerHandler::call_tool(self, request, context))
83    }
84
85    fn list_tools(
86        &self,
87        request: Option<PaginatedRequestParams>,
88        context: RequestContext<RoleServer>,
89    ) -> HandlerFuture<'_, ListToolsResult> {
90        Box::pin(ServerHandler::list_tools(self, request, context))
91    }
92
93    fn get_tool(&self, name: &str) -> Option<Tool> {
94        ServerHandler::get_tool(self, name)
95    }
96}
97
98/// Boxed `Send` future returned by [`Shutdown::shutdown`].
99pub type ShutdownFuture<'a> = Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
100
101/// Closes every live connection pool the server owns.
102///
103/// Implemented by each backend handler so transports can drain pools
104/// gracefully on shutdown, separate from the type-erased [`Server`].
105pub trait Shutdown: Send + Sync {
106    /// Awaits a clean close of all pools. Idempotent.
107    fn shutdown(&self) -> ShutdownFuture<'_>;
108}
109
110impl fmt::Debug for Server {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        f.debug_struct("Server").finish_non_exhaustive()
113    }
114}
115
116impl Server {
117    /// Creates a new server from any backend adapter.
118    ///
119    /// One [`Arc`] is shared as two trait-object views — [`DynHandler`]
120    /// for request handling and [`Shutdown`] for pool draining — so the
121    /// adapter itself is never cloned.
122    pub fn new<S>(handler: S) -> Self
123    where
124        S: ServerHandler + Shutdown,
125    {
126        let handler = Arc::new(handler);
127        Self {
128            handler: handler.clone(),
129            shutdown: handler,
130            error_context: None,
131        }
132    }
133
134    /// Appends `context` to tool error messages it selects, before redaction.
135    #[must_use]
136    pub fn with_error_context(mut self, context: ErrorContext) -> Self {
137        self.error_context = Some(context);
138        self
139    }
140
141    /// Closes every live connection pool. Idempotent.
142    pub async fn shutdown(&self) {
143        self.shutdown.shutdown().await;
144    }
145}
146
147impl ServerHandler for Server {
148    fn get_info(&self) -> ServerConfig {
149        self.handler.get_info()
150    }
151
152    async fn call_tool(
153        &self,
154        request: CallToolRequestParams,
155        mut context: RequestContext<RoleServer>,
156    ) -> Result<CallToolResponse, ErrorData> {
157        if let Some(error_context) = &self.error_context {
158            context.extensions.insert(error_context.clone());
159        }
160        self.handler.call_tool(request, context).await
161    }
162
163    /// Lists the backend's tools without cache hints.
164    ///
165    /// rmcp's `tool_handler` default advertises `cacheScope: public` for a
166    /// tool list that depends on server config. Clearing the hints lets rmcp
167    /// fill the conservative `ttlMs: 0` / `cacheScope: private` instead.
168    async fn list_tools(
169        &self,
170        request: Option<PaginatedRequestParams>,
171        context: RequestContext<RoleServer>,
172    ) -> Result<ListToolsResult, ErrorData> {
173        let mut result = self.handler.list_tools(request, context).await?;
174        result.ttl_ms = None;
175        result.cache_scope = None;
176        Ok(result)
177    }
178
179    fn get_tool(&self, name: &str) -> Option<Tool> {
180        self.handler.get_tool(name)
181    }
182}
183
184/// Returns the shared [`ServerConfig`] for all server implementations.
185///
186/// Builds base [`Implementation`] metadata (name, version, title, URL).
187/// Backend handlers extend this with a backend-specific description
188/// and instructions via the public fields on [`ServerConfig`].
189#[must_use]
190pub fn server_info() -> ServerConfig {
191    let capabilities = ServerCapabilities::builder().enable_tools().build();
192
193    let server_info = Implementation::new(NAME, VERSION)
194        .with_title(TITLE)
195        .with_website_url(HOMEPAGE);
196
197    ServerConfig::new(capabilities).with_server_info(server_info)
198}