mcp-server-sqlite 1.0.0

An MCP server for SQLite with fine-grained access control
Documentation
//! The `list_foreign_keys` tool: returns all foreign key constraints defined on
//! a given table. Each row from SQLite's `PRAGMA foreign_key_list` becomes one
//! entry in the result, so composite foreign keys appear as multiple entries
//! sharing the same `id`.

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,
)]
/// List all foreign key constraints for a given table. Returns one entry per
/// column mapping, including the referenced table, the local and remote
/// columns, and the ON UPDATE / ON DELETE actions. Composite foreign keys
/// produce multiple entries that share the same id.
pub struct ListForeignKeysTool;

impl SqliteServerTool for ListForeignKeysTool {
    const NAME: &str = "list_foreign_keys";

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

    type Input = ListForeignKeysInput;
    type Output = ListForeignKeysOutput;

    fn handle(
        ctx: &Self::Context,
        input: Self::Input,
    ) -> Result<Self::Output, Self::Error> {
        let conn = ctx
            .connection()
            .map_err(|source| ToolError::Connection { source })?;

        let pragma_sql = format!(
            "PRAGMA foreign_key_list({})",
            enquote_identifier(&input.table_name),
        );

        let mut stmt = conn.prepare(&pragma_sql).map_err(|source| {
            ToolError::Tool(ListForeignKeysError::Query { source })
        })?;

        let foreign_keys = stmt
            .query_map([], |row| {
                Ok(ForeignKeyInfo {
                    id: row.get(0)?,
                    from_column: row.get(3)?,
                    to_table: row.get(2)?,
                    to_column: row.get(4)?,
                    on_update: row.get(5)?,
                    on_delete: row.get(6)?,
                })
            })
            .map_err(|source| {
                ToolError::Tool(ListForeignKeysError::Query { source })
            })?
            .collect::<Result<Vec<_>, _>>()
            .map_err(|source| {
                ToolError::Tool(ListForeignKeysError::Query { source })
            })?;

        Ok(ListForeignKeysOutput { foreign_keys })
    }
}

/// Wraps `name` in double-quotes and escapes any embedded double-quote
/// characters, producing a safe SQLite identifier for use in PRAGMA statements.
fn enquote_identifier(name: &str) -> String {
    format!("\"{}\"", name.replace('"', "\"\""))
}

/// The input parameters for the `list_foreign_keys` tool.
#[derive(
    Clone,
    Debug,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Serialize,
    Deserialize,
    schemars::JsonSchema,
)]
pub struct ListForeignKeysInput {
    /// The name of the table whose foreign keys should be listed.
    #[schemars(description = "The name of the table to list foreign keys for")]
    pub table_name: String,
}

/// The result of listing foreign keys for a table.
#[derive(
    Clone,
    Debug,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Serialize,
    Deserialize,
    schemars::JsonSchema,
)]
pub struct ListForeignKeysOutput {
    /// The foreign key constraints found on the table. Each entry represents
    /// one column mapping within a foreign key. Composite keys produce multiple
    /// entries sharing the same `id`.
    pub foreign_keys: Vec<ForeignKeyInfo>,
}

/// A single column mapping within a foreign key constraint. Multiple rows with
/// the same `id` indicate a composite foreign key that spans more than one
/// column.
#[derive(
    Clone,
    Debug,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Serialize,
    Deserialize,
    schemars::JsonSchema,
)]
pub struct ForeignKeyInfo {
    /// The foreign key constraint identifier. All column mappings belonging to
    /// the same composite key share this value.
    pub id: i64,
    /// The column in the local table that participates in the foreign key.
    pub from_column: String,
    /// The referenced (parent) table.
    pub to_table: String,
    /// The referenced column in the parent table.
    pub to_column: String,
    /// The action taken on update of the referenced row (e.g. `"NO ACTION"`,
    /// `"CASCADE"`, `"SET NULL"`, `"SET DEFAULT"`, `"RESTRICT"`).
    pub on_update: String,
    /// The action taken on deletion of the referenced row (e.g. `"NO ACTION"`,
    /// `"CASCADE"`, `"SET NULL"`, `"SET DEFAULT"`, `"RESTRICT"`).
    pub on_delete: String,
}

/// Errors specific to the `list_foreign_keys` tool.
#[derive(Debug, thiserror::Error)]
pub enum ListForeignKeysError {
    /// Failed to execute `PRAGMA foreign_key_list` for the requested table.
    #[error("failed to list foreign keys: {source}")]
    Query {
        /// The underlying rusqlite error.
        source: rusqlite::Error,
    },
}

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