mcp-server-sqlite 1.0.0

An MCP server for SQLite with fine-grained access control
Documentation
//! Trait definitions for MCP server tools. The `SqliteServerTool` trait
//! provides a structured way to define tools with typed input, output, and
//! error types. Implementors declare their schema statically and provide a
//! synchronous handler; the trait takes care of generating the rmcp `Tool`
//! definition, JSON schemas, and the routing closure.

use rmcp::{
    Json,
    handler::server::{
        tool::{schema_for_output, schema_for_type},
        wrapper::Parameters,
    },
    model::IntoContents,
};
use schemars::{JsonSchema, schema_for};
use serde::{Deserialize, Serialize};

/// A statically-typed MCP tool backed by a SQLite server. Each implementor
/// represents a single tool exposed to MCP clients. The trait derives the
/// tool's JSON schema from its associated types and wires up a handler closure
/// compatible with the rmcp `ToolRouter`.
pub trait SqliteServerTool: JsonSchema {
    /// The tool name advertised to MCP clients (e.g. `"execute"`).
    const NAME: &str;

    /// The server type passed to the handler on each invocation, typically
    /// `McpServerSqlite`.
    type Context;
    /// The error type returned by the handler. Must implement `IntoContents` so
    /// rmcp can render it as error content.
    type Error: IntoContents;

    /// The deserialized input parameters received from the MCP client.
    type Input: Serialize + for<'de> Deserialize<'de> + JsonSchema + 'static;
    /// The structured output returned to the MCP client on success.
    type Output: Serialize + for<'de> Deserialize<'de> + JsonSchema + 'static;

    /// Executes the tool's logic against the given context and input. Called
    /// synchronously on the tokio blocking thread pool.
    fn handle(
        ctx: &Self::Context,
        input: Self::Input,
    ) -> Result<Self::Output, Self::Error>;

    /// Builds the rmcp `Tool` definition for this tool, including its name,
    /// description (extracted from the `JsonSchema` derive), input schema, and
    /// output schema.
    fn tool() -> rmcp::model::Tool {
        let description = schema_for!(Self)
            .get("description")
            .and_then(|value| value.as_str().map(|str| str.to_owned()))
            .unwrap_or_default();
        let input_schema = schema_for_type::<Parameters<Self::Input>>();
        let output_schema = schema_for_output::<Self::Output>()
            .expect("Invalid output type schema");

        rmcp::model::Tool::new_with_raw(
            Self::NAME,
            Some(description.into()),
            input_schema,
        )
        .with_raw_output_schema(output_schema)
    }

    /// Returns a closure that deserializes the input parameters, calls
    /// `handle`, and wraps the output in `Json` for structured MCP responses.
    /// Every invocation is automatically traced with the tool name, duration,
    /// and outcome. The closure is compatible with `ToolRouter::with_route`.
    #[allow(clippy::type_complexity)]
    fn handler_func() -> HandlerFuncFor<Self> {
        |ctx, parameters| {
            let span = tracing::info_span!("tool_call", tool = Self::NAME);
            let _guard = span.enter();

            if tracing::enabled!(tracing::Level::DEBUG) {
                let input_json =
                    serde_json::to_string(&parameters.0).unwrap_or_default();
                tracing::debug!(input = %input_json, "request");
            }

            let start = std::time::Instant::now();
            let result = Self::handle(ctx, parameters.0);
            let elapsed = start.elapsed();

            match &result {
                Ok(output) => {
                    if tracing::enabled!(tracing::Level::DEBUG) {
                        let output_json =
                            serde_json::to_string(output).unwrap_or_default();
                        tracing::debug!(output = %output_json, "response");
                    }
                    tracing::info!(elapsed_ms = elapsed.as_millis(), "ok");
                }
                Err(_) => {
                    tracing::warn!(elapsed_ms = elapsed.as_millis(), "error");
                }
            }

            result.map(Json)
        }
    }
}

type HandlerFuncFor<T> = fn(
    &<T as SqliteServerTool>::Context,
    Parameters<<T as SqliteServerTool>::Input>,
) -> Result<
    Json<<T as SqliteServerTool>::Output>,
    <T as SqliteServerTool>::Error,
>;