1use std::process::Stdio;
2#[cfg(feature = "oauth")]
3use std::sync::Arc;
4
5use rmcp::model::{
6 CallToolRequestParams, CallToolResponse, ClientCapabilities, ClientInfo,
7 GetPromptRequestParams, GetPromptResponse, Implementation, ProtocolVersion,
8 ReadResourceRequestParams, ReadResourceResponse, RequestMetaObject,
9};
10use rmcp::service::{ClientInitializeError, ClientServiceExt, RunningService};
11use rmcp::transport::{
12 StreamableHttpClientTransport, TokioChildProcess,
13 streamable_http_client::StreamableHttpClientTransportConfig,
14};
15use rmcp::{ClientHandler, RoleClient};
16use serde_json::{Map, Value};
17use tokio::process::Command;
18
19use crate::config::UpstreamConfig;
20#[cfg(feature = "oauth")]
21use crate::oauth::UpstreamOAuthProvider;
22use crate::process::guard::SpawnGuard;
23use crate::upstream::http_body_cap::BodyCappedHttpClient;
24use crate::upstream::http_client::{HttpTransportDecision, decide_http_transport};
25use crate::upstream::transport::websocket::{
26 WebSocketTransportConfig, connect as connect_websocket_transport,
27};
28use crate::upstream::{
29 CapScope, McpRequestOutcome, McpRoundTrip, ResponseCaps, TransportKind, UpstreamError,
30 UpstreamSnapshot,
31};
32
33use super::lifecycle_compat::{LifecycleAttempt, compatibility_retry, log_fallback};
34
35#[path = "live_support.rs"]
36mod live_support;
37
38use live_support::{
39 bearer_token_from_env, capability_is_absent, drain_stderr, ensure_rustls_crypto_provider,
40 prompt_descriptor, resource_descriptor, stdio_env, tool_descriptor, websocket_authorization,
41};
42
43#[derive(Clone, Copy, Debug, Default)]
44struct UpstreamClientHandler;
45
46#[derive(Clone, Debug)]
47struct RequestScopedClientHandler {
48 info: ClientInfo,
49}
50
51impl ClientHandler for RequestScopedClientHandler {
52 fn get_info(&self) -> ClientInfo {
53 self.info.clone()
54 }
55}
56
57impl ClientHandler for UpstreamClientHandler {
58 fn get_info(&self) -> ClientInfo {
59 ClientInfo::new(
60 ClientCapabilities::builder().enable_tasks().build(),
61 Implementation::new("soma-upstream-client", env!("CARGO_PKG_VERSION")),
62 )
63 .with_protocol_version(ProtocolVersion::LATEST)
64 }
65}
66
67enum ConnectAttemptError {
68 Fatal(UpstreamError),
69 Lifecycle(Box<ClientInitializeError>),
70}
71
72impl ConnectAttemptError {
73 fn into_upstream(self, config: &UpstreamConfig, prefix: &str) -> UpstreamError {
74 match self {
75 Self::Fatal(error) => error,
76 Self::Lifecycle(error) => UpstreamError::connect(config, format!("{prefix}: {error}")),
77 }
78 }
79}
80
81#[derive(Clone)]
82pub(super) struct LiveConnectContext<'a> {
83 response_caps: &'a ResponseCaps,
84 #[cfg(feature = "oauth")]
85 oauth: Option<LiveOauthContext<'a>>,
86}
87
88#[cfg(feature = "oauth")]
89#[derive(Clone)]
90pub(super) struct LiveOauthContext<'a> {
91 pub subject: &'a str,
92 pub provider: Arc<dyn UpstreamOAuthProvider>,
93}
94
95impl<'a> LiveConnectContext<'a> {
96 pub(super) fn shared(response_caps: &'a ResponseCaps) -> Self {
97 Self {
98 response_caps,
99 #[cfg(feature = "oauth")]
100 oauth: None,
101 }
102 }
103
104 #[cfg(feature = "oauth")]
105 pub(super) fn oauth(
106 response_caps: &'a ResponseCaps,
107 subject: &'a str,
108 provider: Arc<dyn UpstreamOAuthProvider>,
109 ) -> Self {
110 Self {
111 response_caps,
112 oauth: Some(LiveOauthContext { subject, provider }),
113 }
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub(super) enum LiveKind {
119 Http(TransportKind),
120 Stdio,
121 WebSocket,
122}
123
124type LiveConnectionFor<H> = (
125 RunningService<RoleClient, H>,
126 rmcp::service::Peer<RoleClient>,
127 LiveKind,
128);
129
130pub(super) struct LiveUpstream {
131 _service: RunningService<RoleClient, UpstreamClientHandler>,
132 peer: rmcp::service::Peer<RoleClient>,
133}
134
135impl LiveUpstream {
136 pub(super) fn peer(&self) -> rmcp::service::Peer<RoleClient> {
137 self.peer.clone()
138 }
139}
140
141pub(super) async fn connect_live(
142 config: &UpstreamConfig,
143 guard: &SpawnGuard,
144 context: LiveConnectContext<'_>,
145) -> Result<(LiveUpstream, UpstreamSnapshot), UpstreamError> {
146 let (service, peer, kind) =
147 connect_with_handler(config, guard, context, UpstreamClientHandler).await?;
148
149 let tools = peer
150 .list_all_tools()
151 .await
152 .map_err(|error| UpstreamError::connect(config, error))?;
153 let resources = if config.proxy_resources {
154 list_resources_or_empty(config, &peer).await?
155 } else {
156 Vec::new()
157 };
158 let prompts = if config.proxy_prompts {
159 list_prompts_or_empty(config, &peer).await?
160 } else {
161 Vec::new()
162 };
163
164 let mut snapshot = UpstreamSnapshot::empty(
165 config.name.clone(),
166 match kind {
167 LiveKind::Http(transport) => transport,
168 LiveKind::Stdio => TransportKind::Stdio,
169 LiveKind::WebSocket => TransportKind::WebSocket,
170 },
171 );
172 snapshot.tools = tools.into_iter().map(tool_descriptor).collect();
173 snapshot.resources = resources.into_iter().map(resource_descriptor).collect();
174 snapshot.prompts = prompts.into_iter().map(prompt_descriptor).collect();
175 Ok((
176 LiveUpstream {
177 _service: service,
178 peer,
179 },
180 snapshot,
181 ))
182}
183
184async fn connect_with_handler<H>(
185 config: &UpstreamConfig,
186 guard: &SpawnGuard,
187 context: LiveConnectContext<'_>,
188 handler: H,
189) -> Result<LiveConnectionFor<H>, UpstreamError>
190where
191 H: ClientHandler + Clone,
192{
193 if let Some(url) = config.url.as_deref() {
194 return match decide_http_transport(url) {
195 HttpTransportDecision::WebSocket => {
196 connect_websocket_with_handler(config, url, handler).await
197 }
198 HttpTransportDecision::Json | HttpTransportDecision::Sse => {
199 connect_http_with_handler(config, url, context, handler).await
200 }
201 };
202 }
203 if let Some(command) = config.command.as_deref() {
204 return connect_stdio_with_handler(config, command, guard, handler).await;
205 }
206 Err(UpstreamError::Unsupported {
207 upstream: config.name.clone(),
208 capability: "transport",
209 })
210}
211
212pub(super) async fn call_live_tool(
213 upstream: &str,
214 peer: rmcp::service::Peer<RoleClient>,
215 tool: String,
216 params: Value,
217) -> Result<Value, UpstreamError> {
218 let Value::Object(args) = params else {
219 return Err(UpstreamError::ParamsMustBeObject);
220 };
221 let result = peer
222 .call_tool(CallToolRequestParams::new(tool).with_arguments(args))
223 .await
224 .map_err(|error| UpstreamError::LiveCall {
225 upstream: upstream.to_owned(),
226 operation: "tools/call",
227 message: error.to_string(),
228 })?;
229 if let Some(value) = result.structured_content.clone() {
230 return Ok(value);
231 }
232 Ok(serde_json::to_value(result).unwrap_or(Value::Null))
233}
234
235pub(super) async fn call_live_tool_once(
236 upstream: &str,
237 peer: rmcp::service::Peer<RoleClient>,
238 tool: String,
239 params: Value,
240 round_trip: McpRoundTrip,
241) -> Result<McpRequestOutcome, UpstreamError> {
242 let Value::Object(args) = params else {
243 return Err(UpstreamError::ParamsMustBeObject);
244 };
245 let mut request = CallToolRequestParams::new(tool).with_arguments(args);
246 request.input_responses = round_trip.input_responses;
247 request.request_state = round_trip.request_state;
248 let response = peer
249 .call_tool_once(request)
250 .await
251 .map_err(|error| UpstreamError::LiveCall {
252 upstream: upstream.to_owned(),
253 operation: "tools/call",
254 message: error.to_string(),
255 })?;
256 match response {
257 CallToolResponse::Complete(result) => {
258 serialize_live_outcome(upstream, "tools/call", result, McpRequestOutcome::Complete)
259 }
260 CallToolResponse::InputRequired(result) => serialize_live_outcome(
261 upstream,
262 "tools/call",
263 result,
264 McpRequestOutcome::InputRequired,
265 ),
266 CallToolResponse::Task(result) => {
267 serialize_live_outcome(upstream, "tools/call", result, McpRequestOutcome::Task)
268 }
269 _ => Err(unsupported_live_outcome(upstream, "tools/call")),
270 }
271}
272
273pub(super) async fn call_live_tool_once_scoped(
274 config: &UpstreamConfig,
275 context: LiveConnectContext<'_>,
276 tool: String,
277 params: Value,
278 mut round_trip: McpRoundTrip,
279) -> Result<McpRequestOutcome, UpstreamError> {
280 let meta = round_trip
281 .request_meta
282 .take()
283 .ok_or_else(|| UpstreamError::LiveCall {
284 upstream: config.name.clone(),
285 operation: "tools/call",
286 message: "request-scoped relay requires downstream MCP metadata".to_owned(),
287 })?;
288 let handler = request_scoped_handler(&config.name, "tools/call", meta)?;
289 let (mut service, peer, _) =
290 connect_with_handler(config, &SpawnGuard::default(), context, handler).await?;
291 let outcome = call_live_tool_once(
292 &config.name,
293 peer,
294 tool,
295 params,
296 McpRoundTrip {
297 input_responses: round_trip.input_responses,
298 request_state: round_trip.request_state,
299 request_meta: None,
300 },
301 )
302 .await;
303 if let Err(error) = service.close().await {
304 tracing::debug!(
305 upstream = %config.name,
306 error = %error,
307 "request-scoped upstream relay did not close cleanly"
308 );
309 }
310 outcome
311}
312
313pub(super) async fn read_live_resource(
314 upstream: &str,
315 peer: rmcp::service::Peer<RoleClient>,
316 uri: String,
317) -> Result<Value, UpstreamError> {
318 let result = peer
319 .read_resource(ReadResourceRequestParams::new(uri))
320 .await
321 .map_err(|error| UpstreamError::LiveCall {
322 upstream: upstream.to_owned(),
323 operation: "resources/read",
324 message: error.to_string(),
325 })?;
326 serde_json::to_value(result).map_err(|error| UpstreamError::LiveCall {
327 upstream: upstream.to_owned(),
328 operation: "resources/read",
329 message: error.to_string(),
330 })
331}
332
333pub(super) async fn read_live_resource_once(
334 upstream: &str,
335 peer: rmcp::service::Peer<RoleClient>,
336 uri: String,
337 round_trip: McpRoundTrip,
338) -> Result<McpRequestOutcome, UpstreamError> {
339 let mut request = ReadResourceRequestParams::new(uri);
340 request.input_responses = round_trip.input_responses;
341 request.request_state = round_trip.request_state;
342 let response =
343 peer.read_resource_once(request)
344 .await
345 .map_err(|error| UpstreamError::LiveCall {
346 upstream: upstream.to_owned(),
347 operation: "resources/read",
348 message: error.to_string(),
349 })?;
350 match response {
351 ReadResourceResponse::Complete(result) => serialize_live_outcome(
352 upstream,
353 "resources/read",
354 result,
355 McpRequestOutcome::Complete,
356 ),
357 ReadResourceResponse::InputRequired(result) => serialize_live_outcome(
358 upstream,
359 "resources/read",
360 result,
361 McpRequestOutcome::InputRequired,
362 ),
363 _ => Err(unsupported_live_outcome(upstream, "resources/read")),
364 }
365}
366
367pub(super) async fn get_live_prompt(
368 upstream: &str,
369 peer: rmcp::service::Peer<RoleClient>,
370 name: String,
371 arguments: Option<Map<String, Value>>,
372) -> Result<Value, UpstreamError> {
373 let mut params = GetPromptRequestParams::new(name);
374 params.arguments = arguments;
375 let result = peer
376 .get_prompt(params)
377 .await
378 .map_err(|error| UpstreamError::LiveCall {
379 upstream: upstream.to_owned(),
380 operation: "prompts/get",
381 message: error.to_string(),
382 })?;
383 serde_json::to_value(result).map_err(|error| UpstreamError::LiveCall {
384 upstream: upstream.to_owned(),
385 operation: "prompts/get",
386 message: error.to_string(),
387 })
388}
389
390pub(super) async fn get_live_prompt_once(
391 upstream: &str,
392 peer: rmcp::service::Peer<RoleClient>,
393 name: String,
394 arguments: Option<Map<String, Value>>,
395 round_trip: McpRoundTrip,
396) -> Result<McpRequestOutcome, UpstreamError> {
397 let mut request = GetPromptRequestParams::new(name);
398 request.arguments = arguments;
399 request.input_responses = round_trip.input_responses;
400 request.request_state = round_trip.request_state;
401 let response =
402 peer.get_prompt_once(request)
403 .await
404 .map_err(|error| UpstreamError::LiveCall {
405 upstream: upstream.to_owned(),
406 operation: "prompts/get",
407 message: error.to_string(),
408 })?;
409 match response {
410 GetPromptResponse::Complete(result) => {
411 serialize_live_outcome(upstream, "prompts/get", result, McpRequestOutcome::Complete)
412 }
413 GetPromptResponse::InputRequired(result) => serialize_live_outcome(
414 upstream,
415 "prompts/get",
416 result,
417 McpRequestOutcome::InputRequired,
418 ),
419 _ => Err(unsupported_live_outcome(upstream, "prompts/get")),
420 }
421}
422
423fn request_scoped_handler(
424 upstream: &str,
425 operation: &'static str,
426 value: Value,
427) -> Result<RequestScopedClientHandler, UpstreamError> {
428 let meta: RequestMetaObject =
429 serde_json::from_value(value).map_err(|error| UpstreamError::LiveCall {
430 upstream: upstream.to_owned(),
431 operation,
432 message: format!("invalid downstream MCP request metadata: {error}"),
433 })?;
434 let protocol_version = meta.protocol_version().unwrap_or(ProtocolVersion::LATEST);
435 let implementation = meta.client_info().unwrap_or_else(|| {
436 Implementation::new("soma-request-scoped-relay", env!("CARGO_PKG_VERSION"))
437 });
438 let capabilities = meta.client_capabilities().unwrap_or_default();
439 Ok(RequestScopedClientHandler {
440 info: ClientInfo::new(capabilities, implementation).with_protocol_version(protocol_version),
441 })
442}
443
444fn unsupported_live_outcome(upstream: &str, operation: &'static str) -> UpstreamError {
445 UpstreamError::LiveCall {
446 upstream: upstream.to_owned(),
447 operation,
448 message: "upstream returned an unsupported MCP response variant".to_owned(),
449 }
450}
451
452fn serialize_live_outcome<T: serde::Serialize>(
453 upstream: &str,
454 operation: &'static str,
455 result: T,
456 wrap: impl FnOnce(Value) -> McpRequestOutcome,
457) -> Result<McpRequestOutcome, UpstreamError> {
458 serde_json::to_value(result)
459 .map(wrap)
460 .map_err(|error| UpstreamError::LiveCall {
461 upstream: upstream.to_owned(),
462 operation,
463 message: error.to_string(),
464 })
465}
466
467async fn connect_http_with_handler<H>(
468 config: &UpstreamConfig,
469 url: &str,
470 context: LiveConnectContext<'_>,
471 handler: H,
472) -> Result<LiveConnectionFor<H>, UpstreamError>
473where
474 H: ClientHandler + Clone,
475{
476 match connect_http_once(
477 config,
478 url,
479 context.clone(),
480 LifecycleAttempt::Modern,
481 handler.clone(),
482 )
483 .await
484 {
485 Ok(connection) => Ok(connection),
486 Err(ConnectAttemptError::Fatal(error)) => Err(error),
487 Err(ConnectAttemptError::Lifecycle(error)) => {
488 let Some(attempt) = compatibility_retry(&error) else {
489 return Err(UpstreamError::connect(
490 config,
491 format!("http connect failed: {error}"),
492 ));
493 };
494 log_fallback(&config.name, "http", attempt, &error);
495 connect_http_once(config, url, context, attempt, handler)
496 .await
497 .map_err(|error| error.into_upstream(config, "http connect failed"))
498 }
499 }
500}
501
502async fn connect_http_once<H>(
503 config: &UpstreamConfig,
504 url: &str,
505 context: LiveConnectContext<'_>,
506 lifecycle: LifecycleAttempt,
507 handler: H,
508) -> Result<LiveConnectionFor<H>, ConnectAttemptError>
509where
510 H: ClientHandler + Clone,
511{
512 ensure_rustls_crypto_provider();
513 let transport_kind = match decide_http_transport(url) {
514 HttpTransportDecision::Json => TransportKind::HttpJson,
515 HttpTransportDecision::Sse => TransportKind::HttpSse,
516 HttpTransportDecision::WebSocket => TransportKind::WebSocket,
517 };
518 let mut transport_config = StreamableHttpClientTransportConfig::with_uri(url.to_owned());
519 #[cfg(feature = "oauth")]
520 if config.oauth.is_some() {
521 let oauth = context.oauth.ok_or_else(|| {
522 ConnectAttemptError::Fatal(UpstreamError::LiveConnect {
523 upstream: config.name.clone(),
524 message: "oauth upstream requires subject-scoped connection context".to_owned(),
525 })
526 })?;
527 let client = BodyCappedHttpClient::default_with_caps(
528 context.response_caps.limit_for(CapScope::HttpJson),
529 context.response_caps.limit_for(CapScope::HttpSseEvent),
530 );
531 let auth_client = oauth
532 .provider
533 .authenticated_http_client(config, oauth.subject, client)
534 .await
535 .map_err(|error| {
536 ConnectAttemptError::Fatal(UpstreamError::LiveConnect {
537 upstream: config.name.clone(),
538 message: error.to_string(),
539 })
540 })?;
541 let transport = StreamableHttpClientTransport::with_client(auth_client, transport_config);
542 let service = handler
543 .clone()
544 .serve_with_lifecycle(transport, lifecycle.mode())
545 .await
546 .map_err(|error| ConnectAttemptError::Lifecycle(Box::new(error)))?;
547 let peer = service.peer().clone();
548 return Ok((service, peer, LiveKind::Http(transport_kind)));
549 }
550 #[cfg(not(feature = "oauth"))]
551 if config.oauth.is_some() {
552 return Err(ConnectAttemptError::Fatal(UpstreamError::LiveConnect {
553 upstream: config.name.clone(),
554 message: "oauth upstream support is not compiled into soma-mcp-client".to_owned(),
555 }));
556 }
557 if let Some(token) = bearer_token_from_env(config) {
558 transport_config = transport_config.auth_header(token);
559 }
560 let client = BodyCappedHttpClient::default_with_caps(
561 context.response_caps.limit_for(CapScope::HttpJson),
562 context.response_caps.limit_for(CapScope::HttpSseEvent),
563 );
564 let transport = StreamableHttpClientTransport::with_client(client, transport_config);
565 let service = handler
566 .serve_with_lifecycle(transport, lifecycle.mode())
567 .await
568 .map_err(|error| ConnectAttemptError::Lifecycle(Box::new(error)))?;
569 let peer = service.peer().clone();
570 Ok((service, peer, LiveKind::Http(transport_kind)))
571}
572
573async fn connect_websocket_with_handler<H>(
574 config: &UpstreamConfig,
575 url: &str,
576 handler: H,
577) -> Result<LiveConnectionFor<H>, UpstreamError>
578where
579 H: ClientHandler + Clone,
580{
581 match connect_websocket_once(config, url, LifecycleAttempt::Modern, handler.clone()).await {
582 Ok(connection) => Ok(connection),
583 Err(error) => {
584 let Some(attempt) = compatibility_retry(&error) else {
585 return Err(UpstreamError::connect(
586 config,
587 format!("websocket connect failed: {error}"),
588 ));
589 };
590 log_fallback(&config.name, "websocket", attempt, &error);
591 connect_websocket_once(config, url, attempt, handler)
592 .await
593 .map_err(|error| {
594 UpstreamError::connect(config, format!("websocket connect failed: {error}"))
595 })
596 }
597 }
598}
599
600async fn connect_websocket_once<H>(
601 config: &UpstreamConfig,
602 url: &str,
603 lifecycle: LifecycleAttempt,
604 handler: H,
605) -> Result<LiveConnectionFor<H>, ClientInitializeError>
606where
607 H: ClientHandler,
608{
609 ensure_rustls_crypto_provider();
610 let transport_config = WebSocketTransportConfig::new(url.to_owned())
611 .with_authorization(websocket_authorization(config));
612 let service = handler
613 .serve_with_lifecycle(
614 connect_websocket_transport(transport_config),
615 lifecycle.mode(),
616 )
617 .await?;
618 let peer = service.peer().clone();
619 Ok((service, peer, LiveKind::WebSocket))
620}
621
622async fn connect_stdio_with_handler<H>(
623 config: &UpstreamConfig,
624 command: &str,
625 guard: &SpawnGuard,
626 handler: H,
627) -> Result<LiveConnectionFor<H>, UpstreamError>
628where
629 H: ClientHandler + Clone,
630{
631 match connect_stdio_once(
632 config,
633 command,
634 guard,
635 LifecycleAttempt::Modern,
636 handler.clone(),
637 )
638 .await
639 {
640 Ok(connection) => Ok(connection),
641 Err(ConnectAttemptError::Fatal(error)) => Err(error),
642 Err(ConnectAttemptError::Lifecycle(error)) => {
643 let Some(attempt) = compatibility_retry(&error) else {
644 return Err(UpstreamError::connect(
645 config,
646 format!("stdio MCP handshake failed: {error}"),
647 ));
648 };
649 log_fallback(&config.name, "stdio", attempt, &error);
650 connect_stdio_once(config, command, guard, attempt, handler)
651 .await
652 .map_err(|error| error.into_upstream(config, "stdio MCP handshake failed"))
653 }
654 }
655}
656
657async fn connect_stdio_once<H>(
658 config: &UpstreamConfig,
659 command_name: &str,
660 guard: &SpawnGuard,
661 lifecycle: LifecycleAttempt,
662 handler: H,
663) -> Result<LiveConnectionFor<H>, ConnectAttemptError>
664where
665 H: ClientHandler,
666{
667 let spec = crate::upstream::pool::connect_stdio::plan_stdio_connection(config, guard).map_err(
668 |error| {
669 ConnectAttemptError::Fatal(UpstreamError::LiveConnect {
670 upstream: config.name.clone(),
671 message: error.to_string(),
672 })
673 },
674 )?;
675 let mut cmd = Command::new(command_name);
676 cmd.args(&spec.args)
677 .env_clear()
678 .envs(stdio_env())
679 .envs(spec.env.iter())
680 .stderr(Stdio::piped());
681 if let Some(env_name) = config.bearer_token_env.as_deref()
682 && let Ok(token) = std::env::var(env_name)
683 {
684 cmd.env(env_name, token);
685 }
686
687 #[cfg(unix)]
688 let command = {
689 use process_wrap::tokio::{CommandWrap, ProcessGroup};
690 let mut wrapped = CommandWrap::from(cmd);
691 wrapped.wrap(ProcessGroup::leader());
692 wrapped
693 };
694 #[cfg(not(unix))]
695 let command = cmd;
696
697 let (transport, stderr) = TokioChildProcess::builder(command)
698 .spawn()
699 .map_err(|error| {
700 ConnectAttemptError::Fatal(UpstreamError::LiveConnect {
701 upstream: config.name.clone(),
702 message: format!("stdio spawn failed: {error}"),
703 })
704 })?;
705 drain_stderr(config.name.clone(), stderr);
706 let service = handler
707 .serve_with_lifecycle(transport, lifecycle.mode())
708 .await
709 .map_err(|error| ConnectAttemptError::Lifecycle(Box::new(error)))?;
710 let peer = service.peer().clone();
711 Ok((service, peer, LiveKind::Stdio))
712}
713
714async fn list_resources_or_empty(
715 config: &UpstreamConfig,
716 peer: &rmcp::service::Peer<RoleClient>,
717) -> Result<Vec<rmcp::model::Resource>, UpstreamError> {
718 match peer.list_all_resources().await {
719 Ok(resources) => Ok(resources),
720 Err(error) if capability_is_absent(&error.to_string()) => Ok(Vec::new()),
721 Err(error) => Err(UpstreamError::LiveConnect {
722 upstream: config.name.clone(),
723 message: format!("resources/list failed: {error}"),
724 }),
725 }
726}
727
728async fn list_prompts_or_empty(
729 config: &UpstreamConfig,
730 peer: &rmcp::service::Peer<RoleClient>,
731) -> Result<Vec<rmcp::model::Prompt>, UpstreamError> {
732 match peer.list_all_prompts().await {
733 Ok(prompts) => Ok(prompts),
734 Err(error) if capability_is_absent(&error.to_string()) => Ok(Vec::new()),
735 Err(error) => Err(UpstreamError::LiveConnect {
736 upstream: config.name.clone(),
737 message: format!("prompts/list failed: {error}"),
738 }),
739 }
740}
741
742#[cfg(test)]
743#[path = "live_tests.rs"]
744mod tests;