Skip to main content

mcp_server_sqlite/tools/
list_indexes_tool.rs

1//! The `list_indexes` tool: returns index metadata for the database. When given
2//! a table name, only indexes on that table are returned; otherwise all indexes
3//! are listed. Each entry includes the index name, owning table, uniqueness
4//! flag, column list, and partial-index predicate (if any).
5
6use rmcp::model::{Content, IntoContents};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10use super::ToolError;
11use crate::{mcp::McpServerSqlite, traits::SqliteServerTool};
12
13#[derive(
14    Clone,
15    Copy,
16    Debug,
17    PartialEq,
18    Eq,
19    PartialOrd,
20    Ord,
21    Hash,
22    Default,
23    Serialize,
24    Deserialize,
25    JsonSchema,
26)]
27/// List indexes in the database. When a table name is provided, only indexes
28/// belonging to that table are returned. Otherwise all indexes across every
29/// table are listed. Each index entry includes its name, the table it belongs
30/// to, whether it enforces uniqueness, the ordered list of indexed columns, and
31/// the WHERE predicate for partial indexes.
32pub struct ListIndexesTool;
33
34impl SqliteServerTool for ListIndexesTool {
35    const NAME: &str = "list_indexes";
36
37    type Context = McpServerSqlite;
38    type Error = ToolError<ListIndexesError>;
39
40    type Input = ListIndexesInput;
41    type Output = ListIndexesOutput;
42
43    fn handle(
44        ctx: &Self::Context,
45        input: Self::Input,
46    ) -> Result<Self::Output, Self::Error> {
47        let conn = ctx
48            .connection()
49            .map_err(|source| ToolError::Connection { source })?;
50
51        let raw_indexes = match &input.table_name {
52            Some(table) => {
53                let mut stmt = conn
54                    .prepare(
55                        "SELECT name, tbl_name, sql FROM sqlite_master \
56                         WHERE type = 'index' AND tbl_name = ? \
57                         ORDER BY tbl_name, name",
58                    )
59                    .map_err(|source| {
60                        ToolError::Tool(ListIndexesError::Query { source })
61                    })?;
62
63                stmt.query_map([table], |row| {
64                    Ok(RawIndex {
65                        name: row.get(0)?,
66                        table_name: row.get(1)?,
67                        sql: row.get(2)?,
68                    })
69                })
70                .map_err(|source| {
71                    ToolError::Tool(ListIndexesError::Query { source })
72                })?
73                .collect::<Result<Vec<_>, _>>()
74                .map_err(|source| {
75                    ToolError::Tool(ListIndexesError::Query { source })
76                })?
77            }
78            None => {
79                let mut stmt = conn
80                    .prepare(
81                        "SELECT name, tbl_name, sql FROM sqlite_master \
82                         WHERE type = 'index' \
83                         ORDER BY tbl_name, name",
84                    )
85                    .map_err(|source| {
86                        ToolError::Tool(ListIndexesError::Query { source })
87                    })?;
88
89                stmt.query_map([], |row| {
90                    Ok(RawIndex {
91                        name: row.get(0)?,
92                        table_name: row.get(1)?,
93                        sql: row.get(2)?,
94                    })
95                })
96                .map_err(|source| {
97                    ToolError::Tool(ListIndexesError::Query { source })
98                })?
99                .collect::<Result<Vec<_>, _>>()
100                .map_err(|source| {
101                    ToolError::Tool(ListIndexesError::Query { source })
102                })?
103            }
104        };
105
106        let indexes = raw_indexes
107            .into_iter()
108            .map(|raw| {
109                let columns = query_index_columns(&conn, &raw.name)?;
110                let unique = is_unique_index(&raw);
111                let partial_predicate =
112                    raw.sql.as_deref().and_then(extract_where_clause);
113
114                Ok(IndexInfo {
115                    name: raw.name,
116                    table_name: raw.table_name,
117                    unique,
118                    columns,
119                    partial_predicate,
120                })
121            })
122            .collect::<Result<Vec<_>, ToolError<ListIndexesError>>>()?;
123
124        Ok(ListIndexesOutput { indexes })
125    }
126}
127
128/// Transient holder for the raw columns returned by the `sqlite_master` query
129/// before enrichment with column names and uniqueness.
130struct RawIndex {
131    /// The index name from `sqlite_master.name`.
132    name: String,
133    /// The table this index belongs to.
134    table_name: String,
135    /// The `CREATE INDEX` SQL, or `None` for auto-generated indexes (e.g. from
136    /// PRIMARY KEY or UNIQUE constraints).
137    sql: Option<String>,
138}
139
140/// Queries `PRAGMA index_info` for the given index and returns the ordered list
141/// of column names.
142fn query_index_columns(
143    conn: &rusqlite::Connection,
144    index_name: &str,
145) -> Result<Vec<String>, ToolError<ListIndexesError>> {
146    let pragma = format!("PRAGMA index_info('{index_name}')");
147    let mut stmt = conn.prepare(&pragma).map_err(|source| {
148        ToolError::Tool(ListIndexesError::Query { source })
149    })?;
150
151    stmt.query_map([], |row| row.get::<_, String>(2))
152        .map_err(|source| ToolError::Tool(ListIndexesError::Query { source }))?
153        .collect::<Result<Vec<_>, _>>()
154        .map_err(|source| ToolError::Tool(ListIndexesError::Query { source }))
155}
156
157/// Determines whether an index enforces uniqueness. Indexes created with
158/// `CREATE UNIQUE INDEX` will contain "UNIQUE" in their DDL. Auto-indexes
159/// generated by PRIMARY KEY or UNIQUE constraints have no DDL but use the
160/// `sqlite_autoindex_` name prefix, so they are treated as unique.
161fn is_unique_index(raw: &RawIndex) -> bool {
162    match &raw.sql {
163        Some(sql) => sql.to_uppercase().contains("UNIQUE"),
164        None => raw.name.starts_with("sqlite_autoindex_"),
165    }
166}
167
168/// Extracts the WHERE-clause predicate from a `CREATE INDEX` SQL statement,
169/// returning the text after the last top-level `WHERE` keyword. Returns `None`
170/// if the statement has no WHERE clause.
171fn extract_where_clause(sql: &str) -> Option<String> {
172    let upper = sql.to_uppercase();
173    let where_pos = upper.rfind(" WHERE ")?;
174    let predicate = sql[where_pos + " WHERE ".len()..].trim();
175    if predicate.is_empty() {
176        return None;
177    }
178    Some(predicate.to_owned())
179}
180
181/// The input parameters for the `list_indexes` tool.
182#[derive(
183    Clone,
184    Debug,
185    Default,
186    PartialEq,
187    Eq,
188    PartialOrd,
189    Ord,
190    Hash,
191    Serialize,
192    Deserialize,
193    schemars::JsonSchema,
194)]
195pub struct ListIndexesInput {
196    /// If provided, only indexes on this table are returned. Omit to list
197    /// indexes across all tables.
198    #[schemars(
199        description = "Filter indexes to this table. Omit for all tables."
200    )]
201    pub table_name: Option<String>,
202}
203
204/// The result of listing indexes in the database.
205#[derive(
206    Clone,
207    Debug,
208    PartialEq,
209    Eq,
210    PartialOrd,
211    Ord,
212    Hash,
213    Serialize,
214    Deserialize,
215    schemars::JsonSchema,
216)]
217pub struct ListIndexesOutput {
218    /// The indexes found, each with its metadata and column list.
219    pub indexes: Vec<IndexInfo>,
220}
221
222/// Metadata for a single index in the database.
223#[derive(
224    Clone,
225    Debug,
226    PartialEq,
227    Eq,
228    PartialOrd,
229    Ord,
230    Hash,
231    Serialize,
232    Deserialize,
233    schemars::JsonSchema,
234)]
235pub struct IndexInfo {
236    /// The index name as recorded in `sqlite_master`.
237    pub name: String,
238    /// The table this index belongs to.
239    pub table_name: String,
240    /// Whether this index enforces a uniqueness constraint.
241    pub unique: bool,
242    /// The ordered list of column names covered by this index.
243    pub columns: Vec<String>,
244    /// The WHERE-clause predicate for partial indexes, or `None` for full
245    /// indexes.
246    pub partial_predicate: Option<String>,
247}
248
249/// Errors specific to the `list_indexes` tool.
250#[derive(Debug, thiserror::Error)]
251pub enum ListIndexesError {
252    /// Failed to query index metadata from `sqlite_master` or `PRAGMA
253    /// index_info`.
254    #[error("failed to list indexes: {source}")]
255    Query {
256        /// The underlying rusqlite error.
257        source: rusqlite::Error,
258    },
259}
260
261/// Converts the list-indexes-specific error into MCP content by rendering the
262/// display string as text.
263impl IntoContents for ListIndexesError {
264    fn into_contents(self) -> Vec<Content> {
265        vec![Content::text(self.to_string())]
266    }
267}