rmcp_openapi/
openapi_spec.rs

1use crate::error::OpenApiError;
2use crate::server::ToolMetadata;
3use crate::tool_generator::ToolGenerator;
4use serde_json::Value;
5
6#[derive(Debug, Clone)]
7pub struct OpenApiSpec {
8    pub raw: Value,
9    pub info: OpenApiInfo,
10    pub operations: Vec<OpenApiOperation>,
11}
12
13#[derive(Debug, Clone)]
14pub struct OpenApiInfo {
15    pub title: String,
16    pub version: String,
17    pub description: Option<String>,
18}
19
20#[derive(Debug, Clone)]
21pub struct OpenApiOperation {
22    pub operation_id: String,
23    pub method: String,
24    pub path: String,
25    pub summary: Option<String>,
26    pub description: Option<String>,
27    pub parameters: Vec<OpenApiParameter>,
28}
29
30#[derive(Debug, Clone, PartialEq)]
31pub enum ParameterLocation {
32    Query,
33    Path,
34    Header,
35    Cookie,
36    FormData,
37}
38
39impl std::fmt::Display for ParameterLocation {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            ParameterLocation::Query => write!(f, "query"),
43            ParameterLocation::Path => write!(f, "path"),
44            ParameterLocation::Header => write!(f, "header"),
45            ParameterLocation::Cookie => write!(f, "cookie"),
46            ParameterLocation::FormData => write!(f, "formData"),
47        }
48    }
49}
50
51impl std::convert::TryFrom<&str> for ParameterLocation {
52    type Error = crate::error::OpenApiError;
53
54    fn try_from(s: &str) -> Result<Self, Self::Error> {
55        match s {
56            "query" => Ok(ParameterLocation::Query),
57            "path" => Ok(ParameterLocation::Path),
58            "header" => Ok(ParameterLocation::Header),
59            "cookie" => Ok(ParameterLocation::Cookie),
60            "formData" => Ok(ParameterLocation::FormData),
61            _ => Err(crate::error::OpenApiError::InvalidParameterLocation(
62                s.to_string(),
63            )),
64        }
65    }
66}
67
68#[derive(Debug, Clone)]
69pub struct OpenApiParameter {
70    pub name: String,
71    pub location: ParameterLocation,
72    pub required: bool,
73    pub param_type: String,
74    pub description: Option<String>,
75    pub schema: Value,
76}
77
78impl OpenApiSpec {
79    /// Load and parse an OpenAPI specification from a URL
80    pub async fn from_url(url: &str) -> Result<Self, OpenApiError> {
81        let client = reqwest::Client::new();
82        let response = client.get(url).send().await?;
83        let text = response.text().await?;
84        let json_value: Value = serde_json::from_str(&text)?;
85
86        Self::from_value(json_value)
87    }
88
89    /// Load and parse an OpenAPI specification from a file
90    pub async fn from_file(path: &str) -> Result<Self, OpenApiError> {
91        let content = tokio::fs::read_to_string(path).await?;
92        let json_value: Value = serde_json::from_str(&content)?;
93
94        Self::from_value(json_value)
95    }
96
97    /// Parse an OpenAPI specification from a JSON value
98    pub fn from_value(json_value: Value) -> Result<Self, OpenApiError> {
99        // Extract info section
100        let info_obj = json_value
101            .get("info")
102            .ok_or_else(|| OpenApiError::Spec("Missing 'info' section".to_string()))?;
103
104        let info = OpenApiInfo {
105            title: info_obj
106                .get("title")
107                .and_then(|v| v.as_str())
108                .unwrap_or("Unknown API")
109                .to_string(),
110            version: info_obj
111                .get("version")
112                .and_then(|v| v.as_str())
113                .unwrap_or("1.0.0")
114                .to_string(),
115            description: info_obj
116                .get("description")
117                .and_then(|v| v.as_str())
118                .map(|s| s.to_string()),
119        };
120
121        // Extract and parse all operations
122        let mut operations = Vec::new();
123
124        let paths = json_value
125            .get("paths")
126            .ok_or_else(|| OpenApiError::Spec("Missing 'paths' section".to_string()))?
127            .as_object()
128            .ok_or_else(|| OpenApiError::Spec("'paths' is not an object".to_string()))?;
129
130        for (path, path_obj) in paths {
131            let path_obj = path_obj
132                .as_object()
133                .ok_or_else(|| OpenApiError::Spec(format!("Path '{path}' is not an object")))?;
134
135            for (method, operation_obj) in path_obj {
136                // Skip non-HTTP methods (like parameters, summary, etc.)
137                if ![
138                    "get", "post", "put", "delete", "patch", "head", "options", "trace",
139                ]
140                .contains(&method.as_str())
141                {
142                    continue;
143                }
144
145                let operation =
146                    Self::parse_operation(method.clone(), path.clone(), operation_obj.clone())?;
147                operations.push(operation);
148            }
149        }
150
151        Ok(OpenApiSpec {
152            raw: json_value,
153            info,
154            operations,
155        })
156    }
157
158    /// Parse a single operation from OpenAPI spec
159    fn parse_operation(
160        method: String,
161        path: String,
162        operation_value: Value,
163    ) -> Result<OpenApiOperation, OpenApiError> {
164        let operation_id = operation_value
165            .get("operationId")
166            .and_then(|v| v.as_str())
167            .unwrap_or(&format!(
168                "{}_{}",
169                method,
170                path.replace('/', "_").replace(['{', '}'], "")
171            ))
172            .to_string();
173
174        let summary = operation_value
175            .get("summary")
176            .and_then(|v| v.as_str())
177            .map(|s| s.to_string());
178
179        let description = operation_value
180            .get("description")
181            .and_then(|v| v.as_str())
182            .map(|s| s.to_string());
183
184        let mut parameters = Vec::new();
185
186        if let Some(params_array) = operation_value.get("parameters").and_then(|v| v.as_array()) {
187            for param in params_array {
188                if let Ok(parameter) = Self::parse_parameter(param.clone()) {
189                    parameters.push(parameter);
190                }
191            }
192        }
193
194        Ok(OpenApiOperation {
195            operation_id,
196            method,
197            path,
198            summary,
199            description,
200            parameters,
201        })
202    }
203
204    /// Parse a parameter from OpenAPI spec
205    fn parse_parameter(param_value: Value) -> Result<OpenApiParameter, OpenApiError> {
206        let name = param_value
207            .get("name")
208            .and_then(|v| v.as_str())
209            .ok_or_else(|| OpenApiError::Spec("Parameter missing 'name'".to_string()))?
210            .to_string();
211
212        let location_str = param_value
213            .get("in")
214            .and_then(|v| v.as_str())
215            .ok_or_else(|| OpenApiError::Spec("Parameter missing 'in'".to_string()))?;
216        let location = ParameterLocation::try_from(location_str)?;
217
218        let required = param_value
219            .get("required")
220            .and_then(|v| v.as_bool())
221            .unwrap_or(false);
222
223        let description = param_value
224            .get("description")
225            .and_then(|v| v.as_str())
226            .map(|s| s.to_string());
227
228        let schema = param_value
229            .get("schema")
230            .cloned()
231            .unwrap_or_else(|| serde_json::json!({"type": "string"}));
232
233        let param_type = schema
234            .get("type")
235            .and_then(|v| v.as_str())
236            .unwrap_or("string")
237            .to_string();
238
239        Ok(OpenApiParameter {
240            name,
241            location,
242            required,
243            param_type,
244            description,
245            schema,
246        })
247    }
248
249    /// Convert all operations to MCP tool metadata
250    pub fn to_tool_metadata(&self) -> Result<Vec<ToolMetadata>, OpenApiError> {
251        let mut tools = Vec::new();
252
253        for operation in &self.operations {
254            let tool_metadata = ToolGenerator::generate_tool_metadata(operation)?;
255            tools.push(tool_metadata);
256        }
257
258        Ok(tools)
259    }
260
261    /// Get operation by operation ID
262    pub fn get_operation(&self, operation_id: &str) -> Option<&OpenApiOperation> {
263        self.operations
264            .iter()
265            .find(|op| op.operation_id == operation_id)
266    }
267
268    /// Get all operation IDs
269    pub fn get_operation_ids(&self) -> Vec<String> {
270        self.operations
271            .iter()
272            .map(|op| op.operation_id.clone())
273            .collect()
274    }
275}