sqlserver-mcp 0.4.0

SQL Server 2025/2022/2019/2017 - master/msdb/sandbox combined catalog MCP server, generated by mcpify.
Documentation
//! MCP prompts — guided, multi-step SQL Server operational workflows layered
//! on top of the generic `search`/`get`/`call` tool trio (`src/tools/`).
//! Hand-authored (not generated by mcpify): see
//! `docs/mcp-prompts-workflow-plan.md` for the design rationale.

pub mod router;

use rmcp::schemars;
use serde::Deserialize;

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct MasterWorkflowArgs {
    /// What the caller is trying to accomplish, in their own words
    pub goal: Option<String>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SqlAgentJobsArgs {
    /// Name of the SQL Agent job to create, inspect, or manage
    pub job_name: Option<String>,
    /// Target database the job's steps should run against
    pub database: Option<String>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct IndexesConstraintsArgs {
    /// Database containing the table to inspect
    pub database: Option<String>,
    /// Schema containing the table to inspect (e.g. "dbo")
    pub schema: Option<String>,
    /// Table whose indexes/constraints to inspect
    pub table: Option<String>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct IndexTuningRecommendationsArgs {
    /// Database to search for missing-index candidates in
    pub database: Option<String>,
    /// Schema containing the table to focus on (e.g. "dbo")
    pub schema: Option<String>,
    /// Table to focus the missing-index search on
    pub table: Option<String>,
}

#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct SecurityProvisioningArgs {
    /// SQL Server login to create or grant access with
    pub login_name: Option<String>,
    /// Database to grant access/role membership in
    pub database: Option<String>,
    /// Database role the login should be added to
    pub role_name: Option<String>,
}

/// Renders a short "Context already provided" header from `(label, value)`
/// pairs, listing what's already known and what's still missing — prepended
/// to a prompt's static markdown body so its numbered steps don't need to
/// re-ask for parameters the caller already supplied. Returns an empty
/// string for a prompt with no arguments at all, so the header adds nothing
/// to prompts like `sqlserver_workflow_performance_diagnostics`.
pub(crate) fn render_context_header(fields: &[(&str, Option<&str>)]) -> String {
    if fields.is_empty() {
        return String::new();
    }

    let mut provided = Vec::new();
    let mut missing = Vec::new();
    for (label, value) in fields {
        match value {
            Some(v) => provided.push(format!("- {label}: {v}")),
            None => missing.push(format!("- {label}")),
        }
    }

    let render_list = |items: &[String]| {
        if items.is_empty() {
            "(none)".to_string()
        } else {
            items.join("\n")
        }
    };

    format!(
        "Context already provided:\n{}\n\nStill missing (ask the user before proceeding, if needed):\n{}\n",
        render_list(&provided),
        render_list(&missing),
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_fields_render_nothing() {
        assert_eq!(render_context_header(&[]), "");
    }

    #[test]
    fn all_supplied_lists_no_missing_fields() {
        let header = render_context_header(&[("goal", Some("explore schema"))]);
        assert!(header.contains("- goal: explore schema"));
        assert!(header.contains("Still missing"));
        assert!(header.ends_with("(none)\n"));
    }

    #[test]
    fn all_missing_lists_no_provided_fields() {
        let header = render_context_header(&[("goal", None)]);
        assert!(header.starts_with("Context already provided:\n(none)"));
        assert!(header.contains("- goal"));
    }

    #[test]
    fn mixed_supplied_and_missing_are_both_listed() {
        let header =
            render_context_header(&[("job_name", Some("nightly_backup")), ("database", None)]);
        assert!(header.contains("- job_name: nightly_backup"));
        assert!(header.contains("- database"));
        assert!(!header.contains("- database: "));
    }
}