1use mcpkit_core::capability::{ServerCapabilities, ServerInfo};
7use mcpkit_core::error::McpError;
8use mcpkit_core::types::{
9 Content, GetPromptResult, Prompt, PromptMessage, Resource, ResourceContents, Tool,
10 ToolAnnotations, ToolOutput,
11};
12use mcpkit_server::{Context, PromptHandler, ResourceHandler, ServerHandler, ToolHandler};
13use serde_json::Value;
14use std::collections::HashMap;
15use std::future::Future;
16use std::sync::Arc;
17
18pub struct MockTool {
20 pub name: String,
22 pub description: Option<String>,
24 pub input_schema: Value,
26 pub annotations: Option<ToolAnnotations>,
28 pub response: MockResponse,
30}
31
32#[derive(Clone)]
34pub enum MockResponse {
35 Text(String),
37 Json(Value),
39 Error(String),
41 Dynamic(Arc<dyn Fn(Value) -> Result<ToolOutput, McpError> + Send + Sync>),
43}
44
45impl MockTool {
46 pub fn new(name: impl Into<String>) -> Self {
48 Self {
49 name: name.into(),
50 description: None,
51 input_schema: serde_json::json!({
52 "type": "object",
53 "properties": {}
54 }),
55 annotations: None,
56 response: MockResponse::Text("OK".to_string()),
57 }
58 }
59
60 pub fn description(mut self, description: impl Into<String>) -> Self {
62 self.description = Some(description.into());
63 self
64 }
65
66 #[must_use]
68 pub fn input_schema(mut self, schema: Value) -> Self {
69 self.input_schema = schema;
70 self
71 }
72
73 #[must_use]
75 pub fn annotations(mut self, annotations: ToolAnnotations) -> Self {
76 self.annotations = Some(annotations);
77 self
78 }
79
80 pub fn returns_text(mut self, text: impl Into<String>) -> Self {
82 self.response = MockResponse::Text(text.into());
83 self
84 }
85
86 #[must_use]
88 pub fn returns_json(mut self, json: Value) -> Self {
89 self.response = MockResponse::Json(json);
90 self
91 }
92
93 pub fn returns_error(mut self, message: impl Into<String>) -> Self {
95 self.response = MockResponse::Error(message.into());
96 self
97 }
98
99 pub fn handler<F>(mut self, handler: F) -> Self
101 where
102 F: Fn(Value) -> Result<ToolOutput, McpError> + Send + Sync + 'static,
103 {
104 self.response = MockResponse::Dynamic(Arc::new(handler));
105 self
106 }
107
108 #[must_use]
110 pub fn to_tool(&self) -> Tool {
111 Tool {
112 name: self.name.clone(),
113 description: self.description.clone(),
114 input_schema: self.input_schema.clone(),
115 annotations: self.annotations.clone(),
116 }
117 }
118
119 pub fn call(&self, args: Value) -> Result<ToolOutput, McpError> {
121 match &self.response {
122 MockResponse::Text(text) => Ok(ToolOutput::text(text.clone())),
123 MockResponse::Json(json) => Ok(ToolOutput::text(serde_json::to_string_pretty(json)?)),
124 MockResponse::Error(msg) => Ok(ToolOutput::error(msg.clone())),
125 MockResponse::Dynamic(f) => f(args),
126 }
127 }
128}
129
130pub struct MockResource {
132 pub uri: String,
134 pub name: String,
136 pub description: Option<String>,
138 pub mime_type: Option<String>,
140 pub content: String,
142}
143
144impl MockResource {
145 pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
147 Self {
148 uri: uri.into(),
149 name: name.into(),
150 description: None,
151 mime_type: None,
152 content: String::new(),
153 }
154 }
155
156 pub fn description(mut self, description: impl Into<String>) -> Self {
158 self.description = Some(description.into());
159 self
160 }
161
162 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
164 self.mime_type = Some(mime_type.into());
165 self
166 }
167
168 pub fn content(mut self, content: impl Into<String>) -> Self {
170 self.content = content.into();
171 self
172 }
173
174 #[must_use]
176 pub fn to_resource(&self) -> Resource {
177 Resource {
178 uri: self.uri.clone(),
179 name: self.name.clone(),
180 description: self.description.clone(),
181 mime_type: self.mime_type.clone(),
182 size: Some(self.content.len() as u64),
183 annotations: None,
184 }
185 }
186
187 #[must_use]
189 pub fn to_contents(&self) -> ResourceContents {
190 ResourceContents {
191 uri: self.uri.clone(),
192 mime_type: self.mime_type.clone(),
193 text: Some(self.content.clone()),
194 blob: None,
195 }
196 }
197}
198
199pub struct MockPrompt {
201 pub name: String,
203 pub description: Option<String>,
205 pub template: String,
207}
208
209impl MockPrompt {
210 pub fn new(name: impl Into<String>) -> Self {
212 Self {
213 name: name.into(),
214 description: None,
215 template: String::new(),
216 }
217 }
218
219 pub fn description(mut self, description: impl Into<String>) -> Self {
221 self.description = Some(description.into());
222 self
223 }
224
225 pub fn template(mut self, template: impl Into<String>) -> Self {
227 self.template = template.into();
228 self
229 }
230
231 #[must_use]
233 pub fn to_prompt(&self) -> Prompt {
234 Prompt {
235 name: self.name.clone(),
236 description: self.description.clone(),
237 arguments: None,
238 }
239 }
240}
241
242pub struct MockServerBuilder {
244 name: String,
245 version: String,
246 tools: Vec<MockTool>,
247 resources: Vec<MockResource>,
248 prompts: Vec<MockPrompt>,
249}
250
251impl Default for MockServerBuilder {
252 fn default() -> Self {
253 Self::new()
254 }
255}
256
257impl MockServerBuilder {
258 #[must_use]
260 pub fn new() -> Self {
261 Self {
262 name: "mock-server".to_string(),
263 version: "1.0.0".to_string(),
264 tools: Vec::new(),
265 resources: Vec::new(),
266 prompts: Vec::new(),
267 }
268 }
269
270 pub fn name(mut self, name: impl Into<String>) -> Self {
272 self.name = name.into();
273 self
274 }
275
276 pub fn version(mut self, version: impl Into<String>) -> Self {
278 self.version = version.into();
279 self
280 }
281
282 #[must_use]
284 pub fn tool(mut self, tool: MockTool) -> Self {
285 self.tools.push(tool);
286 self
287 }
288
289 pub fn tools(mut self, tools: impl IntoIterator<Item = MockTool>) -> Self {
291 self.tools.extend(tools);
292 self
293 }
294
295 #[must_use]
297 pub fn resource(mut self, resource: MockResource) -> Self {
298 self.resources.push(resource);
299 self
300 }
301
302 #[must_use]
304 pub fn prompt(mut self, prompt: MockPrompt) -> Self {
305 self.prompts.push(prompt);
306 self
307 }
308
309 #[must_use]
311 pub fn build(self) -> MockServer {
312 let tools: HashMap<String, MockTool> = self
313 .tools
314 .into_iter()
315 .map(|t| (t.name.clone(), t))
316 .collect();
317
318 let resources: HashMap<String, MockResource> = self
319 .resources
320 .into_iter()
321 .map(|r| (r.uri.clone(), r))
322 .collect();
323
324 let prompts: HashMap<String, MockPrompt> = self
325 .prompts
326 .into_iter()
327 .map(|p| (p.name.clone(), p))
328 .collect();
329
330 MockServer {
331 name: self.name,
332 version: self.version,
333 tools,
334 resources,
335 prompts,
336 }
337 }
338}
339
340pub struct MockServer {
345 name: String,
346 version: String,
347 tools: HashMap<String, MockTool>,
348 resources: HashMap<String, MockResource>,
349 prompts: HashMap<String, MockPrompt>,
350}
351
352impl MockServer {
353 #[must_use]
366 pub fn builder() -> MockServerBuilder {
367 MockServerBuilder::new()
368 }
369
370 #[must_use]
374 #[deprecated(since = "0.2.6", note = "Use `MockServer::builder()` instead")]
375 pub fn new() -> MockServerBuilder {
376 MockServerBuilder::new()
377 }
378
379 #[must_use]
381 pub fn name(&self) -> &str {
382 &self.name
383 }
384
385 #[must_use]
387 pub fn version(&self) -> &str {
388 &self.version
389 }
390}
391
392impl ServerHandler for MockServer {
393 fn server_info(&self) -> ServerInfo {
394 ServerInfo::new(&self.name, &self.version)
395 }
396
397 fn capabilities(&self) -> ServerCapabilities {
398 let mut caps = ServerCapabilities::new();
399 if !self.tools.is_empty() {
400 caps = caps.with_tools();
401 }
402 if !self.resources.is_empty() {
403 caps = caps.with_resources();
404 }
405 if !self.prompts.is_empty() {
406 caps = caps.with_prompts();
407 }
408 caps
409 }
410}
411
412impl ToolHandler for MockServer {
413 fn list_tools(
414 &self,
415 _ctx: &Context,
416 ) -> impl Future<Output = Result<Vec<Tool>, McpError>> + Send {
417 let tools: Vec<Tool> = self.tools.values().map(MockTool::to_tool).collect();
418 async move { Ok(tools) }
419 }
420
421 fn call_tool(
422 &self,
423 name: &str,
424 args: Value,
425 _ctx: &Context,
426 ) -> impl Future<Output = Result<ToolOutput, McpError>> + Send {
427 let result = if let Some(tool) = self.tools.get(name) {
428 tool.call(args)
429 } else {
430 Err(McpError::method_not_found_with_suggestions(
431 name,
432 self.tools.keys().cloned().collect(),
433 ))
434 };
435 async move { result }
436 }
437}
438
439impl ResourceHandler for MockServer {
440 fn list_resources(
441 &self,
442 _ctx: &Context,
443 ) -> impl Future<Output = Result<Vec<Resource>, McpError>> + Send {
444 let resources: Vec<Resource> = self
445 .resources
446 .values()
447 .map(MockResource::to_resource)
448 .collect();
449 async move { Ok(resources) }
450 }
451
452 fn read_resource(
453 &self,
454 uri: &str,
455 _ctx: &Context,
456 ) -> impl Future<Output = Result<Vec<ResourceContents>, McpError>> + Send {
457 let result = if let Some(resource) = self.resources.get(uri) {
458 Ok(vec![resource.to_contents()])
459 } else {
460 Err(McpError::resource_not_found(uri))
461 };
462 async move { result }
463 }
464}
465
466impl PromptHandler for MockServer {
467 fn list_prompts(
468 &self,
469 _ctx: &Context,
470 ) -> impl Future<Output = Result<Vec<Prompt>, McpError>> + Send {
471 let prompts: Vec<Prompt> = self.prompts.values().map(MockPrompt::to_prompt).collect();
472 async move { Ok(prompts) }
473 }
474
475 fn get_prompt(
476 &self,
477 name: &str,
478 _args: Option<serde_json::Map<String, Value>>,
479 _ctx: &Context,
480 ) -> impl Future<Output = Result<GetPromptResult, McpError>> + Send {
481 let result = if let Some(prompt) = self.prompts.get(name) {
482 Ok(GetPromptResult {
483 description: prompt.description.clone(),
484 messages: vec![PromptMessage {
485 role: mcpkit_core::types::Role::User,
486 content: Content::text(&prompt.template),
487 }],
488 })
489 } else {
490 Err(McpError::method_not_found_with_suggestions(
491 name,
492 self.prompts.keys().cloned().collect(),
493 ))
494 };
495 async move { result }
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn test_mock_tool_text() -> Result<(), Box<dyn std::error::Error>> {
505 let tool = MockTool::new("greet").returns_text("Hello!");
506 let result = tool.call(serde_json::json!({}))?;
507 match result {
508 ToolOutput::Success(r) => {
509 assert!(!r.is_error());
510 }
511 ToolOutput::RecoverableError { .. } => panic!("Expected success"),
512 }
513 Ok(())
514 }
515
516 #[test]
517 fn test_mock_tool_error() -> Result<(), Box<dyn std::error::Error>> {
518 let tool = MockTool::new("fail").returns_error("Something went wrong");
519 let result = tool.call(serde_json::json!({}))?;
520 match result {
521 ToolOutput::RecoverableError { message, .. } => {
522 assert!(message.contains("went wrong"));
523 }
524 ToolOutput::Success(_) => panic!("Expected error"),
525 }
526 Ok(())
527 }
528
529 #[test]
530 fn test_mock_tool_dynamic() -> Result<(), Box<dyn std::error::Error>> {
531 let tool = MockTool::new("add").handler(|args| {
532 let a = args
533 .get("a")
534 .and_then(serde_json::Value::as_f64)
535 .unwrap_or(0.0);
536 let b = args
537 .get("b")
538 .and_then(serde_json::Value::as_f64)
539 .unwrap_or(0.0);
540 Ok(ToolOutput::text(format!("{}", a + b)))
541 });
542
543 let result = tool.call(serde_json::json!({"a": 1, "b": 2}))?;
544 match result {
545 ToolOutput::Success(r) => {
546 if let Content::Text(tc) = &r.content[0] {
547 assert_eq!(tc.text, "3");
548 }
549 }
550 ToolOutput::RecoverableError { .. } => panic!("Expected success"),
551 }
552 Ok(())
553 }
554
555 #[test]
556 fn test_mock_server_builder() {
557 let server = MockServer::builder()
558 .name("test-server")
559 .version("2.0.0")
560 .tool(MockTool::new("test").returns_text("ok"))
561 .resource(MockResource::new("test://resource", "Test Resource").content("Test content"))
562 .build();
563
564 assert_eq!(server.name(), "test-server");
565 assert_eq!(server.version(), "2.0.0");
566
567 let caps = server.capabilities();
568 assert!(caps.has_tools());
569 assert!(caps.has_resources());
570 assert!(!caps.has_prompts());
571 }
572}