codex-mcp/src/resource_client.rs
126 lines
use std::sync::Arc;use std::sync::Weak;use anyhow::Context;use anyhow::Result;use arc_swap::ArcSwap;use codex_protocol::mcp::Resource;use codex_protocol::mcp::ResourceContent;use rmcp::model::PaginatedRequestParams;use rmcp::model::ReadResourceRequestParams;use crate::McpConnectionManager;/// One page of resources returned by an MCP server.#[derive(Clone, Debug, PartialEq)]pub struct McpResourcePage { /// Resources advertised on this page. pub resources: Vec<Resource>, /// Opaque cursor to supply when requesting the next page. pub next_cursor: Option<String>,}/// Contents returned after reading one MCP resource.#[derive(Clone, Debug, PartialEq)]pub struct McpResourceReadResult { /// Text or blob content returned for the requested resource. pub contents: Vec<ResourceContent>,}/// Session-scoped access to MCP resources through the currently installed manager.////// The client retains the manager's shared publication handle rather than a manager/// snapshot, so calls automatically use replacements installed during startup and refresh.#[derive(Clone)]pub struct McpResourceClient { manager: Arc<ArcSwap<McpConnectionManager>>,}/// Opaque identity for the manager currently used by an MCP resource client.#[derive(Clone)]pub struct McpResourceClientCacheKey(Weak<McpConnectionManager>);impl PartialEq for McpResourceClientCacheKey { fn eq(&self, other: &Self) -> bool { self.0.ptr_eq(&other.0) }}impl Eq for McpResourceClientCacheKey {}impl std::fmt::Debug for McpResourceClient { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter .debug_struct("McpResourceClient") .finish_non_exhaustive() }}impl McpResourceClient { /// Creates a resource client backed by the session's replaceable MCP manager. pub fn new(manager: Arc<ArcSwap<McpConnectionManager>>) -> Self { Self { manager } } /// Returns an identity that changes whenever the published manager changes. pub fn cache_key(&self) -> McpResourceClientCacheKey { McpResourceClientCacheKey(Arc::downgrade(&self.manager.load_full())) } /// Returns whether the current manager contains the named server. /// /// This does not wait for server startup or imply that startup succeeded. pub async fn has_server(&self, server: &str) -> bool { self.manager.load_full().contains_server(server) } /// Lists one resource page from the named server. pub async fn list_resources( &self, server: &str, cursor: Option<String>, ) -> Result<McpResourcePage> { let params = cursor.map(|cursor| PaginatedRequestParams::default().with_cursor(Some(cursor))); let result = self .manager .load_full() .list_resources(server, params) .await?; let resources = result .resources .into_iter() .map(resource_from_rmcp) .collect::<Result<Vec<_>>>()?; Ok(McpResourcePage { resources, next_cursor: result.next_cursor, }) } /// Reads one resource from the named server. pub async fn read_resource(&self, server: &str, uri: &str) -> Result<McpResourceReadResult> { let result = self .manager .load_full() .read_resource(server, ReadResourceRequestParams::new(uri.to_string())) .await?; let contents = result .contents .into_iter() .map(resource_content_from_rmcp) .collect::<Result<Vec<_>>>()?; Ok(McpResourceReadResult { contents }) }}fn resource_from_rmcp(resource: rmcp::model::Resource) -> Result<Resource> { let value = serde_json::to_value(resource).context("failed to serialize MCP resource")?; Resource::from_mcp_value(value).context("failed to convert MCP resource")}fn resource_content_from_rmcp(content: rmcp::model::ResourceContents) -> Result<ResourceContent> { let value = serde_json::to_value(content).context("failed to serialize MCP resource content")?; serde_json::from_value(value).context("failed to convert MCP resource content")}