mcp-server-sqlite 1.0.0

An MCP server for SQLite with fine-grained access control
Documentation
//! The `create_fts_index` tool: creates a full-text search virtual table backed
//! by FTS5 over columns of an existing table.

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,
)]
/// Create a full-text search index on an existing table using SQLite's FTS5
/// module. Specify the source table and the columns to index. The virtual table
/// is named `{table}_fts` by default and can be queried with the MATCH
/// operator.
pub struct CreateFtsIndexTool;

impl SqliteServerTool for CreateFtsIndexTool {
    const NAME: &str = "create_fts_index";

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

    type Input = CreateFtsIndexInput;
    type Output = CreateFtsIndexOutput;

    fn handle(
        ctx: &Self::Context,
        input: Self::Input,
    ) -> Result<Self::Output, Self::Error> {
        if input.columns.is_empty() {
            return Err(ToolError::Tool(CreateFtsIndexError::NoColumns));
        }

        let conn = ctx
            .connection()
            .map_err(|source| ToolError::Connection { source })?;

        let fts_table = input
            .fts_table_name
            .unwrap_or_else(|| format!("{}_fts", input.table_name));

        let columns = input.columns.join(", ");
        let sql = format!(
            "CREATE VIRTUAL TABLE [{}] USING fts5({}, content=[{}])",
            fts_table, columns, input.table_name,
        );

        conn.execute_batch(&sql).map_err(|source| {
            ToolError::Tool(CreateFtsIndexError::Create { source })
        })?;

        Ok(CreateFtsIndexOutput {
            fts_table_name: fts_table,
        })
    }
}

/// The input parameters for the `create_fts_index` tool.
#[derive(
    Clone,
    Debug,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Serialize,
    Deserialize,
    schemars::JsonSchema,
)]
pub struct CreateFtsIndexInput {
    /// The name of the existing table to index.
    #[schemars(description = "The source table to create an FTS index over")]
    pub table_name: String,
    /// The columns to include in the full-text index.
    #[schemars(description = "The columns to include in the full-text index")]
    pub columns: Vec<String>,
    /// Optional name for the FTS virtual table. Defaults to `{table_name}_fts`
    /// if not provided.
    #[schemars(
        description = "Optional name for the FTS virtual table (defaults to {table}_fts)"
    )]
    pub fts_table_name: Option<String>,
}

/// The result of creating an FTS index.
#[derive(
    Clone,
    Debug,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Serialize,
    Deserialize,
    schemars::JsonSchema,
)]
pub struct CreateFtsIndexOutput {
    /// The name of the created FTS virtual table.
    pub fts_table_name: String,
}

/// Errors specific to the `create_fts_index` tool.
#[derive(Debug, thiserror::Error)]
pub enum CreateFtsIndexError {
    /// No columns were specified for the full-text index.
    #[error("no columns specified for the FTS index")]
    NoColumns,
    /// SQLite failed to create the FTS virtual table.
    #[error("failed to create FTS index: {source}")]
    Create {
        /// The underlying rusqlite error.
        source: rusqlite::Error,
    },
}

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