Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions codex-rs/app-server-protocol/schema/json/ClientRequest.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1930,6 +1930,7 @@ mod tests {
thread_id: Some("thread-1".to_string()),
server: "server-a".to_string(),
uri: "file:///tmp/resource".to_string(),
meta: None,
},
};
assert_eq!(
Expand Down Expand Up @@ -2108,6 +2109,7 @@ mod tests {
thread_id: None,
server: "server-a".to_string(),
uri: "file:///tmp/resource".to_string(),
meta: None,
},
};
assert_eq!(mcp_resource_read.serialization_scope(), None);
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ pub struct McpResourceReadParams {
pub thread_id: Option<String>,
pub server: String,
pub uri: String,
#[serde(rename = "_meta")]
#[ts(optional = nullable, rename = "_meta")]
pub meta: Option<BTreeMap<String, JsonValue>>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
Expand Down
18 changes: 18 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ fn test_absolute_path() -> AbsolutePathBuf {
absolute_path("readable")
}

#[test]
fn mcp_resource_read_params_deserialize_meta() {
let params: McpResourceReadParams = serde_json::from_value(json!({
"server": "codex_apps",
"uri": "test://codex/resource",
"_meta": { "traceId": "trace-123" },
}))
.expect("deserialize resource read params");

assert_eq!(
params.meta,
Some(BTreeMap::from([(
"traceId".to_string(),
json!("trace-123")
)]))
);
}

#[test]
fn thread_sources_round_trip_as_scalar_labels() {
for (source, label) in [
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ Example with notification opt-out:
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`.
- `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly.
- `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, `uri`, and protocol `_meta`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly.
- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result.
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/app-server/src/request_processors/mcp_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,14 +360,15 @@ impl McpRequestProcessor {
thread_id,
server,
uri,
meta,
} = params;

if let Some(thread_id) = thread_id {
let (_, thread) = self.load_thread(&thread_id).await?;
let request_id = request_id.clone();

tokio::spawn(async move {
let result = thread.read_mcp_resource(&server, &uri).await;
let result = thread.read_mcp_resource(&server, &uri, meta).await;
Self::send_mcp_resource_read_response(outgoing, request_id, result).await;
});
return Ok(());
Expand Down Expand Up @@ -395,6 +396,7 @@ impl McpRequestProcessor {
runtime_context,
&server,
&uri,
meta,
)
.await
.and_then(|result| serde_json::to_value(result).map_err(anyhow::Error::from));
Expand Down
12 changes: 12 additions & 0 deletions codex-rs/app-server/tests/common/test_app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,18 @@ impl TestAppServer {
self.send_request("mcpServer/resource/read", params).await
}

/// Send an `mcpServer/resource/read` JSON-RPC request with protocol metadata.
pub async fn send_mcp_resource_read_request_with_meta(
&mut self,
params: McpResourceReadParams,
meta: serde_json::Value,
) -> anyhow::Result<i64> {
let mut params = serde_json::to_value(params)?;
params["_meta"] = meta;
self.send_request("mcpServer/resource/read", Some(params))
.await
}

/// Send an `mcpServer/tool/call` JSON-RPC request.
pub async fn send_mcp_server_tool_call_request(
&mut self,
Expand Down
68 changes: 53 additions & 15 deletions codex-rs/app-server/tests/suite/v2/mcp_resource.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::time::Duration;
Expand Down Expand Up @@ -84,9 +85,9 @@ const SKILLS_READ_CALL_ID: &str = "skills-read";
const SKILLS_READ_AGAIN_CALL_ID: &str = "skills-read-again";

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn mcp_resource_read_returns_resource_contents() -> Result<()> {
async fn mcp_resource_read_forwards_meta() -> Result<()> {
let responses_server = responses::start_mock_server().await;
let (apps_server_url, _apps_server_calls, apps_server_handle) =
let (apps_server_url, apps_server_calls, apps_server_handle) =
start_resource_apps_mcp_server().await?;
let responses_server_uri = responses_server.uri();
let (_codex_home, mut mcp) =
Expand All @@ -105,12 +106,17 @@ async fn mcp_resource_read_returns_resource_contents() -> Result<()> {
.await??;
let ThreadStartResponse { thread, .. } = to_response(thread_start_resp)?;

let expected_meta = resource_read_request_meta();
let read_request_id = mcp
.send_mcp_resource_read_request(McpResourceReadParams {
thread_id: Some(thread.id),
server: "codex_apps".to_string(),
uri: TEST_RESOURCE_URI.to_string(),
})
.send_mcp_resource_read_request_with_meta(
McpResourceReadParams {
thread_id: Some(thread.id),
server: "codex_apps".to_string(),
uri: TEST_RESOURCE_URI.to_string(),
meta: None,
},
serde_json::to_value(&expected_meta)?,
)
.await?;
let read_response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
Expand All @@ -121,6 +127,7 @@ async fn mcp_resource_read_returns_resource_contents() -> Result<()> {
to_response::<McpResourceReadResponse>(read_response)?,
expected_resource_read_response()
);
assert_eq!(apps_server_calls.resource_read_metas(), vec![expected_meta]);

apps_server_handle.abort();
let _ = apps_server_handle.await;
Expand Down Expand Up @@ -526,8 +533,8 @@ enabled = false
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn mcp_resource_read_returns_resource_contents_without_thread() -> Result<()> {
let (apps_server_url, _apps_server_calls, apps_server_handle) =
async fn mcp_resource_read_forwards_meta_without_thread() -> Result<()> {
let (apps_server_url, apps_server_calls, apps_server_handle) =
start_resource_apps_mcp_server().await?;

let codex_home = TempDir::new()?;
Expand Down Expand Up @@ -555,12 +562,17 @@ apps = true
let mut mcp = TestAppServer::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

let expected_meta = resource_read_request_meta();
let read_request_id = mcp
.send_mcp_resource_read_request(McpResourceReadParams {
thread_id: None,
server: "codex_apps".to_string(),
uri: TEST_RESOURCE_URI.to_string(),
})
.send_mcp_resource_read_request_with_meta(
McpResourceReadParams {
thread_id: None,
server: "codex_apps".to_string(),
uri: TEST_RESOURCE_URI.to_string(),
meta: None,
},
serde_json::to_value(&expected_meta)?,
)
.await?;
let read_response: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
Expand All @@ -572,6 +584,7 @@ apps = true
to_response::<McpResourceReadResponse>(read_response)?,
expected_resource_read_response()
);
assert_eq!(apps_server_calls.resource_read_metas(), vec![expected_meta]);

apps_server_handle.abort();
let _ = apps_server_handle.await;
Expand Down Expand Up @@ -624,6 +637,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> {
thread_id: Some("00000000-0000-4000-8000-000000000000".to_string()),
server: "codex_apps".to_string(),
uri: TEST_RESOURCE_URI.to_string(),
meta: None,
},
})
.await;
Expand Down Expand Up @@ -746,6 +760,7 @@ struct ResourceAppsMcpCalls {
list_resources: AtomicUsize,
main_prompt_reads: AtomicUsize,
reference_reads: AtomicUsize,
resource_read_metas: Mutex<Vec<Meta>>,
}

impl ResourceAppsMcpCalls {
Expand All @@ -756,6 +771,13 @@ impl ResourceAppsMcpCalls {
reference_reads: self.reference_reads.load(Ordering::Relaxed),
}
}

fn resource_read_metas(&self) -> Vec<Meta> {
self.resource_read_metas
.lock()
.expect("resource read metadata lock poisoned")
.clone()
}
}

#[derive(Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -827,8 +849,17 @@ impl ServerHandler for ResourceAppsMcpServer {
async fn read_resource(
&self,
request: ReadResourceRequestParams,
_context: RequestContext<RoleServer>,
context: RequestContext<RoleServer>,
) -> Result<ReadResourceResult, rmcp::ErrorData> {
let mut meta = context.meta;
// rmcp adds a transport progress token to outgoing requests. Capture
// only the client-supplied metadata that this test is exercising.
meta.0.remove("progressToken");
self.calls
.resource_read_metas
.lock()
.expect("resource read metadata lock poisoned")
.push(meta);
let uri = request.uri;
if uri == SKILL_MAIN_PROMPT_URI {
self.calls.main_prompt_reads.fetch_add(1, Ordering::Relaxed);
Expand Down Expand Up @@ -899,3 +930,10 @@ fn skill_resource_meta(plugin_name: &str, skill_name: &str) -> Meta {
("skill_name".to_string(), json!(skill_name)),
]))
}

fn resource_read_request_meta() -> Meta {
Meta(serde_json::Map::from_iter([
("traceId".to_string(), json!("trace-123")),
("context".to_string(), json!({ "source": "app-server" })),
]))
}
12 changes: 9 additions & 3 deletions codex-rs/codex-mcp/src/mcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub use auth::should_retry_without_scopes;

pub(crate) mod auth;

use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::env;
Expand All @@ -36,6 +37,7 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::McpAuthStatus;
use rmcp::model::ElicitationCapability;
use rmcp::model::Meta;
use rmcp::model::ReadResourceRequestParams;
use rmcp::model::ReadResourceResult;
use serde_json::Value;
Expand Down Expand Up @@ -301,6 +303,7 @@ pub async fn read_mcp_resource(
runtime_context: McpRuntimeContext,
server: &str,
uri: &str,
meta: Option<BTreeMap<String, Value>>,
) -> anyhow::Result<ReadResourceResult> {
let mut mcp_servers = effective_mcp_servers(config, auth);
mcp_servers.retain(|name, _| name == server);
Expand Down Expand Up @@ -336,9 +339,12 @@ pub async fn read_mcp_resource(
)
.await;

let result = manager
.read_resource(server, ReadResourceRequestParams::new(uri))
.await;
let params = if let Some(meta) = meta {
ReadResourceRequestParams::new(uri).with_meta(Meta(meta.into_iter().collect()))
} else {
ReadResourceRequestParams::new(uri)
};
let result = manager.read_resource(server, params).await;
cancel_token.cancel();
result
}
Expand Down
13 changes: 8 additions & 5 deletions codex-rs/core/src/codex_thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ use codex_thread_store::ThreadStoreResult;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::LegacyAppPathString;
use codex_utils_path_uri::PathUri;
use rmcp::model::Meta;
use rmcp::model::ReadResourceRequestParams;
use std::collections::BTreeMap;
use std::collections::HashMap;
Expand Down Expand Up @@ -607,12 +608,14 @@ impl CodexThread {
&self,
server: &str,
uri: &str,
meta: Option<BTreeMap<String, serde_json::Value>>,
) -> anyhow::Result<serde_json::Value> {
let result = self
.codex
.session
.read_resource(server, ReadResourceRequestParams::new(uri))
.await?;
let params = if let Some(meta) = meta {
ReadResourceRequestParams::new(uri).with_meta(Meta(meta.into_iter().collect()))
} else {
ReadResourceRequestParams::new(uri)
};
let result = self.codex.session.read_resource(server, params).await?;

Ok(serde_json::to_value(result)?)
}
Expand Down
Loading