Skip to main content

sqlserver_mcp_catalog/prompts/
mod.rs

1//! MCP prompts — guided, multi-step SQL Server operational workflows layered
2//! on top of the generic `search`/`get`/`call` tool trio (`src/tools/`).
3//! Hand-authored (not generated by mcpify): see
4//! `docs/mcp-prompts-workflow-plan.md` for the design rationale.
5
6pub mod router;
7
8use rmcp::schemars;
9use serde::Deserialize;
10
11#[derive(Debug, Deserialize, schemars::JsonSchema)]
12pub struct MasterWorkflowArgs {
13    /// What the caller is trying to accomplish, in their own words
14    pub goal: Option<String>,
15}
16
17#[derive(Debug, Deserialize, schemars::JsonSchema)]
18pub struct SqlAgentJobsArgs {
19    /// Name of the SQL Agent job to create, inspect, or manage
20    pub job_name: Option<String>,
21    /// Target database the job's steps should run against
22    pub database: Option<String>,
23}
24
25#[derive(Debug, Deserialize, schemars::JsonSchema)]
26pub struct IndexesConstraintsArgs {
27    /// Database containing the table to inspect
28    pub database: Option<String>,
29    /// Schema containing the table to inspect (e.g. "dbo")
30    pub schema: Option<String>,
31    /// Table whose indexes/constraints to inspect
32    pub table: Option<String>,
33}
34
35#[derive(Debug, Deserialize, schemars::JsonSchema)]
36pub struct IndexTuningRecommendationsArgs {
37    /// Database to search for missing-index candidates in
38    pub database: Option<String>,
39    /// Schema containing the table to focus on (e.g. "dbo")
40    pub schema: Option<String>,
41    /// Table to focus the missing-index search on
42    pub table: Option<String>,
43}
44
45#[derive(Debug, Deserialize, schemars::JsonSchema)]
46pub struct SecurityProvisioningArgs {
47    /// SQL Server login to create or grant access with
48    pub login_name: Option<String>,
49    /// Database to grant access/role membership in
50    pub database: Option<String>,
51    /// Database role the login should be added to
52    pub role_name: Option<String>,
53}
54
55/// Renders a short "Context already provided" header from `(label, value)`
56/// pairs, listing what's already known and what's still missing — prepended
57/// to a prompt's static markdown body so its numbered steps don't need to
58/// re-ask for parameters the caller already supplied. Returns an empty
59/// string for a prompt with no arguments at all, so the header adds nothing
60/// to prompts like `sqlserver-workflow-performance-diagnostics`.
61pub(crate) fn render_context_header(fields: &[(&str, Option<&str>)]) -> String {
62    if fields.is_empty() {
63        return String::new();
64    }
65
66    let mut provided = Vec::new();
67    let mut missing = Vec::new();
68    for (label, value) in fields {
69        match value {
70            Some(v) => provided.push(format!("- {label}: {v}")),
71            None => missing.push(format!("- {label}")),
72        }
73    }
74
75    let render_list = |items: &[String]| {
76        if items.is_empty() {
77            "(none)".to_string()
78        } else {
79            items.join("\n")
80        }
81    };
82
83    format!(
84        "Context already provided:\n{}\n\nStill missing (ask the user before proceeding, if needed):\n{}\n",
85        render_list(&provided),
86        render_list(&missing),
87    )
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn empty_fields_render_nothing() {
96        assert_eq!(render_context_header(&[]), "");
97    }
98
99    #[test]
100    fn all_supplied_lists_no_missing_fields() {
101        let header = render_context_header(&[("goal", Some("explore schema"))]);
102        assert!(header.contains("- goal: explore schema"));
103        assert!(header.contains("Still missing"));
104        assert!(header.ends_with("(none)\n"));
105    }
106
107    #[test]
108    fn all_missing_lists_no_provided_fields() {
109        let header = render_context_header(&[("goal", None)]);
110        assert!(header.starts_with("Context already provided:\n(none)"));
111        assert!(header.contains("- goal"));
112    }
113
114    #[test]
115    fn mixed_supplied_and_missing_are_both_listed() {
116        let header =
117            render_context_header(&[("job_name", Some("nightly_backup")), ("database", None)]);
118        assert!(header.contains("- job_name: nightly_backup"));
119        assert!(header.contains("- database"));
120        assert!(!header.contains("- database: "));
121    }
122}