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(¶meters.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>;