1use crate::config::McpConfig;
4use crate::discovery;
5use crate::error::{McpError, Result};
6use crate::types::{McpCapability, McpTool};
7use bytes::Bytes;
8use http_body_util::{BodyExt, Full};
9use hyper::body::Incoming;
10use hyper::server::conn::http1;
11use hyper::Response;
12use hyper_util::rt::TokioIo;
13use rustapi_core::RustApi;
14use rustapi_openapi::OpenApiSpec;
15use std::collections::HashMap;
16use std::convert::Infallible;
17use std::future::Future;
18use std::net::SocketAddr;
19use std::sync::Arc;
20use tokio::net::TcpListener;
21use tracing::{error, info};
22
23#[derive(Debug, Clone)]
29pub struct McpServer {
30 config: Arc<McpConfig>,
31 openapi: Option<OpenApiSpec>,
33 http_base: Option<String>,
36 tool_map: HashMap<String, ToolExecutionInfo>,
39}
40
41#[derive(Debug, Clone)]
44struct ToolExecutionInfo {
45 path_template: String,
47 method: String,
49}
50
51impl McpServer {
52 pub fn new(config: McpConfig) -> Self {
54 Self {
55 config: Arc::new(config),
56 openapi: None,
57 http_base: None,
58 tool_map: HashMap::new(),
59 }
60 }
61
62 pub fn from_rustapi(app: &RustApi, config: McpConfig) -> Self {
66 let mut server = Self::new(config);
67 let spec = app.openapi_spec().clone();
68 server.openapi = Some(spec.clone());
69 server.rebuild_tool_map(&spec);
70 server
71 }
72
73 pub fn from_spec(config: McpConfig, spec: &OpenApiSpec) -> Self {
75 let mut server = Self::new(config);
76 server.openapi = Some(spec.clone());
77 server.rebuild_tool_map(spec);
78 server
79 }
80
81 pub fn with_openapi(mut self, spec: OpenApiSpec) -> Self {
85 self.rebuild_tool_map(&spec);
86 self.openapi = Some(spec);
87 self
88 }
89
90 pub fn with_http_base(mut self, base: impl Into<String>) -> Self {
97 self.http_base = Some(base.into());
98 self
99 }
100
101 fn rebuild_tool_map(&mut self, spec: &OpenApiSpec) {
104 self.tool_map.clear();
105
106 let config = &*self.config;
107
108 let insert_tool = |map: &mut HashMap<String, ToolExecutionInfo>,
109 path: &str,
110 method: &str,
111 op: &rustapi_openapi::Operation| {
112 if !config.allowed_tags.is_empty() {
113 let has_match = op.tags.iter().any(|t| config.allowed_tags.contains(t));
114 if !has_match {
115 return;
116 }
117 }
118
119 let name = generate_tool_name(method, path, op);
120 let info = ToolExecutionInfo {
121 path_template: path.to_string(),
122 method: method.to_string(),
123 };
124 map.insert(name, info);
125 };
126
127 for (path, path_item) in &spec.paths {
128 if !path_matches_prefixes(path, &config.allowed_path_prefixes) {
129 continue;
130 }
131
132 if let Some(op) = &path_item.get {
133 insert_tool(&mut self.tool_map, path, "GET", op);
134 }
135 if let Some(op) = &path_item.post {
136 insert_tool(&mut self.tool_map, path, "POST", op);
137 }
138 if let Some(op) = &path_item.put {
139 insert_tool(&mut self.tool_map, path, "PUT", op);
140 }
141 if let Some(op) = &path_item.patch {
142 insert_tool(&mut self.tool_map, path, "PATCH", op);
143 }
144 if let Some(op) = &path_item.delete {
145 insert_tool(&mut self.tool_map, path, "DELETE", op);
146 }
147 }
148 }
149
150 pub fn config(&self) -> &McpConfig {
152 &self.config
153 }
154
155 pub fn capabilities(&self) -> Vec<McpCapability> {
157 let mut caps = vec![];
158
159 if self.config.tools_enabled {
160 caps.push(McpCapability::Tools);
161 }
162
163 caps
164 }
165
166 pub fn initialize(&self) -> InitializeResult {
170 InitializeResult {
171 name: self.config.name.clone(),
172 version: self.config.version.clone(),
173 description: self.config.description.clone(),
174 capabilities: self.capabilities(),
175 }
176 }
177
178 pub async fn list_tools(&self) -> Result<Vec<McpTool>> {
183 if !self.config.tools_enabled {
184 return Err(crate::error::McpError::CapabilityNotEnabled(
185 "tools".to_string(),
186 ));
187 }
188
189 if let Some(spec) = &self.openapi {
190 let tools = discovery::extract_tools_from_spec(spec, &self.config);
191 Ok(tools)
192 } else {
193 Ok(vec![])
195 }
196 }
197
198 pub async fn call_tool(
204 &self,
205 req: crate::types::ToolCallRequest,
206 ) -> Result<crate::types::ToolCallResponse> {
207 let info = self.tool_map.get(&req.name).ok_or_else(|| {
208 McpError::ToolNotFound(format!("no tool registered with name '{}'", req.name))
209 })?;
210
211 let path = substitute_path_params(&info.path_template, &req.arguments);
212
213 let base = self.http_base.as_deref().unwrap_or("http://127.0.0.1:8080");
214
215 let url = format!("{}{}", base.trim_end_matches('/'), path);
216
217 let method =
218 reqwest::Method::from_bytes(info.method.as_bytes()).unwrap_or(reqwest::Method::GET);
219
220 let client = reqwest::Client::new();
221
222 let mut request_builder = client.request(method.clone(), &url);
223
224 let is_body_method = matches!(info.method.as_str(), "POST" | "PUT" | "PATCH");
226 if is_body_method && !req.arguments.is_empty() {
227 request_builder = request_builder
228 .header("content-type", "application/json")
229 .json(&req.arguments);
230 } else if !is_body_method && !req.arguments.is_empty() {
231 }
234
235 let resp = request_builder.send().await.map_err(|e| {
236 McpError::ToolExecution(format!("failed to proxy tool call to main API: {}", e))
237 })?;
238
239 let status = resp.status();
240 let is_error = !status.is_success();
241
242 let content = if let Ok(text) = resp.text().await {
243 if text.trim().is_empty() {
244 serde_json::Value::Null
245 } else {
246 serde_json::from_str(&text).unwrap_or(serde_json::Value::String(text))
247 }
248 } else {
249 serde_json::Value::Null
250 };
251
252 Ok(crate::types::ToolCallResponse {
253 content,
254 is_error,
255 meta: Some(serde_json::json!({ "proxied_status": status.as_u16() })),
256 })
257 }
258}
259
260#[derive(Debug, Clone)]
262pub struct InitializeResult {
263 pub name: String,
265 pub version: String,
267 pub description: Option<String>,
269 pub capabilities: Vec<McpCapability>,
271}
272
273impl McpServer {
274 pub async fn serve(self, addr: &str) -> Result<()> {
284 self.serve_with_shutdown(addr, std::future::pending()).await
285 }
286
287 pub async fn serve_with_shutdown<F>(self, addr: &str, signal: F) -> Result<()>
289 where
290 F: Future<Output = ()> + Send + 'static,
291 {
292 let addr: SocketAddr = addr
293 .parse()
294 .map_err(|e| McpError::Transport(format!("invalid MCP address '{}': {}", addr, e)))?;
295
296 let listener = TcpListener::bind(addr).await.map_err(|e| {
297 McpError::Transport(format!("failed to bind MCP listener on {}: {}", addr, e))
298 })?;
299
300 info!("🧠 MCP server listening on http://{}", addr);
301
302 let this = Arc::new(self);
303 tokio::pin!(signal);
304
305 loop {
306 tokio::select! {
307 biased;
308
309 accept_result = listener.accept() => {
310 let (stream, remote) = match accept_result {
311 Ok(v) => v,
312 Err(e) => {
313 error!("MCP accept error: {}", e);
314 continue;
315 }
316 };
317
318 let io = TokioIo::new(stream);
319 let mcp = this.clone();
320
321 tokio::task::spawn(async move {
322 let service = hyper::service::service_fn(move |req| {
323 let mcp = mcp.clone();
324 async move { handle_mcp_http_request(mcp, req).await }
325 });
326
327 if let Err(e) = http1::Builder::new()
328 .serve_connection(io, service)
329 .await
330 {
331 error!("MCP connection error from {}: {}", remote, e);
332 }
333 });
334 }
335
336 _ = &mut signal => {
337 info!("MCP server received shutdown signal");
338 break;
339 }
340 }
341 }
342
343 Ok(())
344 }
345}
346
347async fn handle_mcp_http_request(
349 mcp: Arc<McpServer>,
350 req: hyper::Request<Incoming>,
351) -> std::result::Result<Response<Full<Bytes>>, Infallible> {
352 if req.method() != hyper::Method::POST {
353 let body = Bytes::from_static(b"{\"error\":\"MCP transport expects POST requests\"}");
354 return Ok(Response::builder()
355 .status(405)
356 .header("content-type", "application/json")
357 .body(Full::new(body))
358 .expect("static response must build"));
359 }
360
361 let body_bytes = match req.collect().await {
362 Ok(collected) => collected.to_bytes(),
363 Err(e) => {
364 let err_body = format!("{{\"error\":\"failed to read body: {}\"}}", e);
365 return Ok(Response::builder()
366 .status(400)
367 .header("content-type", "application/json")
368 .body(Full::new(Bytes::from(err_body)))
369 .expect("error response must build"));
370 }
371 };
372
373 let json: serde_json::Value = match serde_json::from_slice(&body_bytes) {
374 Ok(v) => v,
375 Err(_) => {
376 return Ok(jsonrpc_error_response(
377 serde_json::Value::Null,
378 -32700,
379 "parse error",
380 ));
381 }
382 };
383
384 let id = json.get("id").cloned().unwrap_or(serde_json::Value::Null);
385 let method = json.get("method").and_then(|m| m.as_str()).unwrap_or("");
386
387 match method {
388 "initialize" => {
389 let init = mcp.initialize();
390 let result = serde_json::json!({
391 "protocolVersion": "2024-11-05",
392 "serverInfo": {
393 "name": init.name,
394 "version": init.version
395 },
396 "capabilities": {
397 "tools": {}
398 }
399 });
400 Ok(jsonrpc_success_response(id, result))
401 }
402 "tools/list" => match mcp.list_tools().await {
403 Ok(tools) => {
404 let tool_defs: Vec<_> = tools
405 .into_iter()
406 .map(|t| {
407 serde_json::json!({
408 "name": t.name,
409 "description": t.description,
410 "inputSchema": t.input_schema
411 })
412 })
413 .collect();
414
415 let result = serde_json::json!({ "tools": tool_defs });
416 Ok(jsonrpc_success_response(id, result))
417 }
418 Err(e) => Ok(jsonrpc_error_response(
419 id,
420 -32603,
421 &format!("internal error: {}", e),
422 )),
423 },
424 "tools/call" => {
425 let params = json.get("params").cloned().unwrap_or(serde_json::json!({}));
426 let name = params
427 .get("name")
428 .and_then(|v| v.as_str())
429 .unwrap_or("")
430 .to_string();
431 let arguments: std::collections::HashMap<String, serde_json::Value> = params
432 .get("arguments")
433 .and_then(|v| serde_json::from_value(v.clone()).ok())
434 .unwrap_or_default();
435
436 let tool_req = crate::types::ToolCallRequest { name, arguments };
437
438 match mcp.call_tool(tool_req).await {
439 Ok(resp) => {
440 let text = if resp.content.is_null() {
444 String::new()
445 } else if let Some(s) = resp.content.as_str() {
446 s.to_owned()
447 } else {
448 serde_json::to_string_pretty(&resp.content)
449 .unwrap_or_else(|_| resp.content.to_string())
450 };
451
452 let result = serde_json::json!({
453 "content": [{
454 "type": "text",
455 "text": text
456 }],
457 "isError": resp.is_error
458 });
459 Ok(jsonrpc_success_response(id, result))
460 }
461 Err(e) => {
462 let result = serde_json::json!({
465 "content": [{
466 "type": "text",
467 "text": format!("Tool execution error: {}", e)
468 }],
469 "isError": true
470 });
471 Ok(jsonrpc_success_response(id, result))
472 }
473 }
474 }
475 _ => Ok(jsonrpc_error_response(id, -32601, "method not found")),
476 }
477}
478
479fn jsonrpc_success_response(
480 id: serde_json::Value,
481 result: serde_json::Value,
482) -> Response<Full<Bytes>> {
483 let body = serde_json::json!({
484 "jsonrpc": "2.0",
485 "id": id,
486 "result": result
487 });
488 Response::builder()
489 .header("content-type", "application/json")
490 .body(Full::new(Bytes::from(
491 serde_json::to_vec(&body).expect("json must serialize"),
492 )))
493 .expect("response must build")
494}
495
496fn jsonrpc_error_response(
497 id: serde_json::Value,
498 code: i32,
499 message: &str,
500) -> Response<Full<Bytes>> {
501 let body = serde_json::json!({
502 "jsonrpc": "2.0",
503 "id": id,
504 "error": {
505 "code": code,
506 "message": message
507 }
508 });
509 Response::builder()
510 .header("content-type", "application/json")
511 .body(Full::new(Bytes::from(
512 serde_json::to_vec(&body).expect("json must serialize"),
513 )))
514 .expect("response must build")
515}
516
517fn path_matches_prefixes(path: &str, prefixes: &[String]) -> bool {
519 if prefixes.is_empty() {
520 return true;
521 }
522 prefixes.iter().any(|p| path.starts_with(p))
523}
524
525fn generate_tool_name(method: &str, path: &str, op: &rustapi_openapi::Operation) -> String {
526 if let Some(oid) = &op.operation_id {
527 return sanitize_name(oid);
528 }
529 let mut slug = path
530 .trim_start_matches('/')
531 .replace(['/', '{', '}', ':'], "_")
532 .replace(['-', '.', ' '], "_");
533 while slug.contains("__") {
534 slug = slug.replace("__", "_");
535 }
536 let slug = slug.trim_matches('_').to_string();
537 let method_lower = method.to_lowercase();
538 if slug.is_empty() {
539 method_lower
540 } else {
541 format!("{}_{}", method_lower, slug)
542 }
543}
544
545fn sanitize_name(s: &str) -> String {
546 s.chars()
547 .map(|c| {
548 if c.is_alphanumeric() || c == '_' {
549 c
550 } else {
551 '_'
552 }
553 })
554 .collect::<String>()
555 .trim_matches('_')
556 .to_string()
557 .to_lowercase()
558}
559
560fn substitute_path_params(
562 template: &str,
563 args: &std::collections::HashMap<String, serde_json::Value>,
564) -> String {
565 let mut result = template.to_string();
566 for (key, value) in args {
567 let placeholder = format!("{{{}}}", key);
568 if result.contains(&placeholder) {
569 let val_str = match value {
570 serde_json::Value::String(s) => s.clone(),
571 other => other.to_string().trim_matches('"').to_string(),
572 };
573 result = result.replace(&placeholder, &val_str);
574 }
575 }
576 result
577}