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};
11
12use reqwest::header::HeaderMap;
13use std::sync::Arc;
14use url::Url;
15
16use crate::error::{OpenApiError, ToolCallError, ToolCallExecutionError, ToolCallValidationError};
17use crate::http_client::HttpClient;
18use crate::openapi::OpenApiSpecLocation;
19use crate::tool_registry::ToolRegistry;
20
21#[derive(Clone)]
22pub struct OpenApiServer {
23 pub spec_location: OpenApiSpecLocation,
24 pub registry: Arc<ToolRegistry>,
25 pub http_client: HttpClient,
26 pub base_url: Option<Url>,
27 pub tag_filter: Option<Vec<String>>,
28 pub method_filter: Option<Vec<reqwest::Method>>,
29}
30
31#[derive(Debug, Clone, serde::Serialize)]
40pub struct ToolMetadata {
41 pub name: String,
43 pub title: Option<String>,
45 pub description: String,
47 pub parameters: Value,
49 pub output_schema: Option<Value>,
51 pub method: String,
53 pub path: String,
55}
56
57impl From<&ToolMetadata> for Tool {
62 fn from(metadata: &ToolMetadata) -> Self {
63 let input_schema = if let Value::Object(obj) = &metadata.parameters {
65 Arc::new(obj.clone())
66 } else {
67 Arc::new(serde_json::Map::new())
68 };
69
70 let output_schema = metadata.output_schema.as_ref().and_then(|schema| {
72 if let Value::Object(obj) = schema {
73 Some(Arc::new(obj.clone()))
74 } else {
75 None
76 }
77 });
78
79 let annotations = metadata.title.as_ref().map(|title| ToolAnnotations {
81 title: Some(title.clone()),
82 ..Default::default()
83 });
84
85 Tool {
86 name: metadata.name.clone().into(),
87 description: Some(metadata.description.clone().into()),
88 input_schema,
89 output_schema,
90 annotations,
91 }
93 }
94}
95
96impl OpenApiServer {
97 #[must_use]
98 pub fn new(spec_location: OpenApiSpecLocation) -> Self {
99 Self {
100 spec_location,
101 registry: Arc::new(ToolRegistry::new()),
102 http_client: HttpClient::new(),
103 base_url: None,
104 tag_filter: None,
105 method_filter: None,
106 }
107 }
108
109 pub fn with_base_url(
115 spec_location: OpenApiSpecLocation,
116 base_url: Url,
117 ) -> Result<Self, OpenApiError> {
118 let http_client = HttpClient::new().with_base_url(base_url.clone())?;
119 Ok(Self {
120 spec_location,
121 registry: Arc::new(ToolRegistry::new()),
122 http_client,
123 base_url: Some(base_url),
124 tag_filter: None,
125 method_filter: None,
126 })
127 }
128
129 pub fn with_base_url_and_headers(
135 spec_location: OpenApiSpecLocation,
136 base_url: Url,
137 default_headers: HeaderMap,
138 ) -> Result<Self, OpenApiError> {
139 let http_client = HttpClient::new()
140 .with_base_url(base_url.clone())?
141 .with_default_headers(default_headers);
142 Ok(Self {
143 spec_location,
144 registry: Arc::new(ToolRegistry::new()),
145 http_client,
146 base_url: Some(base_url),
147 tag_filter: None,
148 method_filter: None,
149 })
150 }
151
152 #[must_use]
154 pub fn with_default_headers(
155 spec_location: OpenApiSpecLocation,
156 default_headers: HeaderMap,
157 ) -> Self {
158 let http_client = HttpClient::new().with_default_headers(default_headers);
159 Self {
160 spec_location,
161 registry: Arc::new(ToolRegistry::new()),
162 http_client,
163 base_url: None,
164 tag_filter: None,
165 method_filter: None,
166 }
167 }
168
169 pub async fn load_openapi_spec(&mut self) -> Result<(), OpenApiError> {
175 let spec = self.spec_location.load_spec().await?;
177 self.register_spec(spec)
178 }
179
180 pub fn register_spec(&mut self, spec: crate::openapi::OpenApiSpec) -> Result<(), OpenApiError> {
186 let registry = Arc::get_mut(&mut self.registry)
188 .ok_or_else(|| OpenApiError::McpError("Registry is already shared".to_string()))?;
189 let registered_count = registry.register_from_spec(
190 spec,
191 self.tag_filter.as_deref(),
192 self.method_filter.as_deref(),
193 )?;
194
195 println!("Loaded {registered_count} tools from OpenAPI spec");
196 println!("Registry stats: {}", self.registry.get_stats().summary());
197
198 Ok(())
199 }
200
201 #[must_use]
203 pub fn tool_count(&self) -> usize {
204 self.registry.tool_count()
205 }
206
207 #[must_use]
209 pub fn get_tool_names(&self) -> Vec<String> {
210 self.registry.get_tool_names()
211 }
212
213 #[must_use]
215 pub fn has_tool(&self, name: &str) -> bool {
216 self.registry.has_tool(name)
217 }
218
219 #[must_use]
221 pub fn get_registry_stats(&self) -> crate::tool_registry::ToolRegistryStats {
222 self.registry.get_stats()
223 }
224
225 #[must_use]
227 pub fn with_tags(mut self, tags: Option<Vec<String>>) -> Self {
228 self.tag_filter = tags;
229 self
230 }
231
232 #[must_use]
234 pub fn with_methods(mut self, methods: Option<Vec<reqwest::Method>>) -> Self {
235 self.method_filter = methods;
236 self
237 }
238
239 pub fn validate_registry(&self) -> Result<(), OpenApiError> {
245 self.registry.validate_registry()
246 }
247}
248
249impl ServerHandler for OpenApiServer {
250 fn get_info(&self) -> InitializeResult {
251 InitializeResult {
252 protocol_version: ProtocolVersion::V_2024_11_05,
253 server_info: Implementation {
254 name: "OpenAPI MCP Server".to_string(),
255 version: "0.1.0".to_string(),
256 },
257 capabilities: ServerCapabilities {
258 tools: Some(ToolsCapability {
259 list_changed: Some(false),
260 }),
261 ..Default::default()
262 },
263 instructions: Some("Exposes OpenAPI endpoints as MCP tools".to_string()),
264 }
265 }
266
267 async fn list_tools(
268 &self,
269 _request: Option<PaginatedRequestParam>,
270 _context: RequestContext<RoleServer>,
271 ) -> Result<ListToolsResult, ErrorData> {
272 let mut tools = Vec::new();
273
274 for tool_metadata in self.registry.get_all_tools() {
276 let tool = Tool::from(tool_metadata);
277 tools.push(tool);
278 }
279
280 Ok(ListToolsResult {
281 tools,
282 next_cursor: None,
283 })
284 }
285
286 async fn call_tool(
287 &self,
288 request: CallToolRequestParam,
289 _context: RequestContext<RoleServer>,
290 ) -> Result<CallToolResult, ErrorData> {
291 if let Some(tool_metadata) = self.registry.get_tool(&request.name) {
293 let arguments = request.arguments.unwrap_or_default();
294 let arguments_value = Value::Object(arguments.clone());
295
296 match self
298 .http_client
299 .execute_tool_call(tool_metadata, &arguments_value)
300 .await
301 {
302 Ok(response) => {
303 let structured_content = if tool_metadata.output_schema.is_some() {
305 match response.json() {
307 Ok(json_value) => {
308 Some(json!({
310 "status": response.status_code,
311 "body": json_value
312 }))
313 }
314 Err(_) => None, }
316 } else {
317 None
318 };
319
320 let content = if let Some(ref structured) = structured_content {
322 match serde_json::to_string(structured) {
326 Ok(json_string) => Some(vec![Content::text(json_string)]),
327 Err(e) => {
328 let error = ToolCallError::Execution(
330 ToolCallExecutionError::ResponseParsingError {
331 reason: format!(
332 "Failed to serialize structured content: {e}"
333 ),
334 raw_response: None,
335 },
336 );
337 return Err(error.into());
338 }
339 }
340 } else {
341 Some(vec![Content::text(response.to_mcp_content())])
342 };
343
344 Ok(CallToolResult {
346 content,
347 structured_content,
348 is_error: Some(!response.is_success),
349 })
350 }
351 Err(e) => {
352 Err(e.into())
354 }
355 }
356 } else {
357 let tool_names = self.registry.get_tool_names();
359 let tool_name_refs: Vec<&str> = tool_names.iter().map(|s| s.as_str()).collect();
360 let suggestions = crate::find_similar_strings(&request.name, &tool_name_refs);
361
362 let error = ToolCallValidationError::ToolNotFound {
364 tool_name: request.name.to_string(),
365 suggestions,
366 };
367 Err(error.into())
368 }
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375 use crate::error::ToolCallError;
376
377 #[test]
378 fn test_tool_not_found_error_with_suggestions() {
379 let mut server = OpenApiServer::new(OpenApiSpecLocation::Url(
381 Url::parse("test://example").unwrap(),
382 ));
383
384 let tool1 = ToolMetadata {
386 name: "getPetById".to_string(),
387 title: Some("Get Pet by ID".to_string()),
388 description: "Find pet by ID".to_string(),
389 parameters: json!({
390 "type": "object",
391 "properties": {
392 "petId": {
393 "type": "integer"
394 }
395 },
396 "required": ["petId"]
397 }),
398 output_schema: None,
399 method: "GET".to_string(),
400 path: "/pet/{petId}".to_string(),
401 };
402
403 let tool2 = ToolMetadata {
404 name: "getPetsByStatus".to_string(),
405 title: Some("Find Pets by Status".to_string()),
406 description: "Find pets by status".to_string(),
407 parameters: json!({
408 "type": "object",
409 "properties": {
410 "status": {
411 "type": "array",
412 "items": {
413 "type": "string"
414 }
415 }
416 },
417 "required": ["status"]
418 }),
419 output_schema: None,
420 method: "GET".to_string(),
421 path: "/pet/findByStatus".to_string(),
422 };
423
424 let registry = Arc::get_mut(&mut server.registry).unwrap();
426
427 let mock_operation = oas3::spec::Operation::default();
429
430 registry
432 .register_tool(
433 tool1,
434 (
435 mock_operation.clone(),
436 "GET".to_string(),
437 "/pet/{petId}".to_string(),
438 ),
439 )
440 .unwrap();
441 registry
442 .register_tool(
443 tool2,
444 (
445 mock_operation,
446 "GET".to_string(),
447 "/pet/findByStatus".to_string(),
448 ),
449 )
450 .unwrap();
451
452 let tool_names = server.registry.get_tool_names();
454 let tool_name_refs: Vec<&str> = tool_names.iter().map(|s| s.as_str()).collect();
455 let suggestions = crate::find_similar_strings("getPetByID", &tool_name_refs);
456
457 let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
458 tool_name: "getPetByID".to_string(),
459 suggestions,
460 });
461 let error_data: ErrorData = error.into();
462 let error_json = serde_json::to_value(&error_data).unwrap();
463
464 insta::assert_json_snapshot!(error_json);
466 }
467
468 #[test]
469 fn test_tool_not_found_error_no_suggestions() {
470 let mut server = OpenApiServer::new(OpenApiSpecLocation::Url(
472 Url::parse("test://example").unwrap(),
473 ));
474
475 let tool = ToolMetadata {
477 name: "getPetById".to_string(),
478 title: Some("Get Pet by ID".to_string()),
479 description: "Find pet by ID".to_string(),
480 parameters: json!({
481 "type": "object",
482 "properties": {
483 "petId": {
484 "type": "integer"
485 }
486 },
487 "required": ["petId"]
488 }),
489 output_schema: None,
490 method: "GET".to_string(),
491 path: "/pet/{petId}".to_string(),
492 };
493
494 let registry = Arc::get_mut(&mut server.registry).unwrap();
496
497 let mock_operation = oas3::spec::Operation::default();
499
500 registry
502 .register_tool(
503 tool,
504 (
505 mock_operation,
506 "GET".to_string(),
507 "/pet/{petId}".to_string(),
508 ),
509 )
510 .unwrap();
511
512 let tool_names = server.registry.get_tool_names();
514 let tool_name_refs: Vec<&str> = tool_names.iter().map(|s| s.as_str()).collect();
515 let suggestions =
516 crate::find_similar_strings("completelyUnrelatedToolName", &tool_name_refs);
517
518 let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
519 tool_name: "completelyUnrelatedToolName".to_string(),
520 suggestions,
521 });
522 let error_data: ErrorData = error.into();
523 let error_json = serde_json::to_value(&error_data).unwrap();
524
525 insta::assert_json_snapshot!(error_json);
527 }
528
529 #[test]
530 fn test_validation_error_converted_to_error_data() {
531 let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
533 violations: vec![crate::error::ValidationError::InvalidParameter {
534 parameter: "page".to_string(),
535 suggestions: vec!["page_number".to_string()],
536 valid_parameters: vec!["page_number".to_string(), "page_size".to_string()],
537 }],
538 });
539
540 let error_data: ErrorData = error.into();
541 let error_json = serde_json::to_value(&error_data).unwrap();
542
543 assert_eq!(error_json["code"], -32602); insta::assert_json_snapshot!(error_json);
548 }
549}