Skip to main content

mcp_server_sqlite/tools/
execute_tool.rs

1//! The `execute` tool: runs a SQL query against the database and returns typed
2//! results. This is the primary tool exposed by the MCP server.
3
4use rmcp::model::{Content, IntoContents};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use super::ToolError;
9use crate::{mcp::McpServerSqlite, traits::SqliteServerTool};
10
11#[derive(
12    Clone,
13    Copy,
14    Debug,
15    PartialEq,
16    Eq,
17    PartialOrd,
18    Ord,
19    Hash,
20    Default,
21    Serialize,
22    Deserialize,
23    JsonSchema,
24)]
25/// Marker type for the `execute` tool. Implements `SqliteServerTool` to wire up
26/// the tool's schema, handler, and routing. Stateless — all context comes from
27/// `McpServerSqlite`.
28pub struct ExecuteTool;
29
30impl SqliteServerTool for ExecuteTool {
31    const NAME: &str = "execute";
32
33    type Context = McpServerSqlite;
34    type Error = ToolError<ExecuteError>;
35
36    type Input = ExecuteInput;
37    type Output = ExecuteOutput;
38
39    fn handle(
40        ctx: &Self::Context,
41        input: Self::Input,
42    ) -> Result<Self::Output, Self::Error> {
43        let conn = ctx
44            .connection()
45            .map_err(|source| ToolError::Connection { source })?;
46
47        let mut stmt = conn.prepare(&input.query).map_err(|source| {
48            if matches!(
49                source,
50                rusqlite::Error::SqliteFailure(
51                    rusqlite::ffi::Error {
52                        code: rusqlite::ffi::ErrorCode::AuthorizationForStatementDenied,
53                        ..
54                    },
55                    _,
56                )
57            ) {
58                ToolError::AccessDenied {
59                    message: format!(
60                        "the configured access control policy denied this \
61                        statement: {}",
62                        input.query,
63                    ),
64                }
65            } else {
66                ToolError::Tool(ExecuteError::Prepare { source })
67            }
68        })?;
69
70        let column_count = stmt.column_count();
71        let column_names = (0..column_count)
72            .map(|i| stmt.column_name(i).unwrap_or("?").to_owned())
73            .collect::<Vec<String>>();
74
75        let rows = stmt
76            .query_map([], |row: &rusqlite::Row<'_>| {
77                let columns = column_names
78                    .iter()
79                    .enumerate()
80                    .map(|(i, name)| {
81                        let value = row
82                            .get::<_, rusqlite::types::Value>(i)
83                            .map(Value::from)
84                            .unwrap_or(Value::Null);
85                        (name.clone(), value)
86                    })
87                    .collect();
88                Ok(Row { columns })
89            })
90            .map_err(|source| ToolError::Tool(ExecuteError::Query { source }))?
91            .collect::<Result<Vec<_>, _>>()
92            .map_err(|source| {
93                ToolError::Tool(ExecuteError::Query { source })
94            })?;
95
96        let rows_changed = conn.changes();
97
98        Ok(ExecuteOutput { rows, rows_changed })
99    }
100}
101
102/// The input parameters for the `execute` tool.
103#[derive(
104    Clone,
105    Debug,
106    PartialEq,
107    Eq,
108    PartialOrd,
109    Ord,
110    Hash,
111    Serialize,
112    Deserialize,
113    schemars::JsonSchema,
114)]
115pub struct ExecuteInput {
116    /// The SQL query to execute against the database.
117    #[schemars(description = "The SQL query to execute")]
118    pub query: String,
119}
120
121/// The result of executing a SQL query.
122#[derive(
123    Clone,
124    Debug,
125    PartialEq,
126    PartialOrd,
127    Serialize,
128    Deserialize,
129    schemars::JsonSchema,
130)]
131pub struct ExecuteOutput {
132    /// The rows returned by the query. Empty for statements that do not produce
133    /// output (e.g. INSERT, CREATE TABLE).
134    pub rows: Vec<Row>,
135    /// The number of rows changed by the statement. Zero for queries that only
136    /// read data.
137    pub rows_changed: u64,
138}
139
140/// A single row returned from a query, represented as a mapping of column names
141/// to their typed values.
142#[derive(
143    Clone,
144    Debug,
145    PartialEq,
146    PartialOrd,
147    Serialize,
148    Deserialize,
149    schemars::JsonSchema,
150)]
151pub struct Row {
152    /// The column-value pairs for this row. Each key is the column name and
153    /// each value preserves the original SQLite type.
154    pub columns: std::collections::BTreeMap<String, Value>,
155}
156
157/// A dynamically-typed SQLite value preserving the original column type.
158/// Serialized as a tagged enum with `kind` and `value` fields so that consumers
159/// can distinguish between types unambiguously.
160#[serde_with::serde_as]
161#[derive(
162    Clone,
163    Debug,
164    PartialEq,
165    PartialOrd,
166    Serialize,
167    Deserialize,
168    schemars::JsonSchema,
169)]
170#[serde(tag = "kind", content = "value")]
171pub enum Value {
172    /// A SQL NULL.
173    Null,
174    /// A 64-bit signed integer.
175    Integer(i64),
176    /// A 64-bit IEEE 754 floating-point number.
177    Real(f64),
178    /// A UTF-8 string.
179    Text(String),
180    /// Raw binary data, hex-encoded for JSON transport.
181    Blob(
182        #[serde_as(as = "serde_with::hex::Hex")]
183        #[schemars(with = "String")]
184        Vec<u8>,
185    ),
186}
187
188/// Converts a rusqlite `Value` into this crate's `Value`, preserving the type
189/// tag for each SQLite storage class.
190impl From<rusqlite::types::Value> for Value {
191    fn from(value: rusqlite::types::Value) -> Self {
192        match value {
193            rusqlite::types::Value::Null => Self::Null,
194            rusqlite::types::Value::Integer(n) => Self::Integer(n),
195            rusqlite::types::Value::Real(f) => Self::Real(f),
196            rusqlite::types::Value::Text(s) => Self::Text(s),
197            rusqlite::types::Value::Blob(b) => Self::Blob(b),
198        }
199    }
200}
201
202/// Errors specific to the `execute` tool's query preparation and result reading
203/// logic.
204#[derive(Debug, thiserror::Error)]
205pub enum ExecuteError {
206    /// SQLite failed to prepare the statement. This usually means the SQL is
207    /// syntactically invalid or references a table/column that does not exist.
208    #[error("failed to prepare statement: {source}")]
209    Prepare {
210        /// The underlying rusqlite error.
211        source: rusqlite::Error,
212    },
213    /// The query executed but an error occurred while reading a result row.
214    #[error("failed to read query results: {source}")]
215    Query {
216        /// The underlying rusqlite error.
217        source: rusqlite::Error,
218    },
219}
220
221/// Converts the execute-specific error into MCP content by rendering the
222/// display string as text.
223impl IntoContents for ExecuteError {
224    fn into_contents(self) -> Vec<Content> {
225        vec![Content::text(self.to_string())]
226    }
227}