Skip to main content

mcp_streamable_proxy/proxy_handler/
server_handler.rs

1use super::*;
2
3impl ServerHandler for ProxyHandler {
4    fn get_info(&self) -> ServerInfo {
5        self.discovery.info()
6    }
7
8    /// Connect per-session backends before returning capabilities.
9    async fn initialize(
10        &self,
11        request: InitializeRequestParams,
12        context: RequestContext<RoleServer>,
13    ) -> Result<InitializeResult, ErrorData> {
14        context.peer.set_peer_info(request.clone());
15
16        if let Some(session) = &self.per_session {
17            // Bind notify slot before connect so backend notifications during handshake work.
18            session.notify_slot.set(context.peer.clone());
19            self.ensure_connected().await?;
20            info!(
21                "[initialize] Per-session backend ready - MCP ID: {}",
22                self.mcp_id
23            );
24        }
25
26        let mut info = self.get_info();
27        info.protocol_version =
28            negotiate_protocol_version(&request.protocol_version, info.protocol_version);
29        Ok(info)
30    }
31
32    #[tracing::instrument(skip(self, request, context), fields(
33        mcp_id = %self.mcp_id,
34        request = ?request,
35    ))]
36    async fn list_tools(
37        &self,
38        request: Option<PaginatedRequestParams>,
39        context: RequestContext<RoleServer>,
40    ) -> Result<ListToolsResult, ErrorData> {
41        if context.ct.is_cancelled() {
42            return Err(ErrorData::internal_error(
43                "Request cancelled".to_string(),
44                None,
45            ));
46        }
47        let is_first_page = request
48            .as_ref()
49            .and_then(|params| params.cursor.as_ref())
50            .is_none();
51
52        match self.capabilities().tools {
53            Some(_) => {
54                let result = match self
55                    .forward_backend(
56                        &context,
57                        |peer| async move { peer.list_tools(request).await },
58                    )
59                    .await
60                {
61                    Ok(result) => result,
62                    Err(error) if context.ct.is_cancelled() => return Err(error),
63                    Err(error) => {
64                        if let Some(cached) = self.discovery.tools() {
65                            warn!(%error, "real tools/list failed; returning cached tools snapshot");
66                            return Ok(self.filter_tools(cached));
67                        }
68                        return Err(error);
69                    }
70                };
71                if is_first_page && result.next_cursor.is_none() {
72                    self.update_cached_tools(result.clone());
73                }
74                let result = self.filter_tools(result);
75
76                info!(
77                    "[list_tools] Tool list results - MCP ID: {}, number of tools: {}{}",
78                    self.mcp_id,
79                    result.tools.len(),
80                    if self.tool_filter.is_enabled() {
81                        " (filtered)"
82                    } else {
83                        ""
84                    }
85                );
86
87                debug!(
88                    "Proxying list_tools response with {} tools",
89                    result.tools.len()
90                );
91                Ok(result)
92            }
93            None => {
94                warn!("Server doesn't support tools capability");
95                Ok(ListToolsResult::default())
96            }
97        }
98    }
99
100    #[tracing::instrument(skip(self, request, context), fields(
101        mcp_id = %self.mcp_id,
102        tool_name = %request.name,
103        tool_arguments = ?request.arguments,
104    ))]
105    async fn call_tool(
106        &self,
107        mut request: CallToolRequestParams,
108        context: RequestContext<RoleServer>,
109    ) -> Result<CallToolResponse, ErrorData> {
110        let request_id = REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed);
111        let start = Instant::now();
112        let tool_name = request.name.clone();
113
114        info!(
115            "[call_tool:{}] Start - Tool: {}, MCP ID: {}",
116            request_id, tool_name, self.mcp_id
117        );
118
119        if !self.tool_filter.is_allowed(&tool_name) {
120            info!(
121                "[call_tool:{}] Tool is filtered - MCP ID: {}, Tool: {}",
122                request_id, self.mcp_id, tool_name
123            );
124            return Ok(CallToolResult::error(vec![ContentBlock::text(format!(
125                "Tool '{tool_name}' is not allowed by filter configuration"
126            ))])
127            .into());
128        }
129
130        if self.capabilities().tools.is_none() {
131            error!(
132                "[call_tool:{}] The server does not support tools capability - MCP ID: {}",
133                request_id, self.mcp_id
134            );
135            return Ok(CallToolResult::error(vec![ContentBlock::text(
136                "Server doesn't support tools capability",
137            )])
138            .into());
139        }
140
141        merge_context_meta_into_params(&mut request, &context.meta);
142        let _progress_guard = self.maybe_progress_guard(&context);
143
144        info!(
145            "[call_tool:{}] Send request to backend... - Tool: {}, Elapsed time: {}ms",
146            request_id,
147            tool_name,
148            start.elapsed().as_millis()
149        );
150
151        let call_result = self
152            .forward_backend_with_heartbeat(
153                &context,
154                request_id,
155                tool_name.as_ref(),
156                |peer| async move { peer.call_tool_once(request).await },
157            )
158            .await;
159
160        let result =
161            self.finish_call_tool(request_id, tool_name.as_ref(), start.elapsed(), call_result);
162
163        // SEP-2663: when the backend materializes a task for this call, register
164        // its notification route so backend task-status updates reach the caller.
165        // Shared isolation routes via the registry; per-session bridges 1:1 via
166        // notify_slot and needs no registry entry.
167        if let CallToolResponse::Task(ref task_result) = result
168            && self.isolation == BackendIsolation::Shared
169        {
170            self.upstream_peers
171                .register_task(task_result.task.task_id.clone(), context.peer.clone());
172        }
173
174        info!(
175            "[call_tool:{}] Completed - Tool: {}, total time taken: {}ms",
176            request_id,
177            tool_name,
178            start.elapsed().as_millis()
179        );
180        Ok(result)
181    }
182
183    async fn list_resources(
184        &self,
185        request: Option<PaginatedRequestParams>,
186        context: RequestContext<RoleServer>,
187    ) -> Result<rmcp::model::ListResourcesResult, ErrorData> {
188        match self.capabilities().resources {
189            Some(_) => {
190                let result = self
191                    .forward_backend(&context, |peer| async move {
192                        peer.list_resources(request).await
193                    })
194                    .await?;
195                info!(
196                    "[list_resources] Resource list results - MCP ID: {}, resource quantity: {}",
197                    self.mcp_id,
198                    result.resources.len()
199                );
200                debug!("Proxying list_resources response");
201                Ok(result)
202            }
203            None => {
204                warn!("Server doesn't support resources capability");
205                Ok(rmcp::model::ListResourcesResult::default())
206            }
207        }
208    }
209
210    async fn read_resource(
211        &self,
212        mut request: rmcp::model::ReadResourceRequestParams,
213        context: RequestContext<RoleServer>,
214    ) -> Result<ReadResourceResponse, ErrorData> {
215        match self.capabilities().resources {
216            Some(_) => {
217                merge_context_meta_into_params(&mut request, &context.meta);
218                let _progress_guard = self.maybe_progress_guard(&context);
219                let uri = request.uri.clone();
220                let result = self
221                    .forward_backend(&context, |peer| async move {
222                        peer.read_resource_once(request).await
223                    })
224                    .await?;
225                info!(
226                    "[read_resource] Resource read result - MCP ID: {}, URI: {}",
227                    self.mcp_id, uri
228                );
229                debug!("Proxying read_resource response for {}", uri);
230                Ok(result)
231            }
232            None => {
233                error!("Server doesn't support resources capability");
234                Ok(rmcp::model::ReadResourceResult::new(vec![]).into())
235            }
236        }
237    }
238
239    async fn list_resource_templates(
240        &self,
241        request: Option<PaginatedRequestParams>,
242        context: RequestContext<RoleServer>,
243    ) -> Result<rmcp::model::ListResourceTemplatesResult, ErrorData> {
244        match self.capabilities().resources {
245            Some(_) => {
246                let result = self
247                    .forward_backend(&context, |peer| async move {
248                        peer.list_resource_templates(request).await
249                    })
250                    .await?;
251                debug!("Proxying list_resource_templates response");
252                Ok(result)
253            }
254            None => {
255                warn!("Server doesn't support resources capability");
256                Ok(rmcp::model::ListResourceTemplatesResult::default())
257            }
258        }
259    }
260
261    async fn list_prompts(
262        &self,
263        request: Option<PaginatedRequestParams>,
264        context: RequestContext<RoleServer>,
265    ) -> Result<rmcp::model::ListPromptsResult, ErrorData> {
266        match self.capabilities().prompts {
267            Some(_) => {
268                let result =
269                    self.forward_backend(&context, |peer| async move {
270                        peer.list_prompts(request).await
271                    })
272                    .await?;
273                debug!("Proxying list_prompts response");
274                Ok(result)
275            }
276            None => {
277                warn!("Server doesn't support prompts capability");
278                Ok(rmcp::model::ListPromptsResult::default())
279            }
280        }
281    }
282
283    async fn get_prompt(
284        &self,
285        mut request: rmcp::model::GetPromptRequestParams,
286        context: RequestContext<RoleServer>,
287    ) -> Result<GetPromptResponse, ErrorData> {
288        match self.capabilities().prompts {
289            Some(_) => {
290                merge_context_meta_into_params(&mut request, &context.meta);
291                let _progress_guard = self.maybe_progress_guard(&context);
292                let result = self
293                    .forward_backend(&context, |peer| async move {
294                        peer.get_prompt_once(request).await
295                    })
296                    .await?;
297                debug!("Proxying get_prompt response");
298                Ok(result)
299            }
300            None => {
301                warn!("Server doesn't support prompts capability");
302                let messages = Vec::new();
303                Ok(rmcp::model::GetPromptResult::new(messages).into())
304            }
305        }
306    }
307
308    async fn complete(
309        &self,
310        request: rmcp::model::CompleteRequestParams,
311        context: RequestContext<RoleServer>,
312    ) -> Result<rmcp::model::CompleteResult, ErrorData> {
313        let result = self
314            .forward_backend(&context, |peer| async move { peer.complete(request).await })
315            .await?;
316        debug!("Proxying complete response");
317        Ok(result)
318    }
319
320    #[allow(deprecated)]
321    async fn subscribe(
322        &self,
323        request: SubscribeRequestParams,
324        context: RequestContext<RoleServer>,
325    ) -> Result<(), ErrorData> {
326        match self
327            .capabilities()
328            .resources
329            .as_ref()
330            .and_then(|r| r.subscribe)
331        {
332            Some(true) => {
333                self.forward_backend(
334                    &context,
335                    |peer| async move { peer.subscribe(request).await },
336                )
337                .await
338            }
339            _ => Err(ErrorData::method_not_found::<SubscribeRequestMethod>()),
340        }
341    }
342
343    #[allow(deprecated)]
344    async fn unsubscribe(
345        &self,
346        request: UnsubscribeRequestParams,
347        context: RequestContext<RoleServer>,
348    ) -> Result<(), ErrorData> {
349        match self
350            .capabilities()
351            .resources
352            .as_ref()
353            .and_then(|r| r.subscribe)
354        {
355            Some(true) => {
356                self.forward_backend(
357                    &context,
358                    |peer| async move { peer.unsubscribe(request).await },
359                )
360                .await
361            }
362            _ => Err(ErrorData::method_not_found::<UnsubscribeRequestMethod>()),
363        }
364    }
365
366    #[allow(deprecated)]
367    async fn set_level(
368        &self,
369        request: rmcp::model::SetLevelRequestParams,
370        context: RequestContext<RoleServer>,
371    ) -> Result<(), ErrorData> {
372        if self.capabilities().logging.is_none() {
373            return Err(ErrorData::method_not_found::<SetLevelRequestMethod>());
374        }
375        self.forward_backend(
376            &context,
377            |peer| async move { peer.set_level(request).await },
378        )
379        .await
380    }
381
382    async fn get_task(
383        &self,
384        request: GetTaskParams,
385        context: RequestContext<RoleServer>,
386    ) -> Result<GetTaskResult, ErrorData> {
387        if !self.capabilities().supports_tasks() {
388            return Err(ErrorData::method_not_found::<rmcp::model::GetTaskMethod>());
389        }
390        self.forward_backend(&context, |peer| async move {
391            let result = peer
392                .send_request(ClientRequest::GetTaskRequest(GetTaskRequest::new(request)))
393                .await?;
394            match result {
395                ServerResult::GetTaskResult(r) => Ok(r),
396                _ => Err(ServiceError::UnexpectedResponse),
397            }
398        })
399        .await
400    }
401
402    async fn update_task(
403        &self,
404        request: rmcp::model::UpdateTaskParams,
405        context: RequestContext<RoleServer>,
406    ) -> Result<(), ErrorData> {
407        if !self.capabilities().supports_tasks() {
408            return Err(ErrorData::method_not_found::<rmcp::model::UpdateTaskMethod>());
409        }
410        self.forward_backend(&context, |peer| async move { peer.update_task(request).await })
411            .await
412    }
413
414    async fn cancel_task(
415        &self,
416        request: CancelTaskParams,
417        context: RequestContext<RoleServer>,
418    ) -> Result<(), ErrorData> {
419        if !self.capabilities().supports_tasks() {
420            return Err(ErrorData::method_not_found::<rmcp::model::CancelTaskMethod>());
421        }
422        let task_id = request.task_id.clone();
423        self.forward_backend(&context, |peer| async move { peer.cancel_task(request).await })
424            .await?;
425        self.upstream_peers.unregister_task(&task_id);
426        Ok(())
427    }
428
429    async fn on_initialized(&self, context: NotificationContext<RoleServer>) {
430        if let Some(session) = &self.per_session {
431            // Refresh slot (initialize already connected); keep idempotent ensure.
432            session.notify_slot.set(context.peer.clone());
433            if let Err(err) = self.ensure_connected().await {
434                error!(
435                    "[on_initialized] Per-session backend ensure failed - MCP ID: {}, err: {:?}",
436                    self.mcp_id, err
437                );
438            }
439            return;
440        }
441
442        let id = self.upstream_peers.register(context.peer.clone());
443        info!(
444            "[on_initialized] Registered upstream peer {} - MCP ID: {}, peers: {}",
445            id,
446            self.mcp_id,
447            self.upstream_peers.len()
448        );
449    }
450
451    async fn on_progress(
452        &self,
453        notification: rmcp::model::ProgressNotificationParam,
454        _context: NotificationContext<RoleServer>,
455    ) {
456        let peer = {
457            let inner_guard = self.peer.load();
458            let inner = match inner_guard.as_ref() {
459                Some(inner) => inner,
460                None => {
461                    error!(
462                        "Backend connection is not available, cannot forward progress notification"
463                    );
464                    return;
465                }
466            };
467            if inner.peer.is_transport_closed() {
468                error!("Backend transport is closed, cannot forward progress notification");
469                return;
470            }
471            inner.peer.clone()
472        };
473
474        match peer.notify_progress(notification).await {
475            Ok(_) => {
476                debug!("Proxying progress notification");
477            }
478            Err(err) => {
479                error!("Error notifying progress: {:?}", err);
480            }
481        }
482    }
483
484    async fn on_cancelled(
485        &self,
486        notification: rmcp::model::CancelledNotificationParam,
487        _context: NotificationContext<RoleServer>,
488    ) {
489        let peer = {
490            let inner_guard = self.peer.load();
491            let inner = match inner_guard.as_ref() {
492                Some(inner) => inner,
493                None => {
494                    error!(
495                        "Backend connection is not available, cannot forward cancelled notification"
496                    );
497                    return;
498                }
499            };
500            if inner.peer.is_transport_closed() {
501                error!("Backend transport is closed, cannot forward cancelled notification");
502                return;
503            }
504            inner.peer.clone()
505        };
506
507        match peer.notify_cancelled(notification).await {
508            Ok(_) => {
509                debug!("Proxying cancelled notification");
510            }
511            Err(err) => {
512                error!("Error notifying cancelled: {:?}", err);
513            }
514        }
515    }
516
517    /// Forward non-standard / legacy methods to the backend verbatim.
518    ///
519    /// rmcp 3.1.0 routes any method it does not recognise as a standard MCP
520    /// method here — including spec-removed ones like `tasks/list` and
521    /// `tasks/result` that older clients/backends may still use, plus any vendor
522    /// extension. Delegating to [`ProxyHandler::forward_to_backend`] keeps the
523    /// proxy a fully transparent, version-tolerant bridge: every non-standard
524    /// method is forwarded as a custom request and the backend is the source of
525    /// truth. Standard methods never reach this handler.
526    async fn on_custom_request(
527        &self,
528        request: rmcp::model::CustomRequest,
529        _context: RequestContext<RoleServer>,
530    ) -> Result<rmcp::model::CustomResult, ErrorData> {
531        let method = request.method.clone();
532        let params = request.params.unwrap_or_default();
533        let value = self
534            .forward_to_backend(&method, params)
535            .await
536            .map_err(|error| {
537                ErrorData::internal_error(format!("{method} backend error: {error}"), None)
538            })?;
539        Ok(rmcp::model::CustomResult::new(value))
540    }
541}