mirror of
https://github.com/openai/codex.git
synced 2026-04-28 02:11:08 +03:00
feat: replace custom mcp-types crate with equivalents from rmcp (#10349)
We started working with MCP in Codex before
https://crates.io/crates/rmcp was mature, so we had our own crate for
MCP types that was generated from the MCP schema:
8b95d3e082/codex-rs/mcp-types/README.md
Now that `rmcp` is more mature, it makes more sense to use their MCP
types in Rust, as they handle details (like the `_meta` field) that our
custom version ignored. Though one advantage that our custom types had
is that our generated types implemented `JsonSchema` and `ts_rs::TS`,
whereas the types in `rmcp` do not. As such, part of the work of this PR
is leveraging the adapters between `rmcp` types and the serializable
types that are API for us (app server and MCP) introduced in #10356.
Note this PR results in a number of changes to
`codex-rs/app-server-protocol/schema`, which merit special attention
during review. We must ensure that these changes are still
backwards-compatible, which is possible because we have:
```diff
- export type CallToolResult = { content: Array<ContentBlock>, isError?: boolean, structuredContent?: JsonValue, };
+ export type CallToolResult = { content: Array<JsonValue>, structuredContent?: JsonValue, isError?: boolean, _meta?: JsonValue, };
```
so `ContentBlock` has been replaced with the more general `JsonValue`.
Note that `ContentBlock` was defined as:
```typescript
export type ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource;
```
so the deletion of those individual variants should not be a cause of
great concern.
Similarly, we have the following change in
`codex-rs/app-server-protocol/schema/typescript/Tool.ts`:
```
- export type Tool = { annotations?: ToolAnnotations, description?: string, inputSchema: ToolInputSchema, name: string, outputSchema?: ToolOutputSchema, title?: string, };
+ export type Tool = { name: string, title?: string, description?: string, inputSchema: JsonValue, outputSchema?: JsonValue, annotations?: JsonValue, icons?: Array<JsonValue>, _meta?: JsonValue, };
```
so:
- `annotations?: ToolAnnotations` ➡️ `JsonValue`
- `inputSchema: ToolInputSchema` ➡️ `JsonValue`
- `outputSchema?: ToolOutputSchema` ➡️ `JsonValue`
and two new fields: `icons?: Array<JsonValue>, _meta?: JsonValue`
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/10349).
* #10357
* __->__ #10349
* #10356
This commit is contained in:
@@ -18,7 +18,6 @@ codex-protocol = { workspace = true }
|
||||
codex-utils-home-dir = { workspace = true }
|
||||
futures = { workspace = true, default-features = false, features = ["std"] }
|
||||
keyring = { workspace = true, features = ["crypto-rust"] }
|
||||
mcp-types = { path = "../mcp-types" }
|
||||
oauth2 = "5"
|
||||
reqwest = { version = "0.12", default-features = false, features = [
|
||||
"json",
|
||||
|
||||
@@ -9,7 +9,6 @@ use rmcp::model::CreateElicitationResult;
|
||||
use rmcp::model::LoggingLevel;
|
||||
use rmcp::model::LoggingMessageNotificationParam;
|
||||
use rmcp::model::ProgressNotificationParam;
|
||||
use rmcp::model::RequestId;
|
||||
use rmcp::model::ResourceUpdatedNotificationParam;
|
||||
use rmcp::service::NotificationContext;
|
||||
use rmcp::service::RequestContext;
|
||||
@@ -41,11 +40,7 @@ impl ClientHandler for LoggingClientHandler {
|
||||
request: CreateElicitationRequestParam,
|
||||
context: RequestContext<RoleClient>,
|
||||
) -> Result<CreateElicitationResult, rmcp::ErrorData> {
|
||||
let id = match context.id {
|
||||
RequestId::String(id) => mcp_types::RequestId::String(id.to_string()),
|
||||
RequestId::Number(id) => mcp_types::RequestId::Integer(id),
|
||||
};
|
||||
(self.send_elicitation)(id, request)
|
||||
(self.send_elicitation)(context.id, request)
|
||||
.await
|
||||
.map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None))
|
||||
}
|
||||
|
||||
@@ -10,22 +10,9 @@ use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use futures::FutureExt;
|
||||
use futures::future::BoxFuture;
|
||||
use mcp_types::CallToolRequestParams;
|
||||
use mcp_types::CallToolResult;
|
||||
use mcp_types::InitializeRequestParams;
|
||||
use mcp_types::InitializeResult;
|
||||
use mcp_types::ListResourceTemplatesRequestParams;
|
||||
use mcp_types::ListResourceTemplatesResult;
|
||||
use mcp_types::ListResourcesRequestParams;
|
||||
use mcp_types::ListResourcesResult;
|
||||
use mcp_types::ListToolsRequestParams;
|
||||
use mcp_types::ListToolsResult;
|
||||
use mcp_types::ReadResourceRequestParams;
|
||||
use mcp_types::ReadResourceResult;
|
||||
use mcp_types::RequestId;
|
||||
use mcp_types::Tool;
|
||||
use reqwest::header::HeaderMap;
|
||||
use rmcp::model::CallToolRequestParam;
|
||||
use rmcp::model::CallToolResult;
|
||||
use rmcp::model::ClientNotification;
|
||||
use rmcp::model::ClientRequest;
|
||||
use rmcp::model::CreateElicitationRequestParam;
|
||||
@@ -34,9 +21,16 @@ use rmcp::model::CustomNotification;
|
||||
use rmcp::model::CustomRequest;
|
||||
use rmcp::model::Extensions;
|
||||
use rmcp::model::InitializeRequestParam;
|
||||
use rmcp::model::InitializeResult;
|
||||
use rmcp::model::ListResourceTemplatesResult;
|
||||
use rmcp::model::ListResourcesResult;
|
||||
use rmcp::model::ListToolsResult;
|
||||
use rmcp::model::PaginatedRequestParam;
|
||||
use rmcp::model::ReadResourceRequestParam;
|
||||
use rmcp::model::ReadResourceResult;
|
||||
use rmcp::model::RequestId;
|
||||
use rmcp::model::ServerResult;
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::service::RoleClient;
|
||||
use rmcp::service::RunningService;
|
||||
use rmcp::service::{self};
|
||||
@@ -62,9 +56,6 @@ use crate::oauth::StoredOAuthTokens;
|
||||
use crate::program_resolver;
|
||||
use crate::utils::apply_default_headers;
|
||||
use crate::utils::build_default_headers;
|
||||
use crate::utils::convert_call_tool_result;
|
||||
use crate::utils::convert_to_mcp;
|
||||
use crate::utils::convert_to_rmcp;
|
||||
use crate::utils::create_env_for_mcp_server;
|
||||
use crate::utils::run_with_timeout;
|
||||
|
||||
@@ -229,12 +220,11 @@ impl RmcpClient {
|
||||
/// https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#initialization
|
||||
pub async fn initialize(
|
||||
&self,
|
||||
params: InitializeRequestParams,
|
||||
params: InitializeRequestParam,
|
||||
timeout: Option<Duration>,
|
||||
send_elicitation: SendElicitation,
|
||||
) -> Result<InitializeResult> {
|
||||
let rmcp_params: InitializeRequestParam = convert_to_rmcp(params.clone())?;
|
||||
let client_handler = LoggingClientHandler::new(rmcp_params, send_elicitation);
|
||||
let client_handler = LoggingClientHandler::new(params.clone(), send_elicitation);
|
||||
|
||||
let (transport, oauth_persistor) = {
|
||||
let mut guard = self.state.lock().await;
|
||||
@@ -275,7 +265,7 @@ impl RmcpClient {
|
||||
.peer()
|
||||
.peer_info()
|
||||
.ok_or_else(|| anyhow!("handshake succeeded but server info was missing"))?;
|
||||
let initialize_result = convert_to_mcp(initialize_result_rmcp)?;
|
||||
let initialize_result = initialize_result_rmcp.clone();
|
||||
|
||||
{
|
||||
let mut guard = self.state.lock().await;
|
||||
@@ -296,28 +286,26 @@ impl RmcpClient {
|
||||
|
||||
pub async fn list_tools(
|
||||
&self,
|
||||
params: Option<ListToolsRequestParams>,
|
||||
params: Option<PaginatedRequestParam>,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<ListToolsResult> {
|
||||
let result = self.list_tools_with_connector_ids(params, timeout).await?;
|
||||
Ok(ListToolsResult {
|
||||
next_cursor: result.next_cursor,
|
||||
tools: result.tools.into_iter().map(|tool| tool.tool).collect(),
|
||||
})
|
||||
self.refresh_oauth_if_needed().await;
|
||||
let service = self.service().await?;
|
||||
let fut = service.list_tools(params);
|
||||
let result = run_with_timeout(fut, timeout, "tools/list").await?;
|
||||
self.persist_oauth_tokens().await;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn list_tools_with_connector_ids(
|
||||
&self,
|
||||
params: Option<ListToolsRequestParams>,
|
||||
params: Option<PaginatedRequestParam>,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<ListToolsWithConnectorIdResult> {
|
||||
self.refresh_oauth_if_needed().await;
|
||||
let service = self.service().await?;
|
||||
let rmcp_params = params
|
||||
.map(convert_to_rmcp::<_, PaginatedRequestParam>)
|
||||
.transpose()?;
|
||||
|
||||
let fut = service.list_tools(rmcp_params);
|
||||
let fut = service.list_tools(params);
|
||||
let result = run_with_timeout(fut, timeout, "tools/list").await?;
|
||||
let tools = result
|
||||
.tools
|
||||
@@ -327,7 +315,6 @@ impl RmcpClient {
|
||||
let connector_id = Self::meta_string(meta, "connector_id");
|
||||
let connector_name = Self::meta_string(meta, "connector_name")
|
||||
.or_else(|| Self::meta_string(meta, "connector_display_name"));
|
||||
let tool = convert_to_mcp(tool)?;
|
||||
Ok(ToolWithConnectorId {
|
||||
tool,
|
||||
connector_id,
|
||||
@@ -352,53 +339,43 @@ impl RmcpClient {
|
||||
|
||||
pub async fn list_resources(
|
||||
&self,
|
||||
params: Option<ListResourcesRequestParams>,
|
||||
params: Option<PaginatedRequestParam>,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<ListResourcesResult> {
|
||||
self.refresh_oauth_if_needed().await;
|
||||
let service = self.service().await?;
|
||||
let rmcp_params = params
|
||||
.map(convert_to_rmcp::<_, PaginatedRequestParam>)
|
||||
.transpose()?;
|
||||
|
||||
let fut = service.list_resources(rmcp_params);
|
||||
let fut = service.list_resources(params);
|
||||
let result = run_with_timeout(fut, timeout, "resources/list").await?;
|
||||
let converted = convert_to_mcp(result)?;
|
||||
self.persist_oauth_tokens().await;
|
||||
Ok(converted)
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn list_resource_templates(
|
||||
&self,
|
||||
params: Option<ListResourceTemplatesRequestParams>,
|
||||
params: Option<PaginatedRequestParam>,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<ListResourceTemplatesResult> {
|
||||
self.refresh_oauth_if_needed().await;
|
||||
let service = self.service().await?;
|
||||
let rmcp_params = params
|
||||
.map(convert_to_rmcp::<_, PaginatedRequestParam>)
|
||||
.transpose()?;
|
||||
|
||||
let fut = service.list_resource_templates(rmcp_params);
|
||||
let fut = service.list_resource_templates(params);
|
||||
let result = run_with_timeout(fut, timeout, "resources/templates/list").await?;
|
||||
let converted = convert_to_mcp(result)?;
|
||||
self.persist_oauth_tokens().await;
|
||||
Ok(converted)
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn read_resource(
|
||||
&self,
|
||||
params: ReadResourceRequestParams,
|
||||
params: ReadResourceRequestParam,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<ReadResourceResult> {
|
||||
self.refresh_oauth_if_needed().await;
|
||||
let service = self.service().await?;
|
||||
let rmcp_params: ReadResourceRequestParam = convert_to_rmcp(params)?;
|
||||
let fut = service.read_resource(rmcp_params);
|
||||
let fut = service.read_resource(params);
|
||||
let result = run_with_timeout(fut, timeout, "resources/read").await?;
|
||||
let converted = convert_to_mcp(result)?;
|
||||
self.persist_oauth_tokens().await;
|
||||
Ok(converted)
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn call_tool(
|
||||
@@ -409,13 +386,23 @@ impl RmcpClient {
|
||||
) -> Result<CallToolResult> {
|
||||
self.refresh_oauth_if_needed().await;
|
||||
let service = self.service().await?;
|
||||
let params = CallToolRequestParams { arguments, name };
|
||||
let rmcp_params: CallToolRequestParam = convert_to_rmcp(params)?;
|
||||
let arguments = match arguments {
|
||||
Some(Value::Object(map)) => Some(map),
|
||||
Some(other) => {
|
||||
return Err(anyhow!(
|
||||
"MCP tool arguments must be a JSON object, got {other}"
|
||||
));
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let rmcp_params = CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments,
|
||||
};
|
||||
let fut = service.call_tool(rmcp_params);
|
||||
let rmcp_result = run_with_timeout(fut, timeout, "tools/call").await?;
|
||||
let converted = convert_call_tool_result(rmcp_result)?;
|
||||
let result = run_with_timeout(fut, timeout, "tools/call").await?;
|
||||
self.persist_oauth_tokens().await;
|
||||
Ok(converted)
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn send_custom_notification(
|
||||
|
||||
@@ -5,14 +5,11 @@ use std::time::Duration;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use mcp_types::CallToolResult;
|
||||
use reqwest::ClientBuilder;
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::header::HeaderName;
|
||||
use reqwest::header::HeaderValue;
|
||||
use rmcp::model::CallToolResult as RmcpCallToolResult;
|
||||
use rmcp::service::ServiceError;
|
||||
use serde_json::Value;
|
||||
use tokio::time;
|
||||
|
||||
pub(crate) async fn run_with_timeout<F, T>(
|
||||
@@ -33,45 +30,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn convert_call_tool_result(result: RmcpCallToolResult) -> Result<CallToolResult> {
|
||||
let mut value = serde_json::to_value(result)?;
|
||||
if let Some(obj) = value.as_object_mut()
|
||||
&& (obj.get("content").is_none()
|
||||
|| obj.get("content").is_some_and(serde_json::Value::is_null))
|
||||
{
|
||||
obj.insert("content".to_string(), Value::Array(Vec::new()));
|
||||
}
|
||||
serde_json::from_value(value).context("failed to convert call tool result")
|
||||
}
|
||||
|
||||
/// Convert from mcp-types to Rust SDK types.
|
||||
///
|
||||
/// The Rust SDK types are the same as our mcp-types crate because they are both
|
||||
/// derived from the same MCP specification.
|
||||
/// As a result, it should be safe to convert directly from one to the other.
|
||||
pub(crate) fn convert_to_rmcp<T, U>(value: T) -> Result<U>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
U: serde::de::DeserializeOwned,
|
||||
{
|
||||
let json = serde_json::to_value(value)?;
|
||||
serde_json::from_value(json).map_err(|err| anyhow!(err))
|
||||
}
|
||||
|
||||
/// Convert from Rust SDK types to mcp-types.
|
||||
///
|
||||
/// The Rust SDK types are the same as our mcp-types crate because they are both
|
||||
/// derived from the same MCP specification.
|
||||
/// As a result, it should be safe to convert directly from one to the other.
|
||||
pub(crate) fn convert_to_mcp<T, U>(value: T) -> Result<U>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
U: serde::de::DeserializeOwned,
|
||||
{
|
||||
let json = serde_json::to_value(value)?;
|
||||
serde_json::from_value(json).map_err(|err| anyhow!(err))
|
||||
}
|
||||
|
||||
pub(crate) fn create_env_for_mcp_server(
|
||||
extra_env: Option<HashMap<String, String>>,
|
||||
env_vars: &[String],
|
||||
@@ -203,10 +161,7 @@ pub(crate) const DEFAULT_ENV_VARS: &[&str] = &[
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mcp_types::ContentBlock;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::CallToolResult as RmcpCallToolResult;
|
||||
use serde_json::json;
|
||||
|
||||
use serial_test::serial;
|
||||
use std::ffi::OsString;
|
||||
@@ -260,43 +215,4 @@ mod tests {
|
||||
let env = create_env_for_mcp_server(None, &[custom_var.to_string()]);
|
||||
assert_eq!(env.get(custom_var), Some(&value.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_call_tool_result_defaults_missing_content() -> Result<()> {
|
||||
let structured_content = json!({ "key": "value" });
|
||||
let rmcp_result = RmcpCallToolResult {
|
||||
content: vec![],
|
||||
structured_content: Some(structured_content.clone()),
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let result = convert_call_tool_result(rmcp_result)?;
|
||||
|
||||
assert!(result.content.is_empty());
|
||||
assert_eq!(result.structured_content, Some(structured_content));
|
||||
assert_eq!(result.is_error, Some(true));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_call_tool_result_preserves_existing_content() -> Result<()> {
|
||||
let rmcp_result = RmcpCallToolResult::success(vec![rmcp::model::Content::text("hello")]);
|
||||
|
||||
let result = convert_call_tool_result(rmcp_result)?;
|
||||
|
||||
assert_eq!(result.content.len(), 1);
|
||||
match &result.content[0] {
|
||||
ContentBlock::TextContent(text_content) => {
|
||||
assert_eq!(text_content.text, "hello");
|
||||
assert_eq!(text_content.r#type, "text");
|
||||
}
|
||||
other => panic!("expected text content got {other:?}"),
|
||||
}
|
||||
assert_eq!(result.structured_content, None);
|
||||
assert_eq!(result.is_error, Some(false));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,15 @@ use codex_rmcp_client::ElicitationResponse;
|
||||
use codex_rmcp_client::RmcpClient;
|
||||
use codex_utils_cargo_bin::CargoBinError;
|
||||
use futures::FutureExt as _;
|
||||
use mcp_types::ClientCapabilities;
|
||||
use mcp_types::Implementation;
|
||||
use mcp_types::InitializeRequestParams;
|
||||
use mcp_types::ListResourceTemplatesResult;
|
||||
use mcp_types::ReadResourceRequestParams;
|
||||
use mcp_types::ReadResourceResultContents;
|
||||
use mcp_types::Resource;
|
||||
use mcp_types::ResourceTemplate;
|
||||
use mcp_types::TextResourceContents;
|
||||
use rmcp::model::AnnotateAble;
|
||||
use rmcp::model::ClientCapabilities;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
use rmcp::model::Implementation;
|
||||
use rmcp::model::InitializeRequestParam;
|
||||
use rmcp::model::ListResourceTemplatesResult;
|
||||
use rmcp::model::ProtocolVersion;
|
||||
use rmcp::model::ReadResourceRequestParam;
|
||||
use rmcp::model::ResourceContents;
|
||||
use serde_json::json;
|
||||
|
||||
const RESOURCE_URI: &str = "memo://codex/example-note";
|
||||
@@ -24,21 +24,24 @@ fn stdio_server_bin() -> Result<PathBuf, CargoBinError> {
|
||||
codex_utils_cargo_bin::cargo_bin("test_stdio_server")
|
||||
}
|
||||
|
||||
fn init_params() -> InitializeRequestParams {
|
||||
InitializeRequestParams {
|
||||
fn init_params() -> InitializeRequestParam {
|
||||
InitializeRequestParam {
|
||||
capabilities: ClientCapabilities {
|
||||
experimental: None,
|
||||
roots: None,
|
||||
sampling: None,
|
||||
elicitation: Some(json!({})),
|
||||
elicitation: Some(ElicitationCapability {
|
||||
schema_validation: None,
|
||||
}),
|
||||
},
|
||||
client_info: Implementation {
|
||||
name: "codex-test".into(),
|
||||
version: "0.0.0-test".into(),
|
||||
title: Some("Codex rmcp resource test".into()),
|
||||
user_agent: None,
|
||||
icons: None,
|
||||
website_url: None,
|
||||
},
|
||||
protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_string(),
|
||||
protocol_version: ProtocolVersion::V_2025_06_18,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,15 +82,17 @@ async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> {
|
||||
.expect("memo resource present");
|
||||
assert_eq!(
|
||||
memo,
|
||||
&Resource {
|
||||
annotations: None,
|
||||
&rmcp::model::RawResource {
|
||||
uri: RESOURCE_URI.to_string(),
|
||||
name: "example-note".to_string(),
|
||||
title: Some("Example Note".to_string()),
|
||||
description: Some("A sample MCP resource exposed for integration tests.".to_string()),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
name: "example-note".to_string(),
|
||||
size: None,
|
||||
title: Some("Example Note".to_string()),
|
||||
uri: RESOURCE_URI.to_string(),
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
.no_annotation()
|
||||
);
|
||||
let templates = client
|
||||
.list_resource_templates(None, Some(Duration::from_secs(5)))
|
||||
@@ -95,39 +100,39 @@ async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
templates,
|
||||
ListResourceTemplatesResult {
|
||||
meta: None,
|
||||
next_cursor: None,
|
||||
resource_templates: vec![ResourceTemplate {
|
||||
annotations: None,
|
||||
description: Some(
|
||||
"Template for memo://codex/{slug} resources used in tests.".to_string()
|
||||
),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
name: "codex-memo".to_string(),
|
||||
title: Some("Codex Memo".to_string()),
|
||||
uri_template: "memo://codex/{slug}".to_string(),
|
||||
}],
|
||||
resource_templates: vec![
|
||||
rmcp::model::RawResourceTemplate {
|
||||
uri_template: "memo://codex/{slug}".to_string(),
|
||||
name: "codex-memo".to_string(),
|
||||
title: Some("Codex Memo".to_string()),
|
||||
description: Some(
|
||||
"Template for memo://codex/{slug} resources used in tests.".to_string(),
|
||||
),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
}
|
||||
.no_annotation()
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
let read = client
|
||||
.read_resource(
|
||||
ReadResourceRequestParams {
|
||||
ReadResourceRequestParam {
|
||||
uri: RESOURCE_URI.to_string(),
|
||||
},
|
||||
Some(Duration::from_secs(5)),
|
||||
)
|
||||
.await?;
|
||||
let ReadResourceResultContents::TextResourceContents(text) =
|
||||
read.contents.first().expect("resource contents present")
|
||||
else {
|
||||
panic!("expected text resource");
|
||||
};
|
||||
let text = read.contents.first().expect("resource contents present");
|
||||
assert_eq!(
|
||||
text,
|
||||
&TextResourceContents {
|
||||
text: "This is a sample MCP resource served by the rmcp test server.".to_string(),
|
||||
&ResourceContents::TextResourceContents {
|
||||
uri: RESOURCE_URI.to_string(),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
text: "This is a sample MCP resource served by the rmcp test server.".to_string(),
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user