sqlserver_mcp_catalog/core/
mcp_server.rs1use std::sync::Arc;
4
5use rmcp::handler::server::router::tool::ToolRouter;
6use rmcp::handler::server::wrapper::Parameters;
7use rmcp::model::{
8 CallToolResult, ContentBlock, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo,
9};
10use rmcp::service::RequestContext;
11use rmcp::transport::stdio;
12use rmcp::{
13 ErrorData as McpError, RoleServer, ServerHandler, ServiceExt, schemars, tool, tool_handler,
14 tool_router,
15};
16use serde::Deserialize;
17use tokio::sync::Mutex;
18
19use crate::auth::auth_manager::AuthManager;
20use crate::core::config_schema::Config;
21use crate::core::errors::McpifyError;
22use crate::data::store::{cached_store_connection, get_endpoint};
23use crate::tools::call_tool::call_operation;
24use crate::tools::get_tool::get_operation;
25use crate::tools::search_tool::search_operations;
26
27fn default_search_limit() -> usize {
28 5
29}
30
31#[derive(Debug, Deserialize, schemars::JsonSchema)]
32pub struct SearchArgs {
33 pub query: String,
35 #[serde(default = "default_search_limit")]
37 pub limit: usize,
38}
39
40#[derive(Debug, Deserialize, schemars::JsonSchema)]
41pub struct GetArgs {
42 pub operation_id: String,
44}
45
46fn default_call_arguments() -> serde_json::Value {
51 serde_json::json!({})
52}
53
54#[derive(Debug, Deserialize, schemars::JsonSchema)]
55pub struct CallArgs {
56 pub operation_id: String,
58 #[serde(default = "default_call_arguments")]
60 pub arguments: serde_json::Value,
61}
62
63#[derive(Clone)]
68pub struct McpifyServer {
69 api_version: String,
70 config: Config,
71 auth_manager: Arc<Mutex<AuthManager>>,
72 tool_router: ToolRouter<McpifyServer>,
73}
74
75#[tool_router]
76impl McpifyServer {
77 pub fn new(api_version: String, config: Config, auth_manager: Arc<Mutex<AuthManager>>) -> Self {
85 Self {
86 api_version,
87 config,
88 auth_manager,
89 tool_router: Self::tool_router(),
90 }
91 }
92
93 #[tool(
94 description = "Semantic search for SQL Server 2025 - master/msdb/sandbox combined catalog operations using a natural-language query."
95 )]
96 async fn search(
97 &self,
98 Parameters(args): Parameters<SearchArgs>,
99 ) -> Result<CallToolResult, McpError> {
100 let api_version = self.api_version.clone();
101 self.run_tool("search", async move {
102 let conn = cached_store_connection(&api_version)?.lock().unwrap();
103 search_operations(&conn, &args.query, args.limit)
104 })
105 .await
106 }
107
108 #[tool(
109 description = "Return the schema, path, method, and documentation for a specific SQL Server 2025 - master/msdb/sandbox combined catalog operationId."
110 )]
111 async fn get(&self, Parameters(args): Parameters<GetArgs>) -> Result<CallToolResult, McpError> {
112 let api_version = self.api_version.clone();
113 self.run_tool("get", async move {
114 let conn = cached_store_connection(&api_version)?.lock().unwrap();
115 get_operation(&conn, &args.operation_id)
116 })
117 .await
118 }
119
120 #[tool(
121 description = "Validate arguments, invoke a live SQL Server 2025 - master/msdb/sandbox combined catalog API operation, and validate the response."
122 )]
123 async fn call(
124 &self,
125 Parameters(args): Parameters<CallArgs>,
126 _context: RequestContext<RoleServer>,
127 ) -> Result<CallToolResult, McpError> {
128 let api_version = self.api_version.clone();
129 let config = self.config.clone();
130 let auth_manager = self.auth_manager.clone();
131
132 self.run_tool("call", async move {
133 let endpoint = {
138 let conn = cached_store_connection(&api_version)?.lock().unwrap();
139 get_endpoint(&conn, &args.operation_id)?.ok_or_else(|| {
140 McpifyError::NotFound(format!("unknown operationId '{}'", args.operation_id))
141 })?
142 };
143
144 let mut auth_manager = auth_manager.lock().await;
145 call_operation(
146 &endpoint,
147 &config,
148 &mut auth_manager,
149 &args.operation_id,
150 args.arguments,
151 )
152 .await
153 })
154 .await
155 }
156}
157
158impl McpifyServer {
159 async fn run_tool<F>(&self, tool_name: &str, fut: F) -> Result<CallToolResult, McpError>
164 where
165 F: std::future::Future<Output = anyhow::Result<serde_json::Value>>,
166 {
167 match fut.await {
168 Ok(value) => {
169 let text =
170 serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string());
171 Ok(CallToolResult::success(vec![ContentBlock::text(text)]))
172 }
173 Err(err) => {
174 tracing::error!(tool = tool_name, error = %err, "tool execution failed");
175 Ok(CallToolResult::error(vec![ContentBlock::text(
176 err.to_string(),
177 )]))
178 }
179 }
180 }
181}
182
183#[tool_handler(router = self.tool_router.clone())]
188impl ServerHandler for McpifyServer {
189 fn get_info(&self) -> ServerInfo {
190 ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
191 .with_server_info(Implementation::from_build_env())
192 .with_protocol_version(ProtocolVersion::V_2024_11_05)
193 .with_instructions(
194 "Exposes exactly 3 tools -- search, get, call -- backed by an embedded \
195 semantic database, so you never need the full API surface in context."
196 .to_string(),
197 )
198 }
199}
200
201pub async fn connect_stdio<S>(server: S) -> anyhow::Result<()>
205where
206 S: rmcp::ServerHandler,
207{
208 let running = server.serve(stdio()).await?;
209 tracing::info!("MCP server connected over stdio");
210 running.waiting().await?;
211 Ok(())
212}