mcp_postgres/actions/
maintenance.rs1use crate::errors::Result as MCPResult;
2use serde_json::{Value, json};
3use tokio_postgres::Client;
4
5const MAX_IDENTIFIER_LEN: usize = 255;
6
7fn validate_table_name(name: &str) -> std::result::Result<(), crate::errors::MCPError> {
8 if name.is_empty() {
9 return Err(crate::errors::MCPError::InvalidParams(
10 "'table' must not be empty".into(),
11 ));
12 }
13 if name.len() > MAX_IDENTIFIER_LEN {
14 return Err(crate::errors::MCPError::InvalidParams(format!(
15 "'table' exceeds maximum length of {MAX_IDENTIFIER_LEN} characters (got {})",
16 name.len()
17 )));
18 }
19 Ok(())
20}
21
22pub async fn vacuum_analyze(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
24 let table_name = params.as_ref().and_then(|p| {
25 p.get("table")
26 .and_then(|v| v.as_str())
27 .map(|s| s.to_string())
28 });
29
30 if let Some(ref table) = table_name {
31 validate_table_name(table)?;
32 }
33
34 let sql = if let Some(ref table) = table_name {
35 format!("VACUUM ANALYZE {}", table)
36 } else {
37 "VACUUM ANALYZE".to_string()
38 };
39
40 client.execute(&sql, &[]).await?;
41
42 Ok(json!({
43 "status": "success",
44 "action": "VACUUM ANALYZE",
45 "table": table_name
46 }))
47}
48
49pub async fn analyze_table(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
51 let table_name = params
52 .as_ref()
53 .and_then(|p| {
54 p.get("table")
55 .and_then(|v| v.as_str())
56 .map(|s| s.to_string())
57 })
58 .ok_or_else(|| {
59 crate::errors::MCPError::InvalidParams("Missing 'table' parameter".into())
60 })?;
61
62 validate_table_name(&table_name)?;
63
64 client
65 .execute(&format!("ANALYZE {}", table_name), &[])
66 .await?;
67
68 Ok(json!({
69 "status": "success",
70 "action": "ANALYZE",
71 "table": table_name
72 }))
73}
74
75pub async fn reindex_table(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
77 let table_name = params
78 .as_ref()
79 .and_then(|p| {
80 p.get("table")
81 .and_then(|v| v.as_str())
82 .map(|s| s.to_string())
83 })
84 .ok_or_else(|| {
85 crate::errors::MCPError::InvalidParams("Missing 'table' parameter".into())
86 })?;
87
88 validate_table_name(&table_name)?;
89
90 client
91 .execute(&format!("REINDEX TABLE {}", table_name), &[])
92 .await?;
93
94 Ok(json!({
95 "status": "success",
96 "action": "REINDEX",
97 "table": table_name
98 }))
99}
100
101pub async fn get_pg_stat_statements(client: &Client, _params: &Option<&Value>) -> MCPResult<Value> {
103 let rows = client
104 .query(
105 "SELECT query, calls, mean_time, max_time, total_time
106 FROM pg_stat_statements
107 ORDER BY total_time DESC
108 LIMIT 50",
109 &[],
110 )
111 .await;
112
113 match rows {
114 Ok(r) => {
115 let statements: Vec<Value> = r
116 .iter()
117 .map(|row| {
118 json!({
119 "query": row.get::<_, String>(0),
120 "calls": row.get::<_, i64>(1),
121 "mean_time_ms": row.get::<_, f64>(2),
122 "max_time_ms": row.get::<_, f64>(3),
123 "total_time_ms": row.get::<_, f64>(4),
124 })
125 })
126 .collect();
127
128 Ok(json!({ "statements": statements }))
129 }
130 Err(_) => Ok(json!({
131 "error": "pg_stat_statements extension not installed",
132 "statements": []
133 })),
134 }
135}
136
137pub async fn reset_statistics(client: &Client, _params: &Option<&Value>) -> MCPResult<Value> {
139 client.execute("SELECT pg_stat_reset()", &[]).await?;
140
141 Ok(json!({
142 "status": "success",
143 "action": "reset_statistics",
144 "message": "All statistics counters have been reset"
145 }))
146}
147
148pub async fn truncate_table(client: &Client, params: &Option<&Value>) -> MCPResult<Value> {
150 let table = params
151 .as_ref()
152 .and_then(|p| p.get("table").and_then(|v| v.as_str()))
153 .ok_or_else(|| {
154 crate::errors::MCPError::InvalidParams("Missing 'table' parameter".into())
155 })?;
156
157 validate_table_name(table)?;
158
159 let cascade = params
160 .as_ref()
161 .and_then(|p| p.get("cascade").and_then(|v| v.as_bool()))
162 .unwrap_or(false);
163
164 let restart_identity = params
165 .as_ref()
166 .and_then(|p| p.get("restart_identity").and_then(|v| v.as_bool()))
167 .unwrap_or(false);
168
169 let cascade_str = if cascade { " CASCADE" } else { "" };
170 let restart_str = if restart_identity {
171 " RESTART IDENTITY"
172 } else {
173 ""
174 };
175
176 let sql = format!("TRUNCATE TABLE {}{}{}", table, restart_str, cascade_str);
177
178 client.execute(&sql, &[]).await?;
179
180 Ok(json!({
181 "status": "success",
182 "action": "TRUNCATE",
183 "table": table,
184 "cascade": cascade,
185 "restart_identity": restart_identity,
186 "message": format!("Table '{}' has been truncated", table)
187 }))
188}