Skip to main content

codex_mcp/
resource_client.rs

1use std::sync::Arc;
2use std::sync::Weak;
3
4use anyhow::Context;
5use anyhow::Result;
6use codex_protocol::mcp::Resource;
7use codex_protocol::mcp::ResourceContent;
8use rmcp::model::PaginatedRequestParams;
9use rmcp::model::ReadResourceRequestParams;
10
11use crate::McpRuntime;
12use crate::binding_clients::McpBindingClientIdentity;
13use crate::binding_clients::McpBindingClients;
14use crate::connection_manager::McpConnectionManager;
15
16/// One page of resources returned by an MCP server.
17#[derive(Clone, Debug, PartialEq)]
18pub struct McpResourcePage {
19    /// Resources advertised on this page.
20    pub resources: Vec<Resource>,
21    /// Opaque cursor to supply when requesting the next page.
22    pub next_cursor: Option<String>,
23}
24
25/// Contents returned after reading one MCP resource.
26#[derive(Clone, Debug, PartialEq)]
27pub struct McpResourceReadResult {
28    /// Text or blob content returned for the requested resource.
29    pub contents: Vec<ResourceContent>,
30}
31
32/// Access to MCP resources through either the latest runtime or one exact step.
33#[derive(Clone)]
34pub struct McpResourceClient {
35    source: McpResourceSource,
36}
37
38/// Opaque identity for the connection set currently used by an MCP resource client.
39#[derive(Clone)]
40pub struct McpResourceClientCacheKey(McpResourceClientCacheKeyInner);
41
42#[derive(Clone)]
43enum McpResourceClientCacheKeyInner {
44    Latest(Weak<McpConnectionManager>),
45    Exact(McpBindingClientIdentity),
46}
47
48#[derive(Clone)]
49enum McpResourceSource {
50    Latest(Arc<McpRuntime>),
51    Exact(Arc<McpBindingClients>),
52}
53
54impl PartialEq for McpResourceClientCacheKey {
55    fn eq(&self, other: &Self) -> bool {
56        match (&self.0, &other.0) {
57            (
58                McpResourceClientCacheKeyInner::Latest(left),
59                McpResourceClientCacheKeyInner::Latest(right),
60            ) => left.ptr_eq(right),
61            (
62                McpResourceClientCacheKeyInner::Exact(left),
63                McpResourceClientCacheKeyInner::Exact(right),
64            ) => left == right,
65            (
66                McpResourceClientCacheKeyInner::Latest(_),
67                McpResourceClientCacheKeyInner::Exact(_),
68            )
69            | (
70                McpResourceClientCacheKeyInner::Exact(_),
71                McpResourceClientCacheKeyInner::Latest(_),
72            ) => false,
73        }
74    }
75}
76
77impl Eq for McpResourceClientCacheKey {}
78
79impl std::fmt::Debug for McpResourceClient {
80    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        formatter
82            .debug_struct("McpResourceClient")
83            .finish_non_exhaustive()
84    }
85}
86
87impl McpResourceClient {
88    /// Creates a resource client that follows the thread's latest published runtime.
89    pub fn new(runtime: Arc<McpRuntime>) -> Self {
90        Self {
91            source: McpResourceSource::Latest(runtime),
92        }
93    }
94
95    pub(crate) fn for_binding(clients: Arc<McpBindingClients>) -> Self {
96        Self {
97            source: McpResourceSource::Exact(clients),
98        }
99    }
100
101    /// Returns the identity of the connection set used by this client.
102    pub fn cache_key(&self) -> McpResourceClientCacheKey {
103        let key = match &self.source {
104            McpResourceSource::Latest(runtime) => {
105                McpResourceClientCacheKeyInner::Latest(Arc::downgrade(&runtime.snapshot()))
106            }
107            McpResourceSource::Exact(clients) => {
108                McpResourceClientCacheKeyInner::Exact(clients.identity())
109            }
110        };
111        McpResourceClientCacheKey(key)
112    }
113
114    /// Returns whether this client can address the named server.
115    ///
116    /// This does not wait for server startup.
117    pub async fn has_server(&self, server: &str) -> bool {
118        match &self.source {
119            McpResourceSource::Latest(runtime) => runtime.snapshot().contains_server(server),
120            McpResourceSource::Exact(clients) => clients.contains_server(server),
121        }
122    }
123
124    /// Lists one resource page from the named server.
125    pub async fn list_resources(
126        &self,
127        server: &str,
128        cursor: Option<String>,
129    ) -> Result<McpResourcePage> {
130        let params =
131            cursor.map(|cursor| PaginatedRequestParams::default().with_cursor(Some(cursor)));
132        let result = match &self.source {
133            McpResourceSource::Latest(runtime) => {
134                runtime.snapshot().list_resources(server, params).await
135            }
136            McpResourceSource::Exact(clients) => clients.list_resources(server, params).await,
137        }?;
138        let resources = result
139            .resources
140            .into_iter()
141            .map(resource_from_rmcp)
142            .collect::<Result<Vec<_>>>()?;
143        Ok(McpResourcePage {
144            resources,
145            next_cursor: result.next_cursor,
146        })
147    }
148
149    /// Reads one resource from the named server.
150    pub async fn read_resource(&self, server: &str, uri: &str) -> Result<McpResourceReadResult> {
151        let params = ReadResourceRequestParams::new(uri.to_string());
152        let result = match &self.source {
153            McpResourceSource::Latest(runtime) => {
154                runtime.snapshot().read_resource(server, params).await
155            }
156            McpResourceSource::Exact(clients) => clients.read_resource(server, params).await,
157        }?;
158        let contents = result
159            .contents
160            .into_iter()
161            .map(resource_content_from_rmcp)
162            .collect::<Result<Vec<_>>>()?;
163        Ok(McpResourceReadResult { contents })
164    }
165}
166
167fn resource_from_rmcp(resource: rmcp::model::Resource) -> Result<Resource> {
168    let value = serde_json::to_value(resource).context("failed to serialize MCP resource")?;
169    Resource::from_mcp_value(value).context("failed to convert MCP resource")
170}
171
172fn resource_content_from_rmcp(content: rmcp::model::ResourceContents) -> Result<ResourceContent> {
173    let value =
174        serde_json::to_value(content).context("failed to serialize MCP resource content")?;
175    serde_json::from_value(value).context("failed to convert MCP resource content")
176}