1use rmcp::{
2 RoleServer, ServerHandler,
3 model::{
4 CallToolRequestParam, CallToolResult, Content, ErrorData, Implementation, InitializeResult,
5 ListToolsResult, PaginatedRequestParam, ProtocolVersion, ServerCapabilities, Tool,
6 ToolAnnotations, ToolsCapability,
7 },
8 service::RequestContext,
9};
10use serde_json::{Value, json};
11use std::sync::Arc;
12use url::Url;
13
14use crate::error::{OpenApiError, ToolCallError, ToolCallExecutionError, ToolCallValidationError};
15use crate::http_client::HttpClient;
16use crate::openapi::OpenApiSpecLocation;
17use crate::tool_registry::ToolRegistry;
18
19#[derive(Clone)]
20pub struct OpenApiServer {
21 pub spec_location: OpenApiSpecLocation,
22 pub registry: Arc<ToolRegistry>,
23 pub http_client: HttpClient,
24 pub base_url: Option<Url>,
25}
26
27#[derive(Debug, Clone, serde::Serialize)]
36pub struct ToolMetadata {
37 pub name: String,
39 pub title: Option<String>,
41 pub description: String,
43 pub parameters: Value,
45 pub output_schema: Option<Value>,
47 pub method: String,
49 pub path: String,
51}
52
53impl From<&ToolMetadata> for Tool {
58 fn from(metadata: &ToolMetadata) -> Self {
59 let input_schema = if let Value::Object(obj) = &metadata.parameters {
61 Arc::new(obj.clone())
62 } else {
63 Arc::new(serde_json::Map::new())
64 };
65
66 let output_schema = metadata.output_schema.as_ref().and_then(|schema| {
68 if let Value::Object(obj) = schema {
69 Some(Arc::new(obj.clone()))
70 } else {
71 None
72 }
73 });
74
75 let annotations = metadata.title.as_ref().map(|title| ToolAnnotations {
77 title: Some(title.clone()),
78 ..Default::default()
79 });
80
81 Tool {
82 name: metadata.name.clone().into(),
83 description: Some(metadata.description.clone().into()),
84 input_schema,
85 output_schema,
86 annotations,
87 }
89 }
90}
91
92impl OpenApiServer {
93 #[must_use]
94 pub fn new(spec_location: OpenApiSpecLocation) -> Self {
95 Self {
96 spec_location,
97 registry: Arc::new(ToolRegistry::new()),
98 http_client: HttpClient::new(),
99 base_url: None,
100 }
101 }
102
103 pub fn with_base_url(
109 spec_location: OpenApiSpecLocation,
110 base_url: Url,
111 ) -> Result<Self, OpenApiError> {
112 let http_client = HttpClient::new().with_base_url(base_url.clone())?;
113 Ok(Self {
114 spec_location,
115 registry: Arc::new(ToolRegistry::new()),
116 http_client,
117 base_url: Some(base_url),
118 })
119 }
120
121 pub async fn load_openapi_spec(&mut self) -> Result<(), OpenApiError> {
127 let spec = self.spec_location.load_spec().await?;
129 self.register_spec(spec)
130 }
131
132 pub fn register_spec(&mut self, spec: crate::openapi::OpenApiSpec) -> Result<(), OpenApiError> {
138 let registry = Arc::get_mut(&mut self.registry)
140 .ok_or_else(|| OpenApiError::McpError("Registry is already shared".to_string()))?;
141 let registered_count = registry.register_from_spec(spec)?;
142
143 println!("Loaded {registered_count} tools from OpenAPI spec");
144 println!("Registry stats: {}", self.registry.get_stats().summary());
145
146 Ok(())
147 }
148
149 #[must_use]
151 pub fn tool_count(&self) -> usize {
152 self.registry.tool_count()
153 }
154
155 #[must_use]
157 pub fn get_tool_names(&self) -> Vec<String> {
158 self.registry.get_tool_names()
159 }
160
161 #[must_use]
163 pub fn has_tool(&self, name: &str) -> bool {
164 self.registry.has_tool(name)
165 }
166
167 #[must_use]
169 pub fn get_registry_stats(&self) -> crate::tool_registry::ToolRegistryStats {
170 self.registry.get_stats()
171 }
172
173 pub fn validate_registry(&self) -> Result<(), OpenApiError> {
179 self.registry.validate_registry()
180 }
181}
182
183impl ServerHandler for OpenApiServer {
184 fn get_info(&self) -> InitializeResult {
185 InitializeResult {
186 protocol_version: ProtocolVersion::V_2024_11_05,
187 server_info: Implementation {
188 name: "OpenAPI MCP Server".to_string(),
189 version: "0.1.0".to_string(),
190 },
191 capabilities: ServerCapabilities {
192 tools: Some(ToolsCapability {
193 list_changed: Some(false),
194 }),
195 ..Default::default()
196 },
197 instructions: Some("Exposes OpenAPI endpoints as MCP tools".to_string()),
198 }
199 }
200
201 async fn list_tools(
202 &self,
203 _request: Option<PaginatedRequestParam>,
204 _context: RequestContext<RoleServer>,
205 ) -> Result<ListToolsResult, ErrorData> {
206 let mut tools = Vec::new();
207
208 for tool_metadata in self.registry.get_all_tools() {
210 let tool = Tool::from(tool_metadata);
211 tools.push(tool);
212 }
213
214 Ok(ListToolsResult {
215 tools,
216 next_cursor: None,
217 })
218 }
219
220 async fn call_tool(
221 &self,
222 request: CallToolRequestParam,
223 _context: RequestContext<RoleServer>,
224 ) -> Result<CallToolResult, ErrorData> {
225 if let Some(tool_metadata) = self.registry.get_tool(&request.name) {
227 let arguments = request.arguments.unwrap_or_default();
228 let arguments_value = Value::Object(arguments.clone());
229
230 match self
232 .http_client
233 .execute_tool_call(tool_metadata, &arguments_value)
234 .await
235 {
236 Ok(response) => {
237 let structured_content = if tool_metadata.output_schema.is_some() {
239 match response.json() {
241 Ok(json_value) => {
242 Some(json!({
244 "status": response.status_code,
245 "body": json_value
246 }))
247 }
248 Err(_) => None, }
250 } else {
251 None
252 };
253
254 let content = if let Some(ref structured) = structured_content {
256 match serde_json::to_string(structured) {
260 Ok(json_string) => Some(vec![Content::text(json_string)]),
261 Err(e) => {
262 let error = ToolCallError::Execution(
264 ToolCallExecutionError::ResponseParsingError {
265 reason: format!(
266 "Failed to serialize structured content: {e}"
267 ),
268 raw_response: None,
269 },
270 );
271 return Err(error.into());
272 }
273 }
274 } else {
275 Some(vec![Content::text(response.to_mcp_content())])
276 };
277
278 Ok(CallToolResult {
280 content,
281 structured_content,
282 is_error: Some(!response.is_success),
283 })
284 }
285 Err(e) => {
286 Err(e.into())
288 }
289 }
290 } else {
291 let tool_names = self.registry.get_tool_names();
293 let tool_name_refs: Vec<&str> = tool_names.iter().map(|s| s.as_str()).collect();
294 let suggestions = crate::find_similar_strings(&request.name, &tool_name_refs);
295
296 let error = ToolCallValidationError::ToolNotFound {
298 tool_name: request.name.to_string(),
299 suggestions,
300 };
301 Err(error.into())
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::error::ToolCallError;
310
311 #[test]
312 fn test_tool_not_found_error_with_suggestions() {
313 let mut server = OpenApiServer::new(OpenApiSpecLocation::Url(
315 Url::parse("test://example").unwrap(),
316 ));
317
318 let tool1 = ToolMetadata {
320 name: "getPetById".to_string(),
321 title: Some("Get Pet by ID".to_string()),
322 description: "Find pet by ID".to_string(),
323 parameters: json!({
324 "type": "object",
325 "properties": {
326 "petId": {
327 "type": "integer"
328 }
329 },
330 "required": ["petId"]
331 }),
332 output_schema: None,
333 method: "GET".to_string(),
334 path: "/pet/{petId}".to_string(),
335 };
336
337 let tool2 = ToolMetadata {
338 name: "getPetsByStatus".to_string(),
339 title: Some("Find Pets by Status".to_string()),
340 description: "Find pets by status".to_string(),
341 parameters: json!({
342 "type": "object",
343 "properties": {
344 "status": {
345 "type": "array",
346 "items": {
347 "type": "string"
348 }
349 }
350 },
351 "required": ["status"]
352 }),
353 output_schema: None,
354 method: "GET".to_string(),
355 path: "/pet/findByStatus".to_string(),
356 };
357
358 let registry = Arc::get_mut(&mut server.registry).unwrap();
360
361 let mock_operation = oas3::spec::Operation::default();
363
364 registry
366 .register_tool(
367 tool1,
368 (
369 mock_operation.clone(),
370 "GET".to_string(),
371 "/pet/{petId}".to_string(),
372 ),
373 )
374 .unwrap();
375 registry
376 .register_tool(
377 tool2,
378 (
379 mock_operation,
380 "GET".to_string(),
381 "/pet/findByStatus".to_string(),
382 ),
383 )
384 .unwrap();
385
386 let tool_names = server.registry.get_tool_names();
388 let tool_name_refs: Vec<&str> = tool_names.iter().map(|s| s.as_str()).collect();
389 let suggestions = crate::find_similar_strings("getPetByID", &tool_name_refs);
390
391 let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
392 tool_name: "getPetByID".to_string(),
393 suggestions,
394 });
395 let error_data: ErrorData = error.into();
396 let error_json = serde_json::to_value(&error_data).unwrap();
397
398 insta::assert_json_snapshot!(error_json);
400 }
401
402 #[test]
403 fn test_tool_not_found_error_no_suggestions() {
404 let mut server = OpenApiServer::new(OpenApiSpecLocation::Url(
406 Url::parse("test://example").unwrap(),
407 ));
408
409 let tool = ToolMetadata {
411 name: "getPetById".to_string(),
412 title: Some("Get Pet by ID".to_string()),
413 description: "Find pet by ID".to_string(),
414 parameters: json!({
415 "type": "object",
416 "properties": {
417 "petId": {
418 "type": "integer"
419 }
420 },
421 "required": ["petId"]
422 }),
423 output_schema: None,
424 method: "GET".to_string(),
425 path: "/pet/{petId}".to_string(),
426 };
427
428 let registry = Arc::get_mut(&mut server.registry).unwrap();
430
431 let mock_operation = oas3::spec::Operation::default();
433
434 registry
436 .register_tool(
437 tool,
438 (
439 mock_operation,
440 "GET".to_string(),
441 "/pet/{petId}".to_string(),
442 ),
443 )
444 .unwrap();
445
446 let tool_names = server.registry.get_tool_names();
448 let tool_name_refs: Vec<&str> = tool_names.iter().map(|s| s.as_str()).collect();
449 let suggestions =
450 crate::find_similar_strings("completelyUnrelatedToolName", &tool_name_refs);
451
452 let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
453 tool_name: "completelyUnrelatedToolName".to_string(),
454 suggestions,
455 });
456 let error_data: ErrorData = error.into();
457 let error_json = serde_json::to_value(&error_data).unwrap();
458
459 insta::assert_json_snapshot!(error_json);
461 }
462
463 #[test]
464 fn test_validation_error_converted_to_error_data() {
465 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
467 violations: vec![crate::error::ValidationError::InvalidParameter {
468 parameter: "page".to_string(),
469 suggestions: vec!["page_number".to_string()],
470 valid_parameters: vec!["page_number".to_string(), "page_size".to_string()],
471 }],
472 });
473
474 let error_data: ErrorData = error.into();
475 let error_json = serde_json::to_value(&error_data).unwrap();
476
477 assert_eq!(error_json["code"], -32602); insta::assert_json_snapshot!(error_json);
482 }
483}