staticmcp_stdio_bridge_lib/
lib.rs

1use serde::{Deserialize, Serialize};
2use serde_json::{Value, json};
3use std::collections::HashMap;
4use std::path::PathBuf;
5use tokio::fs;
6
7#[derive(Debug, Serialize, Deserialize)]
8pub struct MCPRequest {
9    pub jsonrpc: String,
10    pub id: Option<Value>,
11    pub method: String,
12    pub params: Option<Value>,
13}
14
15#[derive(Debug, Serialize, Deserialize)]
16pub struct MCPResponse {
17    pub jsonrpc: String,
18    pub id: Option<Value>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub result: Option<Value>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub error: Option<MCPError>,
23}
24
25#[derive(Debug, Serialize, Deserialize)]
26pub struct MCPError {
27    pub code: i32,
28    pub message: String,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub data: Option<Value>,
31}
32
33#[derive(Debug, Serialize, Deserialize)]
34pub struct MCPManifest {
35    #[serde(rename = "serverInfo")]
36    pub server_info: Option<ServerInfo>,
37    pub capabilities: Option<Capabilities>,
38}
39
40#[derive(Debug, Serialize, Deserialize)]
41pub struct ServerInfo {
42    pub name: String,
43    pub version: String,
44}
45
46#[derive(Debug, Serialize, Deserialize)]
47pub struct Capabilities {
48    pub resources: Option<Vec<Value>>,
49    pub tools: Option<Vec<Value>>,
50}
51
52#[derive(Debug, Clone)]
53pub enum SourceType {
54    Local(PathBuf),
55    Remote(String),
56}
57
58pub struct StaticMCPBridge {
59    pub source: SourceType,
60    pub manifest: Option<MCPManifest>,
61    pub client: reqwest::Client,
62}
63
64impl StaticMCPBridge {
65    pub fn new(source_path: String) -> Self {
66        let source = if source_path.starts_with("http://") || source_path.starts_with("https://") {
67            SourceType::Remote(source_path.trim_end_matches('/').to_string())
68        } else {
69            SourceType::Local(PathBuf::from(source_path))
70        };
71
72        eprintln!(
73            "🔗 Bridge mode: {}",
74            match &source {
75                SourceType::Remote(_) => "Remote",
76                SourceType::Local(_) => "Local",
77            }
78        );
79
80        match &source {
81            SourceType::Remote(url) => eprintln!("📍 Source: {url}"),
82            SourceType::Local(path) => eprintln!("📍 Source: {}", path.display()),
83        }
84
85        Self {
86            source,
87            manifest: None,
88            client: reqwest::Client::new(),
89        }
90    }
91
92    pub async fn load_json(
93        &self,
94        relative_path: &str,
95    ) -> Result<Value, Box<dyn std::error::Error>> {
96        match &self.source {
97            SourceType::Remote(base_url) => {
98                let url = format!("{base_url}/{relative_path}");
99                eprintln!("🌐 Fetching: {url}");
100                let response = self.client.get(&url).send().await?;
101                if !response.status().is_success() {
102                    return Err(format!(
103                        "HTTP {}: {}",
104                        response.status(),
105                        response.status().canonical_reason().unwrap_or("Unknown")
106                    )
107                    .into());
108                }
109                let text = response.text().await?;
110                Ok(serde_json::from_str(&text)?)
111            }
112            SourceType::Local(base_path) => {
113                let full_path = base_path.join(relative_path);
114                eprintln!("📁 Reading: {}", full_path.display());
115                let content = fs::read_to_string(full_path).await?;
116                Ok(serde_json::from_str(&content)?)
117            }
118        }
119    }
120
121    pub async fn load_manifest(&mut self) -> Result<&MCPManifest, Box<dyn std::error::Error>> {
122        if self.manifest.is_none() {
123            let manifest_data = self.load_json("mcp.json").await?;
124            let manifest: MCPManifest = serde_json::from_value(manifest_data)?;
125
126            let server_name = manifest
127                .server_info
128                .as_ref()
129                .map(|s| s.name.as_str())
130                .unwrap_or("Unknown");
131            let server_version = manifest
132                .server_info
133                .as_ref()
134                .map(|s| s.version.as_str())
135                .unwrap_or("0.0.0");
136
137            eprintln!("✅ Loaded manifest: {server_name} v{server_version}");
138            self.manifest = Some(manifest);
139        }
140        Ok(self.manifest.as_ref().unwrap())
141    }
142
143    pub fn uri_to_path(&self, uri: &str) -> String {
144        if uri.starts_with("file://") {
145            format!("resources/{}.json", uri.strip_prefix("file://").unwrap())
146        } else if uri.contains("://") {
147            let parts: Vec<&str> = uri.split("://").collect();
148            if parts.len() == 2 {
149                format!("resources/{}.json", parts[1])
150            } else {
151                format!("{uri}.json")
152            }
153        } else if uri.ends_with(".json") {
154            uri.to_string()
155        } else {
156            format!("{uri}.json")
157        }
158    }
159
160    pub fn tool_to_path(&self, tool_name: &str, args: &HashMap<String, Value>) -> String {
161        let tool_dir = format!("tools/{tool_name}");
162
163        if args.is_empty() {
164            return format!("{tool_dir}.json");
165        }
166
167        if args.len() == 1 {
168            let arg_value = args.values().next().unwrap();
169            let arg_str = match arg_value {
170                Value::String(s) => s.clone(),
171                Value::Number(n) => n.to_string(),
172                Value::Bool(b) => b.to_string(),
173                _ => serde_json::to_string(arg_value).unwrap_or_default(),
174            };
175            return format!("{tool_dir}/{arg_str}.json");
176        }
177
178        if args.len() == 2 {
179            let mut values: Vec<String> = args
180                .values()
181                .map(|v| match v {
182                    Value::String(s) => s.clone(),
183                    Value::Number(n) => n.to_string(),
184                    Value::Bool(b) => b.to_string(),
185                    _ => serde_json::to_string(v).unwrap_or_default(),
186                })
187                .collect();
188            values.sort(); // Ensure consistent ordering
189            return format!("{}/{}/{}.json", tool_dir, values[0], values[1]);
190        }
191
192        // Multiple arguments - create a hash-like path
193        let mut sorted_args: Vec<(String, String)> = args
194            .iter()
195            .map(|(k, v)| {
196                let val_str = match v {
197                    Value::String(s) => s.clone(),
198                    Value::Number(n) => n.to_string(),
199                    Value::Bool(b) => b.to_string(),
200                    _ => serde_json::to_string(v).unwrap_or_default(),
201                };
202                (k.clone(), val_str)
203            })
204            .collect();
205        sorted_args.sort_by(|a, b| a.0.cmp(&b.0));
206
207        let arg_string = sorted_args
208            .iter()
209            .map(|(k, v)| format!("{k}={v}"))
210            .collect::<Vec<_>>()
211            .join("&");
212
213        let hash = base64::Engine::encode(
214            &base64::engine::general_purpose::STANDARD,
215            arg_string.as_bytes(),
216        )
217        .replace(['/', '+', '='], "_");
218
219        format!("{tool_dir}/{hash}.json")
220    }
221
222    pub async fn handle_initialize(&mut self, id: Option<Value>) -> MCPResponse {
223        MCPResponse {
224            jsonrpc: "2.0".to_string(),
225            id,
226            result: Some(json!({
227                "protocolVersion": "2024-11-05",
228                "capabilities": {
229                    "resources": {},
230                    "tools": {}
231                },
232                "serverInfo": {
233                    "name": "generic-static-mcp-bridge",
234                    "version": "1.0.0"
235                }
236            })),
237            error: None,
238        }
239    }
240
241    pub async fn handle_list_resources(&mut self, id: Option<Value>) -> MCPResponse {
242        match self.load_manifest().await {
243            Ok(manifest) => {
244                let resources = manifest
245                    .capabilities
246                    .as_ref()
247                    .and_then(|c| c.resources.as_ref())
248                    .cloned()
249                    .unwrap_or_default();
250
251                eprintln!("📋 Listed {} resources", resources.len());
252
253                MCPResponse {
254                    jsonrpc: "2.0".to_string(),
255                    id,
256                    result: Some(json!({ "resources": resources })),
257                    error: None,
258                }
259            }
260            Err(e) => {
261                eprintln!("❌ Error listing resources: {e}");
262                MCPResponse {
263                    jsonrpc: "2.0".to_string(),
264                    id,
265                    result: None,
266                    error: Some(MCPError {
267                        code: -32603,
268                        message: format!("Failed to list resources: {e}"),
269                        data: None,
270                    }),
271                }
272            }
273        }
274    }
275
276    pub async fn handle_read_resource(&mut self, id: Option<Value>, params: Value) -> MCPResponse {
277        let uri = params.get("uri").and_then(|u| u.as_str()).unwrap_or("");
278        eprintln!("📖 Reading resource: {uri}");
279
280        let resource_path = self.uri_to_path(uri);
281
282        match self.load_json(&resource_path).await {
283            Ok(resource) => {
284                let contents = if let Some(contents) = resource.get("contents") {
285                    contents.clone()
286                } else if resource.get("uri").is_some()
287                    && resource.get("mimeType").is_some()
288                    && resource.get("text").is_some()
289                {
290                    json!([{
291                        "uri": resource["uri"],
292                        "mimeType": resource["mimeType"],
293                        "text": resource["text"]
294                    }])
295                } else {
296                    json!([{
297                        "uri": uri,
298                        "mimeType": "application/json",
299                        "text": serde_json::to_string_pretty(&resource).unwrap_or_default()
300                    }])
301                };
302
303                MCPResponse {
304                    jsonrpc: "2.0".to_string(),
305                    id,
306                    result: Some(json!({ "contents": contents })),
307                    error: None,
308                }
309            }
310            Err(e) => {
311                eprintln!("❌ Error reading resource {uri}: {e}");
312                MCPResponse {
313                    jsonrpc: "2.0".to_string(),
314                    id,
315                    result: None,
316                    error: Some(MCPError {
317                        code: -32603,
318                        message: format!("Failed to read resource {uri}: {e}"),
319                        data: None,
320                    }),
321                }
322            }
323        }
324    }
325
326    pub async fn handle_list_tools(&mut self, id: Option<Value>) -> MCPResponse {
327        match self.load_manifest().await {
328            Ok(manifest) => {
329                let tools = manifest
330                    .capabilities
331                    .as_ref()
332                    .and_then(|c| c.tools.as_ref())
333                    .cloned()
334                    .unwrap_or_default();
335
336                eprintln!("🔧 Listed {} tools", tools.len());
337
338                MCPResponse {
339                    jsonrpc: "2.0".to_string(),
340                    id,
341                    result: Some(json!({ "tools": tools })),
342                    error: None,
343                }
344            }
345            Err(e) => {
346                eprintln!("❌ Error listing tools: {e}");
347                MCPResponse {
348                    jsonrpc: "2.0".to_string(),
349                    id,
350                    result: None,
351                    error: Some(MCPError {
352                        code: -32603,
353                        message: format!("Failed to list tools: {e}"),
354                        data: None,
355                    }),
356                }
357            }
358        }
359    }
360
361    pub async fn handle_call_tool(&mut self, id: Option<Value>, params: Value) -> MCPResponse {
362        let name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
363        let arguments = params
364            .get("arguments")
365            .and_then(|a| a.as_object())
366            .cloned()
367            .unwrap_or_default();
368
369        let args_map: HashMap<String, Value> = arguments.into_iter().collect();
370
371        eprintln!("🛠️  Calling tool: {name} with args: {args_map:?}");
372
373        let tool_path = self.tool_to_path(name, &args_map);
374
375        match self.load_json(&tool_path).await {
376            Ok(result) => {
377                let content = if result.get("content").is_some() || result.get("contents").is_some()
378                {
379                    result
380                } else {
381                    json!({
382                        "content": [{
383                            "type": "text",
384                            "text": serde_json::to_string_pretty(&result).unwrap_or_default()
385                        }]
386                    })
387                };
388
389                MCPResponse {
390                    jsonrpc: "2.0".to_string(),
391                    id,
392                    result: Some(content),
393                    error: None,
394                }
395            }
396            Err(e) => {
397                eprintln!("❌ Error calling tool {name}: {e}");
398                MCPResponse {
399                    jsonrpc: "2.0".to_string(),
400                    id,
401                    result: Some(json!({
402                        "content": [{
403                            "type": "text",
404                            "text": format!("Error calling {}: {}", name, e)
405                        }],
406                        "isError": true
407                    })),
408                    error: None,
409                }
410            }
411        }
412    }
413
414    pub async fn handle_request(&mut self, request: MCPRequest) -> MCPResponse {
415        match request.method.as_str() {
416            "initialize" => self.handle_initialize(request.id).await,
417            "resources/list" => self.handle_list_resources(request.id).await,
418            "resources/read" => {
419                self.handle_read_resource(request.id, request.params.unwrap_or(json!({})))
420                    .await
421            }
422            "tools/list" => self.handle_list_tools(request.id).await,
423            "tools/call" => {
424                self.handle_call_tool(request.id, request.params.unwrap_or(json!({})))
425                    .await
426            }
427            _ => MCPResponse {
428                jsonrpc: "2.0".to_string(),
429                id: request.id,
430                result: None,
431                error: Some(MCPError {
432                    code: -32601,
433                    message: "Method not found".to_string(),
434                    data: None,
435                }),
436            },
437        }
438    }
439}