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