sqlserver_mcp_catalog/prompts/
router.rs1use rmcp::handler::server::wrapper::Parameters;
9use rmcp::model::{PromptMessage, Role};
10use rmcp::{prompt, prompt_router};
11
12use crate::core::mcp_server::McpifyServer;
13use crate::prompts::{
14 IndexesConstraintsArgs, MasterWorkflowArgs, SecurityProvisioningArgs, SqlAgentJobsArgs,
15 render_context_header,
16};
17
18#[prompt_router(vis = "pub(crate)")]
19impl McpifyServer {
20 #[prompt(
21 name = "sqlserver_workflow",
22 description = "Start here. Presents the available SQL Server operational \
23 workflows, routes to the right guided sub-workflow based on \
24 the user's goal, and — where the environment supports it — \
25 delegates that whole sub-workflow to an isolated sub-task to \
26 spare this conversation's context window."
27 )]
28 async fn sqlserver_workflow_prompt(
29 &self,
30 Parameters(args): Parameters<MasterWorkflowArgs>,
31 ) -> Vec<PromptMessage> {
32 let header = render_context_header(&[("goal", args.goal.as_deref())]);
33 vec![PromptMessage::new_text(
34 Role::User,
35 format!("{header}\n{}", include_str!("content/master.md")),
36 )]
37 }
38
39 #[prompt(
40 name = "sqlserver_workflow_sql_agent_jobs",
41 description = "Guided SQL Agent job setup: create a job, add one or more \
42 steps, attach a schedule, and start it — each step gated on \
43 the previous one being confirmed to exist, with cleanup \
44 guidance for test runs."
45 )]
46 async fn sqlserver_workflow_sql_agent_jobs_prompt(
47 &self,
48 Parameters(args): Parameters<SqlAgentJobsArgs>,
49 ) -> Vec<PromptMessage> {
50 let header = render_context_header(&[
51 ("job_name", args.job_name.as_deref()),
52 ("database", args.database.as_deref()),
53 ]);
54 vec![PromptMessage::new_text(
55 Role::User,
56 format!("{header}\n{}", include_str!("content/sql_agent_jobs.md")),
57 )]
58 }
59
60 #[prompt(
61 name = "sqlserver_workflow_schema_exploration",
62 description = "Discover databases/schemas/tables/views/columns/types/triggers, \
63 either via `sys.*` catalog views or `INFORMATION_SCHEMA.*`."
64 )]
65 async fn sqlserver_workflow_schema_exploration_prompt(&self) -> Vec<PromptMessage> {
66 vec![PromptMessage::new_text(
67 Role::User,
68 include_str!("content/schema_exploration.md"),
69 )]
70 }
71
72 #[prompt(
73 name = "sqlserver_workflow_indexes_constraints",
74 description = "Inspect indexes, index columns, foreign keys, and check \
75 constraints on a table; estimate compression savings."
76 )]
77 async fn sqlserver_workflow_indexes_constraints_prompt(
78 &self,
79 Parameters(args): Parameters<IndexesConstraintsArgs>,
80 ) -> Vec<PromptMessage> {
81 let header = render_context_header(&[
82 ("database", args.database.as_deref()),
83 ("schema", args.schema.as_deref()),
84 ("table", args.table.as_deref()),
85 ]);
86 vec![PromptMessage::new_text(
87 Role::User,
88 format!(
89 "{header}\n{}",
90 include_str!("content/indexes_constraints.md")
91 ),
92 )]
93 }
94
95 #[prompt(
96 name = "sqlserver_workflow_security_provisioning",
97 description = "Guided login/user/role provisioning, including the \
98 built-in-vs-custom-role fork and the new-login-vs-existing-login fork."
99 )]
100 async fn sqlserver_workflow_security_provisioning_prompt(
101 &self,
102 Parameters(args): Parameters<SecurityProvisioningArgs>,
103 ) -> Vec<PromptMessage> {
104 let header = render_context_header(&[
105 ("login_name", args.login_name.as_deref()),
106 ("database", args.database.as_deref()),
107 ("role_name", args.role_name.as_deref()),
108 ]);
109 vec![PromptMessage::new_text(
110 Role::User,
111 format!(
112 "{header}\n{}",
113 include_str!("content/security_provisioning.md")
114 ),
115 )]
116 }
117
118 #[prompt(
119 name = "sqlserver_workflow_server_administration",
120 description = "Server/database config, renaming objects, disk-space usage, \
121 dependency lookup, bulk per-table/per-db operations, linked servers."
122 )]
123 async fn sqlserver_workflow_server_administration_prompt(&self) -> Vec<PromptMessage> {
124 vec![PromptMessage::new_text(
125 Role::User,
126 include_str!("content/server_administration.md"),
127 )]
128 }
129
130 #[prompt(
131 name = "sqlserver_workflow_performance_diagnostics",
132 description = "Thin pointer to the right read-only signal (active requests/sessions, \
133 wait stats, blocking/locks, transactions, resource governor, I/O)."
134 )]
135 async fn sqlserver_workflow_performance_diagnostics_prompt(&self) -> Vec<PromptMessage> {
136 vec![PromptMessage::new_text(
137 Role::User,
138 include_str!("content/performance_diagnostics.md"),
139 )]
140 }
141}
142
143#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::auth::auth_manager::AuthManager;
153 use crate::core::config_schema::AuthMethod;
154
155 fn server() -> McpifyServer {
156 let config: crate::core::config_schema::Config =
157 serde_json::from_value(serde_json::json!({
158 "url": "localhost",
159 "auth_method": "sql_server"
160 }))
161 .unwrap();
162 McpifyServer::new(
163 "2025".to_string(),
164 config,
165 std::sync::Arc::new(tokio::sync::Mutex::new(AuthManager::new(
166 AuthMethod::SqlServer,
167 ))),
168 )
169 }
170
171 #[test]
172 fn prompt_router_registers_every_prompt_name() {
173 let prompts = McpifyServer::prompt_router().list_all();
174 let names: std::collections::BTreeSet<&str> =
175 prompts.iter().map(|p| p.name.as_str()).collect();
176 assert_eq!(
177 names,
178 std::collections::BTreeSet::from([
179 "sqlserver_workflow",
180 "sqlserver_workflow_sql_agent_jobs",
181 "sqlserver_workflow_schema_exploration",
182 "sqlserver_workflow_indexes_constraints",
183 "sqlserver_workflow_security_provisioning",
184 "sqlserver_workflow_server_administration",
185 "sqlserver_workflow_performance_diagnostics",
186 ])
187 );
188 }
189
190 #[test]
191 fn security_provisioning_advertises_every_argument_as_optional() {
192 let prompts = McpifyServer::prompt_router().list_all();
193 let security = prompts
194 .iter()
195 .find(|p| p.name == "sqlserver_workflow_security_provisioning")
196 .expect("sqlserver_workflow_security_provisioning is registered");
197 let arguments = security
198 .arguments
199 .as_ref()
200 .expect("prompt declares arguments");
201 let names: std::collections::BTreeSet<&str> =
202 arguments.iter().map(|a| a.name.as_str()).collect();
203 assert_eq!(
204 names,
205 std::collections::BTreeSet::from(["login_name", "database", "role_name"])
206 );
207 for arg in arguments {
208 assert_ne!(
209 arg.required,
210 Some(true),
211 "{} must not be a required argument",
212 arg.name
213 );
214 }
215 }
216
217 #[tokio::test]
218 async fn master_prompt_links_to_the_sql_agent_jobs_sub_workflow() {
219 let messages = server()
220 .sqlserver_workflow_prompt(Parameters(MasterWorkflowArgs { goal: None }))
221 .await;
222 let text = &messages[0].content.as_text().unwrap().text;
223 assert!(text.contains("sqlserver_workflow_sql_agent_jobs"));
224 }
225
226 #[tokio::test]
227 async fn sql_agent_jobs_prompt_echoes_supplied_args_and_lists_missing_ones() {
228 let messages = server()
229 .sqlserver_workflow_sql_agent_jobs_prompt(Parameters(SqlAgentJobsArgs {
230 job_name: Some("nightly_backup".to_string()),
231 database: None,
232 }))
233 .await;
234 let text = &messages[0].content.as_text().unwrap().text;
235 assert!(text.contains("- job_name: nightly_backup"));
236 assert!(text.contains("- database"));
237 assert!(!text.contains("- database: "));
238 }
239}