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 IndexTuningRecommendationsArgs, IndexesConstraintsArgs, MasterWorkflowArgs,
15 SecurityProvisioningArgs, SqlAgentJobsArgs, 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 (wait stats, query plans, \
133 transactions, resource governor, I/O, In-Memory OLTP/columnstore \
134 health, OS-level pressure)."
135 )]
136 async fn sqlserver_workflow_performance_diagnostics_prompt(&self) -> Vec<PromptMessage> {
137 vec![PromptMessage::new_text(
138 Role::User,
139 include_str!("content/performance_diagnostics.md"),
140 )]
141 }
142
143 #[prompt(
144 name = "sqlserver_workflow_blocking_and_locks",
145 description = "Diagnose a blocking chain down to its head blocker and last \
146 statement, then — only with explicit user confirmation — \
147 terminate the blocking session and verify the chain cleared."
148 )]
149 async fn sqlserver_workflow_blocking_and_locks_prompt(&self) -> Vec<PromptMessage> {
150 vec![PromptMessage::new_text(
151 Role::User,
152 include_str!("content/blocking_and_locks.md"),
153 )]
154 }
155
156 #[prompt(
157 name = "sqlserver_workflow_index_tuning_recommendations",
158 description = "Find missing-index candidates ranked by estimated improvement, \
159 cross-check for overlap with existing indexes, and — only with \
160 explicit user confirmation — create and verify the index."
161 )]
162 async fn sqlserver_workflow_index_tuning_recommendations_prompt(
163 &self,
164 Parameters(args): Parameters<IndexTuningRecommendationsArgs>,
165 ) -> Vec<PromptMessage> {
166 let header = render_context_header(&[
167 ("database", args.database.as_deref()),
168 ("schema", args.schema.as_deref()),
169 ("table", args.table.as_deref()),
170 ]);
171 vec![PromptMessage::new_text(
172 Role::User,
173 format!(
174 "{header}\n{}",
175 include_str!("content/index_tuning_recommendations.md")
176 ),
177 )]
178 }
179}
180
181#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::auth::auth_manager::AuthManager;
191 use crate::core::config_schema::AuthMethod;
192
193 fn server() -> McpifyServer {
194 let config: crate::core::config_schema::Config =
195 serde_json::from_value(serde_json::json!({
196 "url": "localhost",
197 "auth_method": "sql_server"
198 }))
199 .unwrap();
200 McpifyServer::new(
201 "2025".to_string(),
202 config,
203 std::sync::Arc::new(tokio::sync::Mutex::new(AuthManager::new(
204 AuthMethod::SqlServer,
205 ))),
206 )
207 }
208
209 #[test]
210 fn prompt_router_registers_every_prompt_name() {
211 let prompts = McpifyServer::prompt_router().list_all();
212 let names: std::collections::BTreeSet<&str> =
213 prompts.iter().map(|p| p.name.as_str()).collect();
214 assert_eq!(
215 names,
216 std::collections::BTreeSet::from([
217 "sqlserver_workflow",
218 "sqlserver_workflow_sql_agent_jobs",
219 "sqlserver_workflow_schema_exploration",
220 "sqlserver_workflow_indexes_constraints",
221 "sqlserver_workflow_security_provisioning",
222 "sqlserver_workflow_server_administration",
223 "sqlserver_workflow_performance_diagnostics",
224 "sqlserver_workflow_blocking_and_locks",
225 "sqlserver_workflow_index_tuning_recommendations",
226 ])
227 );
228 }
229
230 #[test]
231 fn security_provisioning_advertises_every_argument_as_optional() {
232 let prompts = McpifyServer::prompt_router().list_all();
233 let security = prompts
234 .iter()
235 .find(|p| p.name == "sqlserver_workflow_security_provisioning")
236 .expect("sqlserver_workflow_security_provisioning is registered");
237 let arguments = security
238 .arguments
239 .as_ref()
240 .expect("prompt declares arguments");
241 let names: std::collections::BTreeSet<&str> =
242 arguments.iter().map(|a| a.name.as_str()).collect();
243 assert_eq!(
244 names,
245 std::collections::BTreeSet::from(["login_name", "database", "role_name"])
246 );
247 for arg in arguments {
248 assert_ne!(
249 arg.required,
250 Some(true),
251 "{} must not be a required argument",
252 arg.name
253 );
254 }
255 }
256
257 #[tokio::test]
258 async fn master_prompt_links_to_the_sql_agent_jobs_sub_workflow() {
259 let messages = server()
260 .sqlserver_workflow_prompt(Parameters(MasterWorkflowArgs { goal: None }))
261 .await;
262 let text = &messages[0].content.as_text().unwrap().text;
263 assert!(text.contains("sqlserver_workflow_sql_agent_jobs"));
264 }
265
266 #[tokio::test]
267 async fn sql_agent_jobs_prompt_echoes_supplied_args_and_lists_missing_ones() {
268 let messages = server()
269 .sqlserver_workflow_sql_agent_jobs_prompt(Parameters(SqlAgentJobsArgs {
270 job_name: Some("nightly_backup".to_string()),
271 database: None,
272 }))
273 .await;
274 let text = &messages[0].content.as_text().unwrap().text;
275 assert!(text.contains("- job_name: nightly_backup"));
276 assert!(text.contains("- database"));
277 assert!(!text.contains("- database: "));
278 }
279
280 #[tokio::test]
281 async fn blocking_and_locks_prompt_gates_termination_on_explicit_confirmation() {
282 let messages = server()
283 .sqlserver_workflow_blocking_and_locks_prompt()
284 .await;
285 let text = &messages[0].content.as_text().unwrap().text;
286 assert!(text.to_lowercase().contains("confirm"));
287 assert!(text.contains("sp_executesql"));
288 }
289
290 #[tokio::test]
291 async fn index_tuning_recommendations_prompt_echoes_supplied_args_and_gates_on_confirmation() {
292 let messages = server()
293 .sqlserver_workflow_index_tuning_recommendations_prompt(Parameters(
294 IndexTuningRecommendationsArgs {
295 database: Some("sandbox".to_string()),
296 schema: None,
297 table: Some("orders".to_string()),
298 },
299 ))
300 .await;
301 let text = &messages[0].content.as_text().unwrap().text;
302 assert!(text.contains("- database: sandbox"));
303 assert!(text.contains("- table: orders"));
304 assert!(text.contains("- schema"));
305 assert!(text.to_lowercase().contains("confirm"));
306 }
307
308 #[tokio::test]
309 async fn performance_diagnostics_points_to_the_dedicated_blocking_workflow() {
310 let messages = server()
311 .sqlserver_workflow_performance_diagnostics_prompt()
312 .await;
313 let text = &messages[0].content.as_text().unwrap().text;
314 assert!(text.contains("sqlserver_workflow_blocking_and_locks"));
315 assert!(text.contains("sqlserver_workflow_index_tuning_recommendations"));
316 }
317}