1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Deserialize)]
5pub struct McpServerConfig {
6 pub name: String,
7 pub enabled: bool,
8 pub command: Option<String>,
9 pub args: Option<Vec<String>>,
10 pub timeout_secs: Option<u64>,
11 pub endpoint: Option<String>,
12 pub transport: Option<String>,
13 pub api_key: Option<String>,
14}
15
16pub struct McpClient {
17 config: McpServerConfig,
18 http: reqwest::Client,
19}
20
21#[derive(Debug, thiserror::Error)]
22pub enum McpError {
23 #[error("HTTP request failed: {0}")]
24 Http(#[from] reqwest::Error),
25 #[error("MCP server returned error: {0}")]
26 ServerError(String),
27 #[error("Tool not found: {0}")]
28 ToolNotFound(String),
29 #[error("Deserialize error: {0}")]
30 Deserialize(#[from] serde_json::Error),
31 #[error("MCP server is disabled")]
32 ServerDisabled,
33 #[error("No endpoint configured")]
34 NoEndpoint,
35}
36
37impl McpClient {
38 pub fn new(config: McpServerConfig) -> Self {
39 let http = reqwest::Client::builder()
40 .timeout(std::time::Duration::from_secs(
41 config.timeout_secs.unwrap_or(30),
42 ))
43 .build()
44 .expect("Failed to create HTTP client");
45
46 Self { config, http }
47 }
48
49 pub fn is_enabled(&self) -> bool {
50 self.config.enabled
51 }
52
53 pub fn name(&self) -> &str {
54 &self.config.name
55 }
56
57 pub async fn get_context(&self, path: &str) -> Result<Value, McpError> {
58 let base_url = self.get_base_url()?;
59 let url = format!("{}/api/v1/context?path={}", base_url, path);
60
61 let mut request = self.http.get(&url);
62 if let Some(ref key) = self.config.api_key {
63 request = request.header("Authorization", format!("Bearer {}", key));
64 }
65
66 let response = request.send().await?;
67 self.handle_response(response).await
68 }
69
70 pub async fn write_context(&self, path: &str, value: &str) -> Result<Value, McpError> {
71 let base_url = self.get_base_url()?;
72 let url = format!("{}/api/v1/context", base_url);
73
74 let body = serde_json::json!({
75 "path": path,
76 "value": value
77 });
78
79 let mut request = self.http.post(&url).json(&body);
80 if let Some(ref key) = self.config.api_key {
81 request = request.header("Authorization", format!("Bearer {}", key));
82 }
83
84 let response = request.send().await?;
85 self.handle_response(response).await
86 }
87
88 pub async fn search_context(&self, query: &str, scope: Option<&str>, max_results: Option<usize>) -> Result<Value, McpError> {
89 let base_url = self.get_base_url()?;
90 let url = format!("{}/api/v1/context/search", base_url);
91
92 let mut body = serde_json::json!({
93 "query": query
94 });
95 if let Some(s) = scope {
96 body["scope"] = serde_json::json!(s);
97 }
98 if let Some(m) = max_results {
99 body["max_results"] = serde_json::json!(m);
100 }
101
102 let mut request = self.http.post(&url).json(&body);
103 if let Some(ref key) = self.config.api_key {
104 request = request.header("Authorization", format!("Bearer {}", key));
105 }
106
107 let response = request.send().await?;
108 self.handle_response(response).await
109 }
110
111 pub async fn get_completeness(&self, scope: Option<&str>) -> Result<Value, McpError> {
112 let base_url = self.get_base_url()?;
113 let scope_part = scope.unwrap_or("*");
114 let url = format!("{}/api/v1/completeness/{}", base_url, scope_part);
115
116 let mut request = self.http.get(&url);
117 if let Some(ref key) = self.config.api_key {
118 request = request.header("Authorization", format!("Bearer {}", key));
119 }
120
121 let response = request.send().await?;
122 self.handle_response(response).await
123 }
124
125 pub async fn get_gaps(&self, status: Option<&str>, category: Option<&str>) -> Result<Value, McpError> {
126 let base_url = self.get_base_url()?;
127 let url = format!("{}/api/v1/gaps", base_url);
128
129 let mut request = self.http.get(&url);
130 if let Some(ref key) = self.config.api_key {
131 request = request.header("Authorization", format!("Bearer {}", key));
132 }
133
134 let response = request.send().await?;
135 self.handle_response(response).await
136 }
137
138 pub async fn detect_gaps(&self, category: Option<&str>) -> Result<Value, McpError> {
139 let base_url = self.get_base_url()?;
140 let url = format!("{}/api/v1/gaps/detect", base_url);
141
142 let body = if let Some(cat) = category {
143 serde_json::json!({ "category": cat })
144 } else {
145 serde_json::json!({})
146 };
147
148 let mut request = self.http.post(&url).json(&body);
149 if let Some(ref key) = self.config.api_key {
150 request = request.header("Authorization", format!("Bearer {}", key));
151 }
152
153 let response = request.send().await?;
154 self.handle_response(response).await
155 }
156
157 fn get_base_url(&self) -> Result<String, McpError> {
158 self.config
159 .endpoint
160 .clone()
161 .ok_or(McpError::NoEndpoint)
162 }
163
164 async fn handle_response(&self, response: reqwest::Response) -> Result<Value, McpError> {
165 if !response.status().is_success() {
166 let status = response.status();
167 let text = response.text().await.unwrap_or_default();
168 return Err(McpError::ServerError(format!("HTTP {}: {}", status, text)));
169 }
170
171 let result: Value = response.json().await?;
172 Ok(result)
173 }
174}