1use serde_json::{Value, json};
4
5use crate::registry::ToolName;
6
7#[derive(Debug, Clone)]
8pub struct ToolSpec {
9 pub name: ToolName,
10 pub description: &'static str,
11 pub input_schema: Value,
12}
13
14pub fn phase2_catalog_tools() -> Vec<ToolSpec> {
16 vec![
17 ToolSpec {
18 name: ToolName::ListConnections,
19 description: "List configured connection profiles (never includes passwords).",
20 input_schema: object_schema(&[]),
21 },
22 ToolSpec {
23 name: ToolName::ListDatabases,
24 description: "List databases available for a connection profile.",
25 input_schema: object_schema(&[("connectionId", "string", true)]),
26 },
27 ToolSpec {
28 name: ToolName::ListSchemas,
29 description: "List non-system schemas in the currently selected database.",
30 input_schema: object_schema(&[]),
31 },
32 ToolSpec {
33 name: ToolName::ListObjects,
34 description: "List objects (tables, views, …) in a schema.",
35 input_schema: object_schema(&[("schema", "string", false), ("kind", "string", false)]),
36 },
37 ToolSpec {
38 name: ToolName::GetCurrentContext,
39 description: "Return the active profile, database, and access mode.",
40 input_schema: object_schema(&[]),
41 },
42 ToolSpec {
43 name: ToolName::SwitchConnection,
44 description: "Switch the session to another connection profile / database.",
45 input_schema: object_schema(&[
46 ("connectionId", "string", true),
47 ("database", "string", false),
48 ]),
49 },
50 ToolSpec {
51 name: ToolName::RunSelect,
52 description: "Run a read-only SELECT or WITH query. DML/DDL are rejected. Only reference tables/columns confirmed via list_schemas / list_objects.",
53 input_schema: object_schema(&[("sql", "string", true)]),
54 },
55 ToolSpec {
56 name: ToolName::ExplainQuery,
57 description: "Run EXPLAIN (no ANALYZE execute) for a SELECT/WITH query.",
58 input_schema: object_schema(&[("sql", "string", true)]),
59 },
60 ]
61}
62
63pub fn phase3_index_tools() -> Vec<ToolSpec> {
65 vec![
66 ToolSpec {
67 name: ToolName::SearchSchema,
68 description: "Search the live, auto-indexed database schema using natural language or keywords to find tables, views, materialized views, and functions matching the query. Call this FIRST before writing any SQL — do not assume a table exists without finding it here.",
69 input_schema: object_schema(&[("query", "string", true)]),
70 },
71 ToolSpec {
72 name: ToolName::DescribeObject,
73 description: "Get structural details of a specific database object (table, view, or materialized view) including columns, data types, constraints, and indexes.",
74 input_schema: object_schema(&[("ref", "string", true)]),
75 },
76 ToolSpec {
77 name: ToolName::GetJoinPath,
78 description: "Find the shortest path of join relationships and foreign keys between two database tables.",
79 input_schema: object_schema(&[("a", "string", true), ("b", "string", true)]),
80 },
81 ToolSpec {
82 name: ToolName::SampleValues,
83 description: "Retrieve a list of sample values from a specific table column to inspect its contents. Only works on read-only SELECT queries.",
84 input_schema: object_schema(&[("ref", "string", true), ("col", "string", true)]),
85 },
86 ]
87}
88
89pub fn phase4_tools() -> Vec<ToolSpec> {
91 vec![
92 ToolSpec {
93 name: ToolName::GetDdl,
94 description: "Get the DDL / definition of a database object. Views, materialized views, functions, and indexes return their CREATE statement; tables return structured DDL (columns, constraints, indexes).",
95 input_schema: object_schema(&[("ref", "string", true), ("kind", "string", false)]),
96 },
97 ToolSpec {
98 name: ToolName::TableStats,
99 description: "Get size, row-count, activity (scans, inserts/updates/deletes, dead tuples, vacuum/analyze times) and per-column statistics for a specific table.",
100 input_schema: object_schema(&[("ref", "string", true)]),
101 },
102 ToolSpec {
103 name: ToolName::IndexUsage,
104 description: "Get index usage statistics (scan counts, size, definition, type) for a specific table's indexes. Useful for finding unused or missing indexes.",
105 input_schema: object_schema(&[("ref", "string", true)]),
106 },
107 ToolSpec {
108 name: ToolName::ListRunningQueries,
109 description: "List currently executing (non-idle) queries in the connected database with pid, user, state, wait events, and duration.",
110 input_schema: object_schema(&[]),
111 },
112 ToolSpec {
113 name: ToolName::FindBlockingLocks,
114 description: "Find lock contention: which queries are blocked waiting on locks and which pids/queries are blocking them.",
115 input_schema: object_schema(&[]),
116 },
117 ToolSpec {
118 name: ToolName::SlowQueries,
119 description: "List the slowest statements by mean execution time from pg_stat_statements (requires the extension; returns a hint if not installed).",
120 input_schema: object_schema(&[("limit", "number", false)]),
121 },
122 ToolSpec {
123 name: ToolName::DbHealthCheck,
124 description: "Run a database health overview: size/connection stats, cache hit ratio, tables with dead tuples needing vacuum, active connections, and blocking-lock count. Sections that fail are reported individually; partial results are still returned.",
125 input_schema: object_schema(&[]),
126 },
127 ToolSpec {
128 name: ToolName::ExplainAnalyze,
129 description: "Run EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) on a SELECT/WITH query inside a read-only transaction that is always rolled back. WARNING: the query actually executes (volatile functions run), so expect real query runtime.",
130 input_schema: object_schema(&[("sql", "string", true)]),
131 },
132 ToolSpec {
133 name: ToolName::AnalyzeQueryPlan,
134 description: "Run EXPLAIN (FORMAT JSON) on a SELECT/WITH query and return parsed plan metrics (scan counts, bottlenecks, buffer stats) plus performance recommendations. Set analyze=true to also execute the query for actual timings.",
135 input_schema: object_schema(&[("sql", "string", true), ("analyze", "boolean", false)]),
136 },
137 ToolSpec {
138 name: ToolName::GetIndexStatus,
139 description: "Return schema-index status for the active connection/database: indexed_at, fingerprint, object counts, and optional live fingerprint drift.",
140 input_schema: object_schema(&[]),
141 },
142 ToolSpec {
143 name: ToolName::ListExtensions,
144 description: "List installed PostgreSQL extensions (name, version, schema).",
145 input_schema: object_schema(&[]),
146 },
147 ToolSpec {
148 name: ToolName::ServerSettings,
149 description: "Return key PostgreSQL server settings from pg_settings (memory, connections, timeouts, autovacuum, version).",
150 input_schema: object_schema(&[]),
151 },
152 ToolSpec {
153 name: ToolName::SuggestIndexes,
154 description: "Suggest indexes from high sequential-scan tables, unindexed FK columns, and optional pg_stat_statements / EXPLAIN plan heuristics. Pass sql to analyze a specific query plan.",
155 input_schema: object_schema(&[("limit", "number", false), ("sql", "string", false)]),
156 },
157 ToolSpec {
158 name: ToolName::FindUnusedIndexes,
159 description: "List indexes with idx_scan = 0 (never used since stats reset), excluding primary keys, unique indexes, and constraint-backed indexes.",
160 input_schema: object_schema(&[("limit", "number", false)]),
161 },
162 ToolSpec {
163 name: ToolName::BloatReport,
164 description: "Approximate table bloat via dead-tuple ratio from pg_stat_user_tables (simplified estimate — not physical page bloat). Tables with >1000 dead tuples, ordered by bloat %.",
165 input_schema: object_schema(&[("limit", "number", false)]),
166 },
167 ToolSpec {
168 name: ToolName::FindMissingFks,
169 description: "Find likely missing foreign keys: prefers schema-index join-graph inferred edges; falls back to catalog naming (*_id columns without an FK matching a PK).",
170 input_schema: object_schema(&[("limit", "number", false)]),
171 },
172 ]
173}
174
175pub fn phase4b_tools() -> Vec<ToolSpec> {
177 vec![
178 ToolSpec {
179 name: ToolName::ExportQuery,
180 description: "Run a read-only SELECT/WITH and format results as CSV, JSON, or SQL INSERT statements. Honors max-row / max-char caps. For sqlinsert, pass table as schema.name.",
181 input_schema: object_schema(&[
182 ("sql", "string", true),
183 ("format", "string", false),
184 ("table", "string", false),
185 ]),
186 },
187 ToolSpec {
188 name: ToolName::ListRoles,
189 description: "List PostgreSQL roles (attributes). Pass role to get memberships and table privileges for one role.",
190 input_schema: object_schema(&[("role", "string", false)]),
191 },
192 ToolSpec {
193 name: ToolName::DbDashboard,
194 description: "One-shot live metrics bundle: DB size/owner, connection-state breakdown, top tables by size, object counts, active queries, and blocking locks. Soft-fails per section.",
195 input_schema: object_schema(&[]),
196 },
197 ToolSpec {
198 name: ToolName::DeepPlanAnalysis,
199 description: "Run EXPLAIN (ANALYZE by default) and return severity-graded findings: estimate skew, expensive function/CTE/subquery nodes, and recommendations. Set analyze=false for plan-only (no execution).",
200 input_schema: object_schema(&[("sql", "string", true), ("analyze", "boolean", false)]),
201 },
202 ToolSpec {
203 name: ToolName::SchemaDiff,
204 description: "Compare two schemas in the current database (or sourceSchema vs targetSchema). Returns structured table/column/constraint/index diffs. Read-only — does not apply changes.",
205 input_schema: object_schema(&[
206 ("sourceSchema", "string", true),
207 ("targetSchema", "string", true),
208 ]),
209 },
210 ToolSpec {
211 name: ToolName::GenerateMigration,
212 description: "Emit migration SQL to evolve sourceSchema toward targetSchema (from a live schema_diff). Read-only — returns SQL text, never executes it. Destructive drops are commented out.",
213 input_schema: object_schema(&[
214 ("sourceSchema", "string", true),
215 ("targetSchema", "string", true),
216 ]),
217 },
218 ]
219}
220
221pub fn phase9_write_tools() -> Vec<ToolSpec> {
223 vec![
224 ToolSpec {
225 name: ToolName::ExecuteSql,
226 description: "Execute DML (and DDL in admin mode) inside an explicit transaction. Set dry_run=true to roll back after execution. Errors always roll back.",
227 input_schema: object_schema(&[("sql", "string", true), ("dry_run", "boolean", false)]),
228 },
229 ToolSpec {
230 name: ToolName::EditRow,
231 description: "Structured insert, update, or delete by primary key. The server builds parameterized SQL — pass table (schema.name), action, values, and pk for update/delete.",
232 input_schema: object_schema(&[
233 ("table", "string", true),
234 ("action", "string", true),
235 ("values", "object", false),
236 ("pk", "object", false),
237 ]),
238 },
239 ToolSpec {
240 name: ToolName::ImportData,
241 description: "Batch INSERT rows from a JSON array of objects into a table. Optional columns array fixes column order; otherwise keys from the first row are used.",
242 input_schema: object_schema(&[
243 ("table", "string", true),
244 ("rows", "array", true),
245 ("columns", "array", false),
246 ]),
247 },
248 ToolSpec {
249 name: ToolName::ApplyDdl,
250 description: "Apply a DDL statement (CREATE, ALTER, DROP, TRUNCATE, …) in admin mode inside a transaction. Set dry_run=true to roll back.",
251 input_schema: object_schema(&[("sql", "string", true), ("dry_run", "boolean", false)]),
252 },
253 ToolSpec {
254 name: ToolName::CreateIndexConcurrently,
255 description: "Run CREATE INDEX CONCURRENTLY outside a transaction (non-blocking index build). Admin mode only.",
256 input_schema: object_schema(&[("sql", "string", true)]),
257 },
258 ToolSpec {
259 name: ToolName::RunMaintenance,
260 description: "Run VACUUM, ANALYZE, or REINDEX outside a transaction. Admin mode only. Optional table (schema.name); vacuum supports full=true.",
261 input_schema: object_schema(&[
262 ("action", "string", true),
263 ("table", "string", false),
264 ("full", "boolean", false),
265 ]),
266 },
267 ToolSpec {
268 name: ToolName::TerminateQuery,
269 description: "Cancel (pg_cancel_backend) or force-terminate (pg_terminate_backend) a backend by pid. Admin mode only. Refuses superuser targets and the current session.",
270 input_schema: object_schema(&[("pid", "number", true), ("force", "boolean", false)]),
271 },
272 ]
273}
274
275pub fn active_tools() -> Vec<ToolSpec> {
277 let mut specs = phase2_catalog_tools();
278 specs.extend(phase3_index_tools());
279 specs.extend(phase4_tools());
280 specs.extend(phase4b_tools());
281 specs.extend(phase9_write_tools());
282 specs
283}
284
285fn object_schema(props: &[(&str, &str, bool)]) -> Value {
286 let mut properties = serde_json::Map::new();
287 let mut required = Vec::new();
288 for (name, ty, req) in props {
289 let prop_val = match *ty {
290 "array" => match *name {
291 "columns" => json!({ "type": "array", "items": { "type": "string" } }),
292 "rows" => json!({ "type": "array", "items": { "type": "object" } }),
293 _ => json!({ "type": "array", "items": {} }),
294 },
295 _ => json!({ "type": *ty }),
296 };
297 properties.insert((*name).into(), prop_val);
298 if *req {
299 required.push(json!(*name));
300 }
301 }
302 json!({
303 "type": "object",
304 "properties": properties,
305 "required": required
306 })
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use crate::registry::ToolName;
313
314 #[test]
315 fn active_tools_lists_forty_one() {
316 let specs = active_tools();
317 assert_eq!(specs.len(), 41);
318 assert_eq!(specs.len(), ToolName::ACTIVE.len());
319 for (spec, name) in specs.iter().zip(ToolName::ACTIVE.iter()) {
320 assert_eq!(spec.name, *name);
321 }
322 }
323
324 #[test]
325 fn phase9_write_tools_count() {
326 assert_eq!(phase9_write_tools().len(), ToolName::PHASE9.len());
327 }
328
329 #[test]
330 fn array_properties_have_items() {
331 for tool in active_tools() {
332 if let Some(props) = tool
333 .input_schema
334 .get("properties")
335 .and_then(|p| p.as_object())
336 {
337 for (prop_name, prop_val) in props {
338 if prop_val.get("type").and_then(|t| t.as_str()) == Some("array") {
339 assert!(
340 prop_val.get("items").is_some(),
341 "Tool '{}' parameter '{}' is array type but missing 'items'",
342 tool.name.as_str(),
343 prop_name
344 );
345 }
346 }
347 }
348 }
349 }
350}