mcp-server-sqlite 1.0.0

An MCP server for SQLite with fine-grained access control
Documentation
//! The `explain_query` tool: returns the EXPLAIN QUERY PLAN output for a SQL
//! statement, showing how SQLite will execute the query.

use rmcp::model::{Content, IntoContents};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use super::ToolError;
use crate::{mcp::McpServerSqlite, traits::SqliteServerTool};

#[derive(
    Clone,
    Copy,
    Debug,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Default,
    Serialize,
    Deserialize,
    JsonSchema,
)]
/// Show the query execution plan for a SQL statement. Returns the EXPLAIN QUERY
/// PLAN output showing how SQLite will scan tables, use indexes, and join data.
pub struct ExplainQueryTool;

impl SqliteServerTool for ExplainQueryTool {
    const NAME: &str = "explain_query";

    type Context = McpServerSqlite;
    type Error = ToolError<ExplainQueryError>;

    type Input = ExplainQueryInput;
    type Output = ExplainQueryOutput;

    fn handle(
        ctx: &Self::Context,
        input: Self::Input,
    ) -> Result<Self::Output, Self::Error> {
        let conn = ctx
            .connection()
            .map_err(|source| ToolError::Connection { source })?;

        let explain_sql = format!("EXPLAIN QUERY PLAN {}", input.query);

        let mut stmt =
            conn.prepare(&explain_sql).map_err(|source| {
                if matches!(
                    source,
                    rusqlite::Error::SqliteFailure(
                        rusqlite::ffi::Error {
                            code: rusqlite::ffi::ErrorCode::AuthorizationForStatementDenied,
                            ..
                        },
                        _,
                    )
                ) {
                    ToolError::AccessDenied {
                        message: format!(
                            "the configured access control policy \
                            denied this statement: {}",
                            input.query,
                        ),
                    }
                } else {
                    ToolError::Tool(ExplainQueryError::Query { source })
                }
            })?;

        let plan = stmt
            .query_map([], |row| {
                Ok(PlanNode {
                    id: row.get(0)?,
                    parent: row.get(1)?,
                    detail: row.get(3)?,
                })
            })
            .map_err(|source| {
                ToolError::Tool(ExplainQueryError::Query { source })
            })?
            .collect::<Result<Vec<_>, _>>()
            .map_err(|source| {
                ToolError::Tool(ExplainQueryError::Query { source })
            })?;

        Ok(ExplainQueryOutput { plan })
    }
}

/// The input parameters for the `explain_query` tool.
#[derive(
    Clone,
    Debug,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Serialize,
    Deserialize,
    schemars::JsonSchema,
)]
pub struct ExplainQueryInput {
    /// The SQL query to explain.
    #[schemars(description = "The SQL query to explain")]
    pub query: String,
}

/// The result of explaining a SQL query's execution plan.
#[derive(
    Clone,
    Debug,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Serialize,
    Deserialize,
    schemars::JsonSchema,
)]
pub struct ExplainQueryOutput {
    /// The nodes of the query plan tree. Each node describes one step in
    /// SQLite's execution strategy.
    pub plan: Vec<PlanNode>,
}

/// A single node in the EXPLAIN QUERY PLAN output tree.
#[derive(
    Clone,
    Debug,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Serialize,
    Deserialize,
    schemars::JsonSchema,
)]
pub struct PlanNode {
    /// The unique identifier for this plan node.
    pub id: i64,
    /// The identifier of this node's parent, or zero if this is a root node.
    pub parent: i64,
    /// A human-readable description of the operation performed at this step
    /// (e.g. "SCAN users", "SEARCH users USING INDEX idx_email").
    pub detail: String,
}

/// Errors specific to the `explain_query` tool.
#[derive(Debug, thiserror::Error)]
pub enum ExplainQueryError {
    /// The EXPLAIN QUERY PLAN statement failed. This usually means the
    /// underlying SQL is invalid or references objects that do not exist.
    #[error("failed to explain query: {source}")]
    Query {
        /// The underlying rusqlite error.
        source: rusqlite::Error,
    },
}

/// Converts the explain-query-specific error into MCP content by rendering the
/// display string as text.
impl IntoContents for ExplainQueryError {
    fn into_contents(self) -> Vec<Content> {
        vec![Content::text(self.to_string())]
    }
}