Skip to main content

nexql_tools/
registry.rs

1// SPDX-License-Identifier: GPL-3.0-only
2// Copyright (C) 2026 NexQL-OSS Team
3
4//! Tool name catalog for the MCP surface.
5
6/// Tool surface preset profiles to control context window overhead.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum ToolProfile {
9    /// Core schema search, object inspection, join path, query execution, and export tools.
10    Query,
11    /// DBA health checks, index suggestions, table stats, locks, slow queries, and maintenance.
12    Dba,
13    /// Minimal initial tool surface (5 core tools) + discover_tools meta-tool for lazy tool activation.
14    Meta,
15    /// All active MCP surface tools.
16    #[default]
17    Full,
18}
19
20impl ToolProfile {
21    pub fn as_str(self) -> &'static str {
22        match self {
23            Self::Query => "query",
24            Self::Dba => "dba",
25            Self::Meta => "meta",
26            Self::Full => "full",
27        }
28    }
29
30    pub fn parse(s: &str) -> Option<Self> {
31        match s.to_lowercase().as_str() {
32            "query" => Some(Self::Query),
33            "dba" => Some(Self::Dba),
34            "meta" => Some(Self::Meta),
35            "full" => Some(Self::Full),
36            _ => None,
37        }
38    }
39}
40
41/// Read-only tool surface (catalog + index + Phase 4 monitoring/DDL + Phase 4b breadth).
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum ToolName {
44    ResolveTarget,
45    Orient,
46    InspectOrSearch,
47    SearchAllDatabases,
48    SearchSchema,
49    DescribeObject,
50    GetJoinPath,
51    SampleValues,
52    RunSelect,
53    ExplainQuery,
54    ListConnections,
55    ListDatabases,
56    ListSchemas,
57    ListObjects,
58    GetCurrentContext,
59    SwitchConnection,
60    GetDdl,
61    TableStats,
62    IndexUsage,
63    ListRunningQueries,
64    FindBlockingLocks,
65    SlowQueries,
66    DbHealthCheck,
67    GetIndexStatus,
68    ListExtensions,
69    ServerSettings,
70    SuggestIndexes,
71    FindUnusedIndexes,
72    BloatReport,
73    FindMissingFks,
74    ExportQuery,
75    ListRoles,
76    DbDashboard,
77    DeepPlanAnalysis,
78    SchemaDiff,
79    GenerateMigration,
80    ExecuteSql,
81    EditRow,
82    ImportData,
83    ApplyDdl,
84    CreateIndexConcurrently,
85    RunMaintenance,
86    TerminateQuery,
87    DiscoverTools,
88    AutoTuneQuery,
89    CheckDdlSafety,
90    RebuildIndex,
91    RefreshIndex,
92    RunDoctor,
93    SetupConnection,
94    SaveProfile,
95    TestProfile,
96    ExportProfile,
97    ImportProfile,
98}
99
100impl ToolName {
101    pub const PHASE2: &'static [ToolName] = &[
102        Self::ListConnections,
103        Self::ListDatabases,
104        Self::ListSchemas,
105        Self::ListObjects,
106        Self::GetCurrentContext,
107        Self::SwitchConnection,
108        Self::RunSelect,
109        Self::ExplainQuery,
110        Self::DiscoverTools,
111        Self::RunDoctor,
112        Self::SetupConnection,
113        Self::SaveProfile,
114        Self::TestProfile,
115        Self::ExportProfile,
116        Self::ImportProfile,
117    ];
118
119    /// Index-backed tools (require `nexql-mcp index build` or `rebuild_index`).
120    pub const PHASE3: &'static [ToolName] = &[
121        Self::ResolveTarget,
122        Self::Orient,
123        Self::InspectOrSearch,
124        Self::SearchAllDatabases,
125        Self::SearchSchema,
126        Self::DescribeObject,
127        Self::GetJoinPath,
128        Self::SampleValues,
129        Self::RebuildIndex,
130        Self::RefreshIndex,
131    ];
132
133    /// Phase 4 monitoring / DDL / index-status (+ free advisory tools).
134    pub const PHASE4: &'static [ToolName] = &[
135        Self::GetDdl,
136        Self::TableStats,
137        Self::IndexUsage,
138        Self::ListRunningQueries,
139        Self::FindBlockingLocks,
140        Self::SlowQueries,
141        Self::DbHealthCheck,
142        Self::GetIndexStatus,
143        Self::ListExtensions,
144        Self::ServerSettings,
145        Self::SuggestIndexes,
146        Self::FindUnusedIndexes,
147        Self::BloatReport,
148        Self::FindMissingFks,
149    ];
150
151    /// Phase 4b read-only breadth (export / roles / dashboard; more tools land here).
152    pub const PHASE4B: &'static [ToolName] = &[
153        Self::ExportQuery,
154        Self::ListRoles,
155        Self::DbDashboard,
156        Self::DeepPlanAnalysis,
157        Self::SchemaDiff,
158        Self::GenerateMigration,
159        Self::AutoTuneQuery,
160        Self::CheckDdlSafety,
161    ];
162
163    /// Phase 9 write/admin tools (listed always; gated at call time by access mode).
164    pub const PHASE9: &'static [ToolName] = &[
165        Self::ExecuteSql,
166        Self::EditRow,
167        Self::ImportData,
168        Self::ApplyDdl,
169        Self::CreateIndexConcurrently,
170        Self::RunMaintenance,
171        Self::TerminateQuery,
172    ];
173
174    /// Full tools/list surface for the current phase.
175    pub const ACTIVE: &'static [ToolName] = &[
176        Self::ListConnections,
177        Self::ListDatabases,
178        Self::ListSchemas,
179        Self::ListObjects,
180        Self::GetCurrentContext,
181        Self::SwitchConnection,
182        Self::RunSelect,
183        Self::ExplainQuery,
184        Self::DiscoverTools,
185        Self::RunDoctor,
186        Self::SetupConnection,
187        Self::SaveProfile,
188        Self::TestProfile,
189        Self::ExportProfile,
190        Self::ImportProfile,
191        Self::ResolveTarget,
192        Self::Orient,
193        Self::InspectOrSearch,
194        Self::SearchAllDatabases,
195        Self::SearchSchema,
196        Self::DescribeObject,
197        Self::GetJoinPath,
198        Self::SampleValues,
199        Self::RebuildIndex,
200        Self::RefreshIndex,
201        Self::GetDdl,
202        Self::TableStats,
203        Self::IndexUsage,
204        Self::ListRunningQueries,
205        Self::FindBlockingLocks,
206        Self::SlowQueries,
207        Self::DbHealthCheck,
208        Self::GetIndexStatus,
209        Self::ListExtensions,
210        Self::ServerSettings,
211        Self::SuggestIndexes,
212        Self::FindUnusedIndexes,
213        Self::BloatReport,
214        Self::FindMissingFks,
215        Self::ExportQuery,
216        Self::ListRoles,
217        Self::DbDashboard,
218        Self::DeepPlanAnalysis,
219        Self::SchemaDiff,
220        Self::GenerateMigration,
221        Self::AutoTuneQuery,
222        Self::CheckDdlSafety,
223        Self::ExecuteSql,
224        Self::EditRow,
225        Self::ImportData,
226        Self::ApplyDdl,
227        Self::CreateIndexConcurrently,
228        Self::RunMaintenance,
229        Self::TerminateQuery,
230    ];
231
232    /// Read-only subset (Phase 2–4b). Write/admin tools are in ACTIVE but gated at dispatch.
233    pub const READ_ONLY: &'static [ToolName] = &[
234        Self::ListConnections,
235        Self::ListDatabases,
236        Self::ListSchemas,
237        Self::ListObjects,
238        Self::GetCurrentContext,
239        Self::SwitchConnection,
240        Self::RunSelect,
241        Self::ExplainQuery,
242        Self::RunDoctor,
243        Self::SetupConnection,
244        Self::SaveProfile,
245        Self::TestProfile,
246        Self::ExportProfile,
247        Self::ImportProfile,
248        Self::ResolveTarget,
249        Self::Orient,
250        Self::InspectOrSearch,
251        Self::SearchAllDatabases,
252        Self::SearchSchema,
253        Self::DescribeObject,
254        Self::GetJoinPath,
255        Self::SampleValues,
256        Self::RebuildIndex,
257        Self::RefreshIndex,
258        Self::GetDdl,
259        Self::TableStats,
260        Self::IndexUsage,
261        Self::ListRunningQueries,
262        Self::FindBlockingLocks,
263        Self::SlowQueries,
264        Self::DbHealthCheck,
265        Self::GetIndexStatus,
266        Self::ListExtensions,
267        Self::ServerSettings,
268        Self::SuggestIndexes,
269        Self::FindUnusedIndexes,
270        Self::BloatReport,
271        Self::FindMissingFks,
272        Self::ExportQuery,
273        Self::ListRoles,
274        Self::DbDashboard,
275        Self::DeepPlanAnalysis,
276        Self::SchemaDiff,
277        Self::GenerateMigration,
278        // Non-destructive (both callees are read-only); was previously missing
279        // from this list — drive-by fix found alongside the EXPLAIN consolidation.
280        Self::AutoTuneQuery,
281    ];
282
283    /// Subset of tools optimized for context-constrained query & schema exploration tasks.
284    pub const QUERY_PROFILE: &'static [ToolName] = &[
285        Self::ListConnections,
286        Self::ListDatabases,
287        Self::ListSchemas,
288        Self::ListObjects,
289        Self::GetCurrentContext,
290        Self::SwitchConnection,
291        Self::RunSelect,
292        Self::ExplainQuery,
293        Self::ResolveTarget,
294        Self::Orient,
295        Self::InspectOrSearch,
296        Self::SearchAllDatabases,
297        Self::SearchSchema,
298        Self::DescribeObject,
299        Self::GetJoinPath,
300        Self::SampleValues,
301        Self::RebuildIndex,
302        Self::RefreshIndex,
303        Self::RunDoctor,
304        Self::GetDdl,
305        Self::ExportQuery,
306    ];
307
308    /// Subset of tools optimized for database administration, performance tuning, and health checks.
309    pub const DBA_PROFILE: &'static [ToolName] = &[
310        Self::ListConnections,
311        Self::GetCurrentContext,
312        Self::TableStats,
313        Self::IndexUsage,
314        Self::ListRunningQueries,
315        Self::FindBlockingLocks,
316        Self::SlowQueries,
317        Self::DbHealthCheck,
318        Self::GetIndexStatus,
319        Self::ListExtensions,
320        Self::ServerSettings,
321        Self::SuggestIndexes,
322        Self::FindUnusedIndexes,
323        Self::BloatReport,
324        Self::FindMissingFks,
325        Self::DbDashboard,
326        Self::DeepPlanAnalysis,
327        Self::SchemaDiff,
328        Self::GenerateMigration,
329        Self::AutoTuneQuery,
330        Self::CheckDdlSafety,
331        Self::RunMaintenance,
332        Self::TerminateQuery,
333        Self::RebuildIndex,
334        Self::RefreshIndex,
335        Self::RunDoctor,
336    ];
337
338    /// Minimal initial tool surface with discover_tools for lazy tool activation.
339    pub const META_PROFILE: &'static [ToolName] = &[
340        Self::ListConnections,
341        Self::GetCurrentContext,
342        Self::Orient,
343        Self::InspectOrSearch,
344        Self::SearchAllDatabases,
345        Self::SearchSchema,
346        Self::DescribeObject,
347        Self::RunSelect,
348        Self::DiscoverTools,
349        Self::RunDoctor,
350        Self::SetupConnection,
351        Self::SaveProfile,
352        Self::TestProfile,
353    ];
354
355    pub fn for_profile(profile: ToolProfile) -> &'static [ToolName] {
356        match profile {
357            ToolProfile::Query => Self::QUERY_PROFILE,
358            ToolProfile::Dba => Self::DBA_PROFILE,
359            ToolProfile::Meta => Self::META_PROFILE,
360            ToolProfile::Full => Self::ACTIVE,
361        }
362    }
363
364    pub fn as_str(self) -> &'static str {
365        match self {
366            Self::ResolveTarget => "resolve_target",
367            Self::Orient => "orient",
368            Self::InspectOrSearch => "inspect_or_search",
369            Self::SearchAllDatabases => "search_all_databases",
370            Self::SearchSchema => "search_schema",
371            Self::DescribeObject => "describe_object",
372            Self::GetJoinPath => "get_join_path",
373            Self::SampleValues => "sample_values",
374            Self::RunSelect => "run_select",
375            Self::ExplainQuery => "explain_query",
376            Self::ListConnections => "list_connections",
377            Self::ListDatabases => "list_databases",
378            Self::ListSchemas => "list_schemas",
379            Self::ListObjects => "list_objects",
380            Self::GetCurrentContext => "get_current_context",
381            Self::SwitchConnection => "switch_connection",
382            Self::GetDdl => "get_ddl",
383            Self::TableStats => "table_stats",
384            Self::IndexUsage => "index_usage",
385            Self::ListRunningQueries => "list_running_queries",
386            Self::FindBlockingLocks => "find_blocking_locks",
387            Self::SlowQueries => "slow_queries",
388            Self::DbHealthCheck => "db_health_check",
389            Self::GetIndexStatus => "get_index_status",
390            Self::ListExtensions => "list_extensions",
391            Self::ServerSettings => "server_settings",
392            Self::SuggestIndexes => "suggest_indexes",
393            Self::FindUnusedIndexes => "find_unused_indexes",
394            Self::BloatReport => "bloat_report",
395            Self::FindMissingFks => "find_missing_fks",
396            Self::ExportQuery => "export_query",
397            Self::ListRoles => "list_roles",
398            Self::DbDashboard => "db_dashboard",
399            Self::DeepPlanAnalysis => "deep_plan_analysis",
400            Self::SchemaDiff => "schema_diff",
401            Self::GenerateMigration => "generate_migration",
402            Self::AutoTuneQuery => "auto_tune_query",
403            Self::CheckDdlSafety => "check_ddl_safety",
404            Self::ExecuteSql => "execute_sql",
405            Self::EditRow => "edit_row",
406            Self::ImportData => "import_data",
407            Self::ApplyDdl => "apply_ddl",
408            Self::CreateIndexConcurrently => "create_index_concurrently",
409            Self::RunMaintenance => "run_maintenance",
410            Self::TerminateQuery => "terminate_query",
411            Self::DiscoverTools => "discover_tools",
412            Self::RebuildIndex => "rebuild_index",
413            Self::RefreshIndex => "refresh_index",
414            Self::RunDoctor => "run_doctor",
415            Self::SetupConnection => "setup_connection",
416            Self::SaveProfile => "save_profile",
417            Self::TestProfile => "test_profile",
418            Self::ExportProfile => "export_profile",
419            Self::ImportProfile => "import_profile",
420        }
421    }
422
423    pub fn parse(s: &str) -> Option<Self> {
424        Self::ACTIVE.iter().copied().find(|t| t.as_str() == s)
425    }
426
427    /// MCP client hints (`readOnlyHint`, `destructiveHint`, …).
428    pub fn hints(self) -> ToolHints {
429        let read_only = Self::READ_ONLY.contains(&self);
430        let destructive = matches!(
431            self,
432            Self::ApplyDdl
433                | Self::EditRow
434                | Self::ImportData
435                | Self::ExecuteSql
436                | Self::RunMaintenance
437                | Self::TerminateQuery
438        );
439        let idempotent = read_only
440            && !matches!(
441                self,
442                Self::RebuildIndex
443                    | Self::RefreshIndex
444                    | Self::RunDoctor
445                    | Self::SetupConnection
446                    | Self::SaveProfile
447                    | Self::ImportProfile
448                    | Self::SwitchConnection
449                    | Self::DiscoverTools
450            );
451        ToolHints {
452            read_only,
453            destructive,
454            idempotent,
455            open_world: true,
456        }
457    }
458}
459
460/// Hints surfaced as MCP `tools/list` annotations.
461#[derive(Debug, Clone, Copy, PartialEq, Eq)]
462pub struct ToolHints {
463    pub read_only: bool,
464    pub destructive: bool,
465    pub idempotent: bool,
466    pub open_world: bool,
467}
468
469#[cfg(test)]
470mod tests {
471    use super::ToolName;
472
473    #[test]
474    fn read_only_is_forty_one_tools() {
475        assert_eq!(ToolName::READ_ONLY.len(), 45);
476    }
477
478    #[test]
479    fn phase9_has_seven_tools() {
480        assert_eq!(ToolName::PHASE9.len(), 7);
481    }
482
483    #[test]
484    fn active_surface_is_fifty_four_tools() {
485        assert_eq!(ToolName::ACTIVE.len(), 54);
486    }
487
488    #[test]
489    fn read_only_subset_of_active() {
490        for tool in ToolName::READ_ONLY {
491            assert!(ToolName::ACTIVE.contains(tool));
492        }
493        assert_ne!(ToolName::READ_ONLY.len(), ToolName::ACTIVE.len());
494    }
495
496    #[test]
497    fn tool_hints_classify_read_and_write_tools() {
498        assert!(ToolName::RunSelect.hints().read_only);
499        assert!(!ToolName::RunSelect.hints().destructive);
500        assert!(!ToolName::ApplyDdl.hints().read_only);
501        assert!(ToolName::ApplyDdl.hints().destructive);
502    }
503
504    /// Guards `docs/tools/README.md` against drifting from the real tool surface:
505    /// every `ToolName::ACTIVE` variant must be named (as `` `snake_case` ``) in the
506    /// doc, and the doc's declared count must equal `ToolName::ACTIVE.len()`.
507    #[test]
508    fn docs_tools_readme_matches_active_surface() {
509        let doc = include_str!("../../../docs/tools/README.md");
510
511        let declared: usize = doc
512            .lines()
513            .next()
514            .and_then(|line| line.split('(').nth(1))
515            .and_then(|rest| rest.split_whitespace().next())
516            .and_then(|n| n.parse().ok())
517            .expect("first line must read \"Active catalog (<N> tools ...)\"");
518        assert_eq!(
519            declared,
520            ToolName::ACTIVE.len(),
521            "docs/tools/README.md's declared tool count is stale"
522        );
523
524        for tool in ToolName::ACTIVE {
525            let needle = format!("`{}`", tool.as_str());
526            assert!(
527                doc.contains(&needle),
528                "docs/tools/README.md is missing `{}` (tool #{:?})",
529                tool.as_str(),
530                tool
531            );
532        }
533    }
534}