1use crate::client::{RuntimeMcpServer, RuntimeMcpTransport, ToolExposure};
2use rmcp::{
3 ErrorData as McpError, Peer, RoleServer, ServerHandler,
4 model::{
5 CacheScope, CallToolRequestParams, CallToolResponse, CallToolResult, CancelTaskParams, ClientCapabilities,
6 ContentBlock, CreateTaskResult, DetailedTask, DiscoverResult, GetTaskParams, GetTaskResult, Implementation,
7 ListToolsResult, PaginatedRequestParams, ProgressNotificationParam, ProtocolVersion, ResultType,
8 ServerCapabilities, ServerInfo, Tool, UpdateTaskParams,
9 },
10 service::{DynService, RequestContext},
11};
12use serde_json::json;
13use std::collections::{BTreeMap, HashMap, VecDeque};
14use std::future::Future;
15use std::sync::{Arc, Mutex};
16use std::time::Duration;
17
18pub fn fake_mcp(name: &str, server: FakeMcpServer) -> RuntimeMcpServer {
19 RuntimeMcpServer::new(name, RuntimeMcpTransport::InMemory { server: server.into_dyn() }, ToolExposure::ModelVisible)
20}
21
22#[derive(Clone)]
25pub struct FakeMcpServer {
26 state: FakeMcpState,
27}
28
29#[derive(Clone, Default)]
30pub struct FakeMcpState {
31 inner: Arc<Mutex<FakeMcpStateInner>>,
32}
33
34#[derive(Clone)]
35pub struct CapturedToolCall {
36 pub request: CallToolRequestParams,
37 pub context_meta: serde_json::Map<String, serde_json::Value>,
38}
39
40#[derive(Clone)]
41pub struct CapturedTaskUpdate {
42 pub task_id: String,
43 pub input_responses: rmcp::model::InputResponses,
44}
45
46#[derive(Clone)]
47pub struct FakeTool {
48 definition: Tool,
49 responses: HashMap<Option<String>, FakeToolResponse>,
50 handler: Option<ToolHandler>,
51}
52
53#[derive(Clone)]
54pub struct FakeToolResponse {
55 response: CallToolResponse,
56 delay: Duration,
57 progress: Vec<FakeProgress>,
58 task_progress: Vec<(f64, Option<f64>)>,
59}
60
61#[derive(Clone)]
62struct FakeProgress {
63 progress: f64,
64 total: Option<f64>,
65 message: Option<String>,
66}
67
68impl FakeMcpServer {
69 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub fn with_tool(self, tool: FakeTool) -> Self {
74 self.state.add_tool(tool);
75 self
76 }
77
78 pub fn with_task(self, task_id: impl Into<String>, states: impl IntoIterator<Item = DetailedTask>) -> Self {
79 self.state.script_task(task_id, states);
80 self
81 }
82
83 pub fn with_task_get_failures(self, failures: usize) -> Self {
84 self.state.lock().task_get_failures = failures;
85 self
86 }
87
88 pub fn with_task_update_failures(self, failures: usize) -> Self {
89 self.state.lock().task_update_failures = failures;
90 self
91 }
92
93 pub fn state(&self) -> FakeMcpState {
94 self.state.clone()
95 }
96
97 pub fn into_dyn(self) -> Box<dyn DynService<RoleServer>> {
98 Box::new(self)
99 }
100}
101
102impl FakeMcpState {
103 pub fn calls_for(&self, tool: &str) -> Vec<CapturedToolCall> {
104 self.lock().calls.iter().filter(|call| call.request.name.as_ref() == tool).cloned().collect()
105 }
106
107 pub fn task_get_ids(&self) -> Vec<String> {
108 self.lock().task_get_ids.clone()
109 }
110
111 pub fn task_updates(&self) -> Vec<CapturedTaskUpdate> {
112 self.lock().task_updates.clone()
113 }
114
115 pub fn task_cancel_ids(&self) -> Vec<String> {
116 self.lock().task_cancel_ids.clone()
117 }
118
119 pub fn client_capabilities(&self) -> Option<ClientCapabilities> {
120 self.lock().client_capabilities.clone()
121 }
122
123 pub fn script_task(&self, task_id: impl Into<String>, states: impl IntoIterator<Item = DetailedTask>) {
124 self.lock().tasks.insert(task_id.into(), states.into_iter().collect());
125 }
126
127 fn task_for(&self, task_id: &str) -> Result<Option<DetailedTask>, ()> {
128 let mut inner = self.lock();
129 inner.task_get_ids.push(task_id.to_string());
130 if inner.task_get_failures > 0 {
131 inner.task_get_failures -= 1;
132 return Err(());
133 }
134 let Some(states) = inner.tasks.get_mut(task_id) else {
135 return Ok(None);
136 };
137 Ok(if states.len() > 1 { states.pop_front() } else { states.front().cloned() })
138 }
139
140 fn record_task_update(&self, request: UpdateTaskParams) -> bool {
141 let mut inner = self.lock();
142 inner
143 .task_updates
144 .push(CapturedTaskUpdate { task_id: request.task_id, input_responses: request.input_responses });
145 if inner.task_update_failures > 0 {
146 inner.task_update_failures -= 1;
147 false
148 } else {
149 true
150 }
151 }
152
153 fn record_task_cancel(&self, request: CancelTaskParams) {
154 self.lock().task_cancel_ids.push(request.task_id);
155 }
156
157 pub fn add_tool(&self, tool: FakeTool) {
158 self.lock().tools.insert(tool.definition.name.to_string(), tool);
159 }
160
161 pub async fn add_tool_and_notify(&self, tool: FakeTool) {
162 let peers = {
163 let mut inner = self.lock();
164 inner.tools.insert(tool.definition.name.to_string(), tool);
165 inner.peers.clone()
166 };
167 for peer in peers {
168 let _ = peer.notify_tool_list_changed().await;
169 }
170 }
171
172 pub async fn clear_tools_and_notify(&self) {
173 let peers = {
174 let mut inner = self.lock();
175 inner.tools.clear();
176 inner.peers.clone()
177 };
178 for peer in peers {
179 let _ = peer.notify_tool_list_changed().await;
180 }
181 }
182
183 pub fn fail_next_tool_list(&self) {
184 self.lock().tool_list_failures += 1;
185 }
186
187 fn definitions(&self) -> Vec<Tool> {
188 self.lock().tools.values().map(|tool| tool.definition.clone()).collect()
189 }
190
191 fn response_for(
192 &self,
193 request: &CallToolRequestParams,
194 context_meta: serde_json::Map<String, serde_json::Value>,
195 ) -> Option<FakeToolResponse> {
196 let mut inner = self.lock();
197 inner.calls.push(CapturedToolCall { request: request.clone(), context_meta });
198 inner.tools.get(request.name.as_ref()).and_then(|tool| tool.response_for(request))
199 }
200
201 fn lock(&self) -> std::sync::MutexGuard<'_, FakeMcpStateInner> {
202 self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
203 }
204}
205
206impl FakeTool {
207 pub fn new(name: impl Into<String>) -> Self {
208 let name = name.into();
209 let schema = serde_json::from_value(json!({ "type": "object", "properties": {} }))
210 .expect("empty object schema is valid");
211 Self {
212 definition: Tool::new(name, "Fake MCP tool", Arc::new(schema)),
213 responses: HashMap::new(),
214 handler: None,
215 }
216 }
217
218 pub fn description(mut self, description: impl Into<String>) -> Self {
219 self.definition.description = Some(description.into().into());
220 self
221 }
222
223 pub fn responds(mut self, response: impl Into<FakeToolResponse>) -> Self {
224 self.responses.insert(None, response.into());
225 self
226 }
227
228 pub fn when_state(mut self, state: impl Into<String>, response: impl Into<FakeToolResponse>) -> Self {
229 self.responses.insert(Some(state.into()), response.into());
230 self
231 }
232
233 pub fn responds_with(
236 mut self,
237 handler: impl Fn(&CallToolRequestParams) -> FakeToolResponse + Send + Sync + 'static,
238 ) -> Self {
239 self.handler = Some(Arc::new(handler));
240 self
241 }
242
243 fn response_for(&self, request: &CallToolRequestParams) -> Option<FakeToolResponse> {
244 self.responses
245 .get(&request.request_state.as_deref().map(str::to_string))
246 .cloned()
247 .or_else(|| self.handler.as_ref().map(|handler| handler(request)))
248 }
249}
250
251impl FakeToolResponse {
252 pub fn new(response: impl Into<CallToolResponse>) -> Self {
253 Self { response: response.into(), delay: Duration::ZERO, progress: Vec::new(), task_progress: Vec::new() }
254 }
255
256 pub fn text(text: impl Into<String>) -> Self {
257 Self::new(CallToolResult::success(vec![ContentBlock::text(text.into())]))
258 }
259
260 pub fn task(task: CreateTaskResult) -> Self {
261 Self::new(CallToolResponse::Task(task))
262 }
263
264 pub fn delay(mut self, delay: Duration) -> Self {
265 self.delay = delay;
266 self
267 }
268
269 pub fn progress(mut self, progress: f64, total: Option<f64>) -> Self {
270 self.progress.push(FakeProgress { progress, total, message: None });
271 self
272 }
273
274 pub fn progress_message(mut self, progress: f64, message: impl Into<String>) -> Self {
275 self.progress.push(FakeProgress { progress, total: None, message: Some(message.into()) });
276 self
277 }
278
279 pub fn task_progress(mut self, progress: f64, total: Option<f64>) -> Self {
280 self.task_progress.push((progress, total));
281 self
282 }
283}
284
285impl<T> From<T> for FakeToolResponse
286where
287 T: Into<CallToolResponse>,
288{
289 fn from(response: T) -> Self {
290 Self::new(response)
291 }
292}
293
294impl Default for FakeMcpServer {
295 fn default() -> Self {
296 Self { state: FakeMcpState::default() }
297 .with_tool(add_numbers())
298 .with_tool(divide_numbers())
299 .with_tool(slow_tool())
300 }
301}
302
303impl ServerHandler for FakeMcpServer {
304 fn discover(
305 &self,
306 context: RequestContext<RoleServer>,
307 ) -> impl Future<Output = Result<DiscoverResult, McpError>> + Send + '_ {
308 self.state.lock().client_capabilities = context.meta.client_capabilities();
309 std::future::ready(Ok(DiscoverResult::from_server_info(
310 ServerHandler::supported_protocol_versions(self).into_owned(),
311 ServerHandler::get_info(self),
312 )))
313 }
314
315 fn get_info(&self) -> ServerInfo {
316 ServerInfo::new(ServerCapabilities::builder().enable_tools().enable_tasks().build())
317 .with_server_info(
318 Implementation::new("fake-mcp-server", "0.1.0").with_description("A fake MCP server for testing"),
319 )
320 .with_instructions("A fake MCP server for testing")
321 }
322
323 fn get_task(
324 &self,
325 request: GetTaskParams,
326 _context: RequestContext<RoleServer>,
327 ) -> impl Future<Output = Result<GetTaskResult, McpError>> + Send + '_ {
328 let result = match self.state.task_for(&request.task_id) {
329 Ok(Some(task)) => Ok(GetTaskResult::new(task)),
330 Ok(None) => Err(McpError::invalid_params(format!("unknown task: {}", request.task_id), None)),
331 Err(()) => Err(McpError::internal_error("scripted tasks/get failure", None)),
332 };
333 std::future::ready(result)
334 }
335
336 fn update_task(
337 &self,
338 request: UpdateTaskParams,
339 _context: RequestContext<RoleServer>,
340 ) -> impl Future<Output = Result<(), McpError>> + Send + '_ {
341 std::future::ready(
342 self.state
343 .record_task_update(request)
344 .then_some(())
345 .ok_or_else(|| McpError::internal_error("scripted tasks/update failure", None)),
346 )
347 }
348
349 fn cancel_task(
350 &self,
351 request: CancelTaskParams,
352 _context: RequestContext<RoleServer>,
353 ) -> impl Future<Output = Result<(), McpError>> + Send + '_ {
354 self.state.record_task_cancel(request);
355 std::future::ready(Ok(()))
356 }
357
358 fn list_tools(
359 &self,
360 _request: Option<PaginatedRequestParams>,
361 context: RequestContext<RoleServer>,
362 ) -> impl Future<Output = Result<ListToolsResult, McpError>> + Send + '_ {
363 let supports_cache_hints =
364 context.protocol_version().is_some_and(|version| version >= ProtocolVersion::V_2026_07_28);
365 let tools = {
366 let mut inner = self.state.lock();
367 if inner.tool_list_failures > 0 {
368 inner.tool_list_failures -= 1;
369 return std::future::ready(Err(McpError::internal_error("scripted tools/list failure", None)));
370 }
371 if inner.peers.is_empty() {
372 inner.peers.push(context.peer);
373 }
374 inner.tools.values().map(|tool| tool.definition.clone()).collect()
375 };
376 std::future::ready(Ok(ListToolsResult {
377 result_type: Some(ResultType::COMPLETE),
378 tools,
379 meta: None,
380 next_cursor: None,
381 ttl_ms: supports_cache_hints.then_some(0),
382 cache_scope: supports_cache_hints.then_some(CacheScope::Public),
383 }))
384 }
385
386 fn get_tool(&self, name: &str) -> Option<Tool> {
387 self.state.definitions().into_iter().find(|tool| tool.name == name)
388 }
389
390 async fn call_tool(
391 &self,
392 request: CallToolRequestParams,
393 context: RequestContext<RoleServer>,
394 ) -> Result<CallToolResponse, McpError> {
395 let response = self.state.response_for(&request, context.meta.0.0.clone());
396 let Some(response) = response else {
397 return Err(McpError::invalid_params(format!("unknown tool: {}", request.name), None));
398 };
399
400 if !response.delay.is_zero() {
401 tokio::time::sleep(response.delay).await;
402 }
403 if let Some(token) = context.meta.get_progress_token() {
404 for FakeProgress { progress, total, message } in response.progress {
405 let mut notification = ProgressNotificationParam::new(token.clone(), progress);
406 if let Some(total) = total {
407 notification = notification.with_total(total);
408 }
409 if let Some(message) = message {
410 notification = notification.with_message(message);
411 }
412 let _ = context.peer.notify_progress(notification).await;
413 }
414 if !response.task_progress.is_empty() {
415 let peer = context.peer.clone();
416 let token = token.clone();
417 tokio::spawn(async move {
418 tokio::task::yield_now().await;
419 for (progress, total) in response.task_progress {
420 let mut notification = ProgressNotificationParam::new(token.clone(), progress);
421 if let Some(total) = total {
422 notification = notification.with_total(total);
423 }
424 let _ = peer.notify_progress(notification).await;
425 }
426 });
427 }
428 }
429 Ok(response.response)
430 }
431}
432
433type ToolHandler = Arc<dyn Fn(&CallToolRequestParams) -> FakeToolResponse + Send + Sync>;
434
435#[derive(Default)]
436struct FakeMcpStateInner {
437 tools: BTreeMap<String, FakeTool>,
438 calls: Vec<CapturedToolCall>,
439 client_capabilities: Option<ClientCapabilities>,
440 tasks: HashMap<String, VecDeque<DetailedTask>>,
441 task_get_ids: Vec<String>,
442 task_updates: Vec<CapturedTaskUpdate>,
443 task_cancel_ids: Vec<String>,
444 task_get_failures: usize,
445 task_update_failures: usize,
446 tool_list_failures: usize,
447 peers: Vec<Peer<RoleServer>>,
448}
449
450fn add_numbers() -> FakeTool {
451 FakeTool::new("add_numbers").description("Adds two numbers together").responds_with(|request| {
452 let sum = int_arg(request, "a") + int_arg(request, "b");
453 FakeToolResponse::new(CallToolResult::structured(json!({ "sum": sum })))
454 })
455}
456
457fn divide_numbers() -> FakeTool {
458 FakeTool::new("divide_numbers").description("Divides two numbers").responds_with(|request| {
459 let (a, b) = (int_arg(request, "a"), int_arg(request, "b"));
460 if b == 0 {
461 return FakeToolResponse::new(CallToolResult::error(vec![ContentBlock::text("Division by zero")]));
462 }
463 FakeToolResponse::new(CallToolResult::structured(json!({ "quotient": a / b })))
464 })
465}
466
467fn slow_tool() -> FakeTool {
468 FakeTool::new("slow_tool")
469 .description("A tool that sleeps for a specified duration (for testing timeouts)")
470 .responds_with(|request| {
471 let sleep_ms = int_arg(request, "sleep_ms").unsigned_abs();
472 FakeToolResponse::new(CallToolResult::structured(json!({ "message": format!("Slept for {sleep_ms}ms") })))
473 .delay(Duration::from_millis(sleep_ms))
474 })
475}
476
477fn int_arg(request: &CallToolRequestParams, name: &str) -> i64 {
478 request.arguments.as_ref().and_then(|args| args.get(name)).and_then(serde_json::Value::as_i64).unwrap_or_default()
479}