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