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 SecurityProvisioningArgs {
37    /// SQL Server login to create or grant access with
38    pub login_name: Option<String>,
39    /// Database to grant access/role membership in
40    pub database: Option<String>,
41    /// Database role the login should be added to
42    pub role_name: Option<String>,
43}
44
45/// Renders a short "Context already provided" header from `(label, value)`
46/// pairs, listing what's already known and what's still missing — prepended
47/// to a prompt's static markdown body so its numbered steps don't need to
48/// re-ask for parameters the caller already supplied. Returns an empty
49/// string for a prompt with no arguments at all, so the header adds nothing
50/// to prompts like `sqlserver_workflow_performance_diagnostics`.
51pub(crate) fn render_context_header(fields: &[(&str, Option<&str>)]) -> String {
52    if fields.is_empty() {
53        return String::new();
54    }
55
56    let mut provided = Vec::new();
57    let mut missing = Vec::new();
58    for (label, value) in fields {
59        match value {
60            Some(v) => provided.push(format!("- {label}: {v}")),
61            None => missing.push(format!("- {label}")),
62        }
63    }
64
65    let render_list = |items: &[String]| {
66        if items.is_empty() {
67            "(none)".to_string()
68        } else {
69            items.join("\n")
70        }
71    };
72
73    format!(
74        "Context already provided:\n{}\n\nStill missing (ask the user before proceeding, if needed):\n{}\n",
75        render_list(&provided),
76        render_list(&missing),
77    )
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn empty_fields_render_nothing() {
86        assert_eq!(render_context_header(&[]), "");
87    }
88
89    #[test]
90    fn all_supplied_lists_no_missing_fields() {
91        let header = render_context_header(&[("goal", Some("explore schema"))]);
92        assert!(header.contains("- goal: explore schema"));
93        assert!(header.contains("Still missing"));
94        assert!(header.ends_with("(none)\n"));
95    }
96
97    #[test]
98    fn all_missing_lists_no_provided_fields() {
99        let header = render_context_header(&[("goal", None)]);
100        assert!(header.starts_with("Context already provided:\n(none)"));
101        assert!(header.contains("- goal"));
102    }
103
104    #[test]
105    fn mixed_supplied_and_missing_are_both_listed() {
106        let header =
107            render_context_header(&[("job_name", Some("nightly_backup")), ("database", None)]);
108        assert!(header.contains("- job_name: nightly_backup"));
109        assert!(header.contains("- database"));
110        assert!(!header.contains("- database: "));
111    }
112}