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,
)]
pub struct ListIndexesTool;
impl SqliteServerTool for ListIndexesTool {
const NAME: &str = "list_indexes";
type Context = McpServerSqlite;
type Error = ToolError<ListIndexesError>;
type Input = ListIndexesInput;
type Output = ListIndexesOutput;
fn handle(
ctx: &Self::Context,
input: Self::Input,
) -> Result<Self::Output, Self::Error> {
let conn = ctx
.connection()
.map_err(|source| ToolError::Connection { source })?;
let raw_indexes = match &input.table_name {
Some(table) => {
let mut stmt = conn
.prepare(
"SELECT name, tbl_name, sql FROM sqlite_master \
WHERE type = 'index' AND tbl_name = ? \
ORDER BY tbl_name, name",
)
.map_err(|source| {
ToolError::Tool(ListIndexesError::Query { source })
})?;
stmt.query_map([table], |row| {
Ok(RawIndex {
name: row.get(0)?,
table_name: row.get(1)?,
sql: row.get(2)?,
})
})
.map_err(|source| {
ToolError::Tool(ListIndexesError::Query { source })
})?
.collect::<Result<Vec<_>, _>>()
.map_err(|source| {
ToolError::Tool(ListIndexesError::Query { source })
})?
}
None => {
let mut stmt = conn
.prepare(
"SELECT name, tbl_name, sql FROM sqlite_master \
WHERE type = 'index' \
ORDER BY tbl_name, name",
)
.map_err(|source| {
ToolError::Tool(ListIndexesError::Query { source })
})?;
stmt.query_map([], |row| {
Ok(RawIndex {
name: row.get(0)?,
table_name: row.get(1)?,
sql: row.get(2)?,
})
})
.map_err(|source| {
ToolError::Tool(ListIndexesError::Query { source })
})?
.collect::<Result<Vec<_>, _>>()
.map_err(|source| {
ToolError::Tool(ListIndexesError::Query { source })
})?
}
};
let indexes = raw_indexes
.into_iter()
.map(|raw| {
let columns = query_index_columns(&conn, &raw.name)?;
let unique = is_unique_index(&raw);
let partial_predicate =
raw.sql.as_deref().and_then(extract_where_clause);
Ok(IndexInfo {
name: raw.name,
table_name: raw.table_name,
unique,
columns,
partial_predicate,
})
})
.collect::<Result<Vec<_>, ToolError<ListIndexesError>>>()?;
Ok(ListIndexesOutput { indexes })
}
}
struct RawIndex {
name: String,
table_name: String,
sql: Option<String>,
}
fn query_index_columns(
conn: &rusqlite::Connection,
index_name: &str,
) -> Result<Vec<String>, ToolError<ListIndexesError>> {
let pragma = format!("PRAGMA index_info('{index_name}')");
let mut stmt = conn.prepare(&pragma).map_err(|source| {
ToolError::Tool(ListIndexesError::Query { source })
})?;
stmt.query_map([], |row| row.get::<_, String>(2))
.map_err(|source| ToolError::Tool(ListIndexesError::Query { source }))?
.collect::<Result<Vec<_>, _>>()
.map_err(|source| ToolError::Tool(ListIndexesError::Query { source }))
}
fn is_unique_index(raw: &RawIndex) -> bool {
match &raw.sql {
Some(sql) => sql.to_uppercase().contains("UNIQUE"),
None => raw.name.starts_with("sqlite_autoindex_"),
}
}
fn extract_where_clause(sql: &str) -> Option<String> {
let upper = sql.to_uppercase();
let where_pos = upper.rfind(" WHERE ")?;
let predicate = sql[where_pos + " WHERE ".len()..].trim();
if predicate.is_empty() {
return None;
}
Some(predicate.to_owned())
}
#[derive(
Clone,
Debug,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
pub struct ListIndexesInput {
#[schemars(
description = "Filter indexes to this table. Omit for all tables."
)]
pub table_name: Option<String>,
}
#[derive(
Clone,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
pub struct ListIndexesOutput {
pub indexes: Vec<IndexInfo>,
}
#[derive(
Clone,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
pub struct IndexInfo {
pub name: String,
pub table_name: String,
pub unique: bool,
pub columns: Vec<String>,
pub partial_predicate: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum ListIndexesError {
#[error("failed to list indexes: {source}")]
Query {
source: rusqlite::Error,
},
}
impl IntoContents for ListIndexesError {
fn into_contents(self) -> Vec<Content> {
vec![Content::text(self.to_string())]
}
}