1use rmcp::model::{Content, IntoContents};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9use super::ToolError;
10use crate::{mcp::McpServerSqlite, traits::SqliteServerTool};
11
12#[derive(
13 Clone,
14 Copy,
15 Debug,
16 PartialEq,
17 Eq,
18 PartialOrd,
19 Ord,
20 Hash,
21 Default,
22 Serialize,
23 Deserialize,
24 JsonSchema,
25)]
26pub struct ListTriggersTool;
30
31impl SqliteServerTool for ListTriggersTool {
32 const NAME: &str = "list_triggers";
33
34 type Context = McpServerSqlite;
35 type Error = ToolError<ListTriggersError>;
36
37 type Input = ListTriggersInput;
38 type Output = ListTriggersOutput;
39
40 fn handle(
41 ctx: &Self::Context,
42 input: Self::Input,
43 ) -> Result<Self::Output, Self::Error> {
44 let conn = ctx
45 .connection()
46 .map_err(|source| ToolError::Connection { source })?;
47
48 let triggers = match &input.table_name {
49 Some(table) => {
50 let mut stmt = conn
51 .prepare(
52 "SELECT name, tbl_name, sql FROM sqlite_master \
53 WHERE type = 'trigger' AND tbl_name = ?1 \
54 ORDER BY tbl_name, name",
55 )
56 .map_err(|source| {
57 ToolError::Tool(ListTriggersError::Query { source })
58 })?;
59
60 stmt.query_map([table], |row| Ok(trigger_info_from_row(row)))
61 .map_err(|source| {
62 ToolError::Tool(ListTriggersError::Query { source })
63 })?
64 .collect::<Result<Vec<_>, _>>()
65 .map_err(|source| {
66 ToolError::Tool(ListTriggersError::Query { source })
67 })?
68 }
69 None => {
70 let mut stmt = conn
71 .prepare(
72 "SELECT name, tbl_name, sql FROM sqlite_master \
73 WHERE type = 'trigger' \
74 ORDER BY tbl_name, name",
75 )
76 .map_err(|source| {
77 ToolError::Tool(ListTriggersError::Query { source })
78 })?;
79
80 stmt.query_map([], |row| Ok(trigger_info_from_row(row)))
81 .map_err(|source| {
82 ToolError::Tool(ListTriggersError::Query { source })
83 })?
84 .collect::<Result<Vec<_>, _>>()
85 .map_err(|source| {
86 ToolError::Tool(ListTriggersError::Query { source })
87 })?
88 }
89 };
90
91 Ok(ListTriggersOutput { triggers })
92 }
93}
94
95fn trigger_info_from_row(row: &rusqlite::Row<'_>) -> TriggerInfo {
98 let name: String = row.get(0).unwrap_or_default();
99 let table_name: String = row.get(1).unwrap_or_default();
100 let sql: Option<String> = row.get(2).unwrap_or_default();
101 let sql_text = sql.unwrap_or_default();
102
103 let (timing, event) = parse_timing_and_event(&name, &sql_text);
104
105 TriggerInfo {
106 name,
107 table_name,
108 event,
109 timing,
110 sql: sql_text,
111 }
112}
113
114fn parse_timing_and_event(trigger_name: &str, sql: &str) -> (String, String) {
125 let upper = sql.to_uppercase();
126 let search_name = trigger_name.to_uppercase();
127
128 let after_name_pos =
129 upper.find(&search_name).map(|pos| pos + search_name.len());
130
131 let remainder = match after_name_pos {
132 Some(pos) => &upper[pos..],
133 None => return (unknown(), unknown()),
134 };
135
136 let trimmed = remainder.trim_start();
137
138 let timing = if trimmed.starts_with("BEFORE") {
139 "BEFORE"
140 } else if trimmed.starts_with("AFTER") {
141 "AFTER"
142 } else if trimmed.starts_with("INSTEAD OF") {
143 "INSTEAD OF"
144 } else {
145 return (unknown(), unknown());
146 };
147
148 let after_timing = trimmed[timing.len()..].trim_start();
149
150 let event = if after_timing.starts_with("INSERT") {
151 "INSERT"
152 } else if after_timing.starts_with("UPDATE") {
153 "UPDATE"
154 } else if after_timing.starts_with("DELETE") {
155 "DELETE"
156 } else {
157 return (timing.to_owned(), unknown());
158 };
159
160 (timing.to_owned(), event.to_owned())
161}
162
163fn unknown() -> String {
165 "UNKNOWN".to_owned()
166}
167
168#[derive(
170 Clone,
171 Debug,
172 Default,
173 PartialEq,
174 Eq,
175 PartialOrd,
176 Ord,
177 Hash,
178 Serialize,
179 Deserialize,
180 schemars::JsonSchema,
181)]
182pub struct ListTriggersInput {
183 #[schemars(description = "Optional table name to filter triggers by. \
187 Omit to list all triggers.")]
188 pub table_name: Option<String>,
189}
190
191#[derive(
193 Clone,
194 Debug,
195 PartialEq,
196 Eq,
197 PartialOrd,
198 Ord,
199 Hash,
200 Serialize,
201 Deserialize,
202 schemars::JsonSchema,
203)]
204pub struct ListTriggersOutput {
205 pub triggers: Vec<TriggerInfo>,
207}
208
209#[derive(
211 Clone,
212 Debug,
213 PartialEq,
214 Eq,
215 PartialOrd,
216 Ord,
217 Hash,
218 Serialize,
219 Deserialize,
220 schemars::JsonSchema,
221)]
222pub struct TriggerInfo {
223 pub name: String,
225 pub table_name: String,
227 pub event: String,
229 pub timing: String,
232 pub sql: String,
234}
235
236#[derive(Debug, thiserror::Error)]
238pub enum ListTriggersError {
239 #[error("failed to list triggers: {source}")]
241 Query {
242 source: rusqlite::Error,
244 },
245}
246
247impl IntoContents for ListTriggersError {
250 fn into_contents(self) -> Vec<Content> {
251 vec![Content::text(self.to_string())]
252 }
253}