Skip to main content

mcp_server_sqlite/tools/
list_foreign_keys_tool.rs

1//! The `list_foreign_keys` tool: returns all foreign key constraints defined on
2//! a given table. Each row from SQLite's `PRAGMA foreign_key_list` becomes one
3//! entry in the result, so composite foreign keys appear as multiple entries
4//! sharing the same `id`.
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 all foreign key constraints for a given table. Returns one entry per
28/// column mapping, including the referenced table, the local and remote
29/// columns, and the ON UPDATE / ON DELETE actions. Composite foreign keys
30/// produce multiple entries that share the same id.
31pub struct ListForeignKeysTool;
32
33impl SqliteServerTool for ListForeignKeysTool {
34    const NAME: &str = "list_foreign_keys";
35
36    type Context = McpServerSqlite;
37    type Error = ToolError<ListForeignKeysError>;
38
39    type Input = ListForeignKeysInput;
40    type Output = ListForeignKeysOutput;
41
42    fn handle(
43        ctx: &Self::Context,
44        input: Self::Input,
45    ) -> Result<Self::Output, Self::Error> {
46        let conn = ctx
47            .connection()
48            .map_err(|source| ToolError::Connection { source })?;
49
50        let pragma_sql = format!(
51            "PRAGMA foreign_key_list({})",
52            enquote_identifier(&input.table_name),
53        );
54
55        let mut stmt = conn.prepare(&pragma_sql).map_err(|source| {
56            ToolError::Tool(ListForeignKeysError::Query { source })
57        })?;
58
59        let foreign_keys = stmt
60            .query_map([], |row| {
61                Ok(ForeignKeyInfo {
62                    id: row.get(0)?,
63                    from_column: row.get(3)?,
64                    to_table: row.get(2)?,
65                    to_column: row.get(4)?,
66                    on_update: row.get(5)?,
67                    on_delete: row.get(6)?,
68                })
69            })
70            .map_err(|source| {
71                ToolError::Tool(ListForeignKeysError::Query { source })
72            })?
73            .collect::<Result<Vec<_>, _>>()
74            .map_err(|source| {
75                ToolError::Tool(ListForeignKeysError::Query { source })
76            })?;
77
78        Ok(ListForeignKeysOutput { foreign_keys })
79    }
80}
81
82/// Wraps `name` in double-quotes and escapes any embedded double-quote
83/// characters, producing a safe SQLite identifier for use in PRAGMA statements.
84fn enquote_identifier(name: &str) -> String {
85    format!("\"{}\"", name.replace('"', "\"\""))
86}
87
88/// The input parameters for the `list_foreign_keys` tool.
89#[derive(
90    Clone,
91    Debug,
92    PartialEq,
93    Eq,
94    PartialOrd,
95    Ord,
96    Hash,
97    Serialize,
98    Deserialize,
99    schemars::JsonSchema,
100)]
101pub struct ListForeignKeysInput {
102    /// The name of the table whose foreign keys should be listed.
103    #[schemars(description = "The name of the table to list foreign keys for")]
104    pub table_name: String,
105}
106
107/// The result of listing foreign keys for a table.
108#[derive(
109    Clone,
110    Debug,
111    PartialEq,
112    Eq,
113    PartialOrd,
114    Ord,
115    Hash,
116    Serialize,
117    Deserialize,
118    schemars::JsonSchema,
119)]
120pub struct ListForeignKeysOutput {
121    /// The foreign key constraints found on the table. Each entry represents
122    /// one column mapping within a foreign key. Composite keys produce multiple
123    /// entries sharing the same `id`.
124    pub foreign_keys: Vec<ForeignKeyInfo>,
125}
126
127/// A single column mapping within a foreign key constraint. Multiple rows with
128/// the same `id` indicate a composite foreign key that spans more than one
129/// column.
130#[derive(
131    Clone,
132    Debug,
133    PartialEq,
134    Eq,
135    PartialOrd,
136    Ord,
137    Hash,
138    Serialize,
139    Deserialize,
140    schemars::JsonSchema,
141)]
142pub struct ForeignKeyInfo {
143    /// The foreign key constraint identifier. All column mappings belonging to
144    /// the same composite key share this value.
145    pub id: i64,
146    /// The column in the local table that participates in the foreign key.
147    pub from_column: String,
148    /// The referenced (parent) table.
149    pub to_table: String,
150    /// The referenced column in the parent table.
151    pub to_column: String,
152    /// The action taken on update of the referenced row (e.g. `"NO ACTION"`,
153    /// `"CASCADE"`, `"SET NULL"`, `"SET DEFAULT"`, `"RESTRICT"`).
154    pub on_update: String,
155    /// The action taken on deletion of the referenced row (e.g. `"NO ACTION"`,
156    /// `"CASCADE"`, `"SET NULL"`, `"SET DEFAULT"`, `"RESTRICT"`).
157    pub on_delete: String,
158}
159
160/// Errors specific to the `list_foreign_keys` tool.
161#[derive(Debug, thiserror::Error)]
162pub enum ListForeignKeysError {
163    /// Failed to execute `PRAGMA foreign_key_list` for the requested table.
164    #[error("failed to list foreign keys: {source}")]
165    Query {
166        /// The underlying rusqlite error.
167        source: rusqlite::Error,
168    },
169}
170
171/// Converts the list-foreign-keys-specific error into MCP content by rendering
172/// the display string as text.
173impl IntoContents for ListForeignKeysError {
174    fn into_contents(self) -> Vec<Content> {
175        vec![Content::text(self.to_string())]
176    }
177}