Skip to main content

dbmcp_server/
tool.rs

1//! Declarative tool registry for per-backend MCP routers.
2//!
3//! A backend declares its tools as a `const` slice of [`ToolSpec`] rows, each
4//! row carrying the gating flags for that tool. [`ToolRouterExt::from_specs`]
5//! folds the slice into a [`ToolRouter`], skipping tools the current mode forbids.
6//! This keeps the read-only / pinned gating matrix as data, not control flow.
7
8use rmcp::handler::server::router::tool::{AsyncTool, ToolRouter};
9
10/// Declarative registration entry for one MCP tool.
11///
12/// Pairs the tool's router-registration function with its mode gates.
13pub struct ToolSpec<H: Send + Sync + 'static> {
14    /// Registers the tool on a router, returning the extended router.
15    register: fn(ToolRouter<H>) -> ToolRouter<H>,
16    /// Whether the tool registers only when a database name is pinned in config.
17    pinned: bool,
18    /// Whether the tool is hidden in read-only mode.
19    read_only: bool,
20}
21
22impl<H: Send + Sync + 'static> std::fmt::Debug for ToolSpec<H> {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.debug_struct("ToolSpec")
25            .field("pinned", &self.pinned)
26            .field("read_only", &self.read_only)
27            .finish_non_exhaustive()
28    }
29}
30
31impl<H: Send + Sync + 'static> ToolSpec<H> {
32    /// Creates a spec for an async tool `T` with its pinned and read-only gates.
33    #[must_use]
34    pub const fn async_tool<T: AsyncTool<H> + 'static>(pinned: bool, read_only: bool) -> Self {
35        Self {
36            register: ToolRouter::with_async_tool::<T>,
37            pinned,
38            read_only,
39        }
40    }
41}
42
43/// Extends [`ToolRouter`] with declarative construction from a [`ToolSpec`] table.
44pub trait ToolRouterExt<H: Send + Sync + 'static>: Sized {
45    /// Builds a router from `specs`, skipping mode-gated tools.
46    ///
47    /// A spec is skipped when its `read_only` gate coincides with `read_only`
48    /// mode, or its `pinned` gate disagrees with the current `pinned` mode.
49    #[must_use]
50    fn from_specs(specs: &[ToolSpec<H>], read_only: bool, pinned: bool) -> Self;
51}
52
53impl<H: Send + Sync + 'static> ToolRouterExt<H> for ToolRouter<H> {
54    fn from_specs(specs: &[ToolSpec<H>], read_only: bool, pinned: bool) -> Self {
55        specs
56            .iter()
57            .filter(|spec| (!spec.read_only || !read_only) && spec.pinned == pinned)
58            .fold(ToolRouter::new(), |router, spec| (spec.register)(router))
59    }
60}