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}