1use 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)]
27pub 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
128struct RawIndex {
131 name: String,
133 table_name: String,
135 sql: Option<String>,
138}
139
140fn 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
157fn 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
168fn 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#[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 #[schemars(
199 description = "Filter indexes to this table. Omit for all tables."
200 )]
201 pub table_name: Option<String>,
202}
203
204#[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 pub indexes: Vec<IndexInfo>,
220}
221
222#[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 pub name: String,
238 pub table_name: String,
240 pub unique: bool,
242 pub columns: Vec<String>,
244 pub partial_predicate: Option<String>,
247}
248
249#[derive(Debug, thiserror::Error)]
251pub enum ListIndexesError {
252 #[error("failed to list indexes: {source}")]
255 Query {
256 source: rusqlite::Error,
258 },
259}
260
261impl IntoContents for ListIndexesError {
264 fn into_contents(self) -> Vec<Content> {
265 vec![Content::text(self.to_string())]
266 }
267}