Skip to main content

mcp_server_sqlite/
traits.rs

1//! Trait definitions for MCP server tools. The `SqliteServerTool` trait
2//! provides a structured way to define tools with typed input, output, and
3//! error types. Implementors declare their schema statically and provide a
4//! synchronous handler; the trait takes care of generating the rmcp `Tool`
5//! definition, JSON schemas, and the routing closure.
6
7use rmcp::{
8    Json,
9    handler::server::{
10        tool::{schema_for_output, schema_for_type},
11        wrapper::Parameters,
12    },
13    model::IntoContents,
14};
15use schemars::{JsonSchema, schema_for};
16use serde::{Deserialize, Serialize};
17
18/// A statically-typed MCP tool backed by a SQLite server. Each implementor
19/// represents a single tool exposed to MCP clients. The trait derives the
20/// tool's JSON schema from its associated types and wires up a handler closure
21/// compatible with the rmcp `ToolRouter`.
22pub trait SqliteServerTool: JsonSchema {
23    /// The tool name advertised to MCP clients (e.g. `"execute"`).
24    const NAME: &str;
25
26    /// The server type passed to the handler on each invocation, typically
27    /// `McpServerSqlite`.
28    type Context;
29    /// The error type returned by the handler. Must implement `IntoContents` so
30    /// rmcp can render it as error content.
31    type Error: IntoContents;
32
33    /// The deserialized input parameters received from the MCP client.
34    type Input: Serialize + for<'de> Deserialize<'de> + JsonSchema + 'static;
35    /// The structured output returned to the MCP client on success.
36    type Output: Serialize + for<'de> Deserialize<'de> + JsonSchema + 'static;
37
38    /// Executes the tool's logic against the given context and input. Called
39    /// synchronously on the tokio blocking thread pool.
40    fn handle(
41        ctx: &Self::Context,
42        input: Self::Input,
43    ) -> Result<Self::Output, Self::Error>;
44
45    /// Builds the rmcp `Tool` definition for this tool, including its name,
46    /// description (extracted from the `JsonSchema` derive), input schema, and
47    /// output schema.
48    fn tool() -> rmcp::model::Tool {
49        let description = schema_for!(Self)
50            .get("description")
51            .and_then(|value| value.as_str().map(|str| str.to_owned()))
52            .unwrap_or_default();
53        let input_schema = schema_for_type::<Parameters<Self::Input>>();
54        let output_schema = schema_for_output::<Self::Output>()
55            .expect("Invalid output type schema");
56
57        rmcp::model::Tool::new_with_raw(
58            Self::NAME,
59            Some(description.into()),
60            input_schema,
61        )
62        .with_raw_output_schema(output_schema)
63    }
64
65    /// Returns a closure that deserializes the input parameters, calls
66    /// `handle`, and wraps the output in `Json` for structured MCP responses.
67    /// Every invocation is automatically traced with the tool name, duration,
68    /// and outcome. The closure is compatible with `ToolRouter::with_route`.
69    #[allow(clippy::type_complexity)]
70    fn handler_func() -> HandlerFuncFor<Self> {
71        |ctx, parameters| {
72            let span = tracing::info_span!("tool_call", tool = Self::NAME);
73            let _guard = span.enter();
74
75            if tracing::enabled!(tracing::Level::DEBUG) {
76                let input_json =
77                    serde_json::to_string(&parameters.0).unwrap_or_default();
78                tracing::debug!(input = %input_json, "request");
79            }
80
81            let start = std::time::Instant::now();
82            let result = Self::handle(ctx, parameters.0);
83            let elapsed = start.elapsed();
84
85            match &result {
86                Ok(output) => {
87                    if tracing::enabled!(tracing::Level::DEBUG) {
88                        let output_json =
89                            serde_json::to_string(output).unwrap_or_default();
90                        tracing::debug!(output = %output_json, "response");
91                    }
92                    tracing::info!(elapsed_ms = elapsed.as_millis(), "ok");
93                }
94                Err(_) => {
95                    tracing::warn!(elapsed_ms = elapsed.as_millis(), "error");
96                }
97            }
98
99            result.map(Json)
100        }
101    }
102}
103
104type HandlerFuncFor<T> = fn(
105    &<T as SqliteServerTool>::Context,
106    Parameters<<T as SqliteServerTool>::Input>,
107) -> Result<
108    Json<<T as SqliteServerTool>::Output>,
109    <T as SqliteServerTool>::Error,
110>;