Files
codex/codex-rs/rmcp-client/tests/resources.rs
rka-oai 035295b46e Improve sandboxing, MCP errors, and cached approvals (#41196)
Expand Windows deny-read globs robustly across protected directories while preserving filesystem enforcement. Preserve structured MCP tool and resource errors through app-server responses. Bind cached Guardian classifications to current local and root authorization state so stale scores cannot approve actions.

Add regression coverage for the sandbox resolver, structured protocol errors, and authorization changes.

GitOrigin-RevId: 4b80ed724d869afeca79204222d8465fa99d3a24
2026-08-27 20:05:08 +00:00

145 lines
4.7 KiB
Rust

use std::ffi::OsString;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use codex_rmcp_client::ElicitationAction;
use codex_rmcp_client::ElicitationResponse;
use codex_rmcp_client::LocalStdioServerLauncher;
use codex_rmcp_client::RmcpClient;
use codex_rmcp_client::mcp_error;
use codex_utils_cargo_bin::CargoBinError;
use futures::FutureExt as _;
use pretty_assertions::assert_eq;
use rmcp::model::ClientCapabilities;
use rmcp::model::ElicitationCapability;
use rmcp::model::FormElicitationCapability;
use rmcp::model::Implementation;
use rmcp::model::InitializeRequestParams;
use rmcp::model::ListResourceTemplatesResult;
use rmcp::model::ProtocolVersion;
use rmcp::model::ReadResourceRequestParams;
use rmcp::model::ResourceContents;
use serde_json::json;
const RESOURCE_URI: &str = "memo://codex/example-note";
fn stdio_server_bin() -> Result<PathBuf, CargoBinError> {
codex_utils_cargo_bin::cargo_bin("test_stdio_server")
}
fn init_params() -> InitializeRequestParams {
let mut capabilities = ClientCapabilities::default();
capabilities.elicitation =
Some(ElicitationCapability::new().with_form(FormElicitationCapability::new()));
InitializeRequestParams::new(
capabilities,
Implementation::new("codex-test", "0.0.0-test").with_title("Codex rmcp resource test"),
)
.with_protocol_version(ProtocolVersion::V_2025_06_18)
}
async fn resource_client() -> anyhow::Result<RmcpClient> {
let client = RmcpClient::new_stdio_client(
stdio_server_bin()?.into(),
Vec::<OsString>::new(),
/*env*/ None,
&[],
/*cwd*/ None,
Arc::new(LocalStdioServerLauncher::new(std::env::current_dir()?)),
)
.await?;
client
.initialize(
init_params(),
Some(Duration::from_secs(5)),
Box::new(|_, _| {
async {
Ok(ElicitationResponse {
action: ElicitationAction::Accept,
content: Some(json!({})),
meta: None,
})
}
.boxed()
}),
)
.await?;
Ok(client)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> {
let client = resource_client().await?;
let list = client
.list_resources(/*params*/ None, Some(Duration::from_secs(5)))
.await?;
let memo = list
.resources
.iter()
.find(|resource| resource.uri == RESOURCE_URI)
.expect("memo resource present");
assert_eq!(
memo,
&rmcp::model::Resource::new(RESOURCE_URI, "example-note")
.with_title("Example Note")
.with_description("A sample MCP resource exposed for integration tests.")
.with_mime_type("text/plain")
);
let templates = client
.list_resource_templates(/*params*/ None, Some(Duration::from_secs(5)))
.await?;
let mut expected_templates = ListResourceTemplatesResult::with_all_items(vec![
rmcp::model::ResourceTemplate::new("memo://codex/{slug}", "codex-memo")
.with_title("Codex Memo")
.with_description("Template for memo://codex/{slug} resources used in tests.")
.with_mime_type("text/plain"),
]);
expected_templates.result_type = None;
assert_eq!(templates, expected_templates);
let read = client
.read_resource(
ReadResourceRequestParams::new(RESOURCE_URI),
Some(Duration::from_secs(5)),
)
.await?;
let text = read.contents.first().expect("resource contents present");
assert_eq!(
text,
&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,
}
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn rmcp_client_preserves_each_resource_error() -> anyhow::Result<()> {
let client = resource_client().await?;
for uri in ["memo://codex/missing-first", "memo://codex/missing-second"] {
let error = client
.read_resource(
ReadResourceRequestParams::new(uri),
Some(Duration::from_secs(5)),
)
.await
.expect_err("missing resource must return a protocol error")
.context("resources/read failed");
assert_eq!(
mcp_error(&error),
Some(&rmcp::ErrorData::resource_not_found(
"resource_not_found",
Some(json!({ "uri": uri })),
))
);
}
Ok(())
}