Make MCP resource read threadless (#18292)

## Summary

Making thread id optional so that we can better cache resources for MCPs
for connectors since their resource templates is universal and not
particular to projects.

- Make `mcpServer/resource/read` accept an optional `threadId`
- Read resources from the current MCP config when no thread is supplied
- Keep the existing thread-scoped path when `threadId` is present
- Update the generated schemas, README, and integration coverage

## Testing
- `just write-app-server-schema`
- `just fmt`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-mcp`
- `cargo test -p codex-app-server --test all mcp_resource`
- `just fix -p codex-mcp`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-app-server`
This commit is contained in:
Matthew Zeng
2026-04-20 19:59:36 -07:00
committed by GitHub
Unverified
parent 58e7605efc
commit 1132ef887c
11 changed files with 249 additions and 67 deletions
@@ -290,6 +290,7 @@ use codex_mcp::McpSnapshotDetail;
use codex_mcp::collect_mcp_server_status_snapshot_with_detail_and_authorization_header;
use codex_mcp::discover_supported_scopes;
use codex_mcp::effective_mcp_servers_with_authorization_header;
use codex_mcp::read_mcp_resource as read_mcp_resource_without_thread;
use codex_mcp::resolve_oauth_scopes;
use codex_models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_protocol::ThreadId;
@@ -5922,50 +5923,115 @@ impl CodexMessageProcessor {
params: McpResourceReadParams,
) {
let outgoing = Arc::clone(&self.outgoing);
let (_, thread) = match self.load_thread(&params.thread_id).await {
Ok(thread) => thread,
let McpResourceReadParams {
thread_id,
server,
uri,
} = params;
if let Some(thread_id) = thread_id {
let (_, thread) = match self.load_thread(&thread_id).await {
Ok(thread) => thread,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return;
}
};
tokio::spawn(async move {
let result = thread.read_mcp_resource(&server, &uri).await;
Self::send_mcp_resource_read_response(outgoing, request_id, result).await;
});
return;
}
let config = match self.load_latest_config(/*fallback_cwd*/ None).await {
Ok(config) => config,
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return;
}
};
let mcp_config = config
.to_mcp_config(self.thread_manager.plugins_manager().as_ref())
.await;
let auth = self.auth_manager.auth().await;
let runtime_environment = match self.thread_manager.environment_manager().current().await {
Ok(Some(environment)) => {
// Resource reads without a thread have no turn cwd. This fallback
// is used only by executor-backed stdio MCPs whose config omits `cwd`.
McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf())
}
Ok(None) => McpRuntimeEnvironment::new(
Arc::new(codex_exec_server::Environment::default()),
config.cwd.to_path_buf(),
),
Err(err) => {
let error = JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("failed to create environment: {err}"),
data: None,
};
self.outgoing.send_error(request_id, error).await;
return;
}
};
tokio::spawn(async move {
let result = thread.read_mcp_resource(&params.server, &params.uri).await;
match result {
Ok(result) => match serde_json::from_value::<McpResourceReadResponse>(result) {
Ok(response) => {
outgoing.send_response(request_id, response).await;
}
Err(error) => {
outgoing
.send_error(
request_id,
JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!(
"failed to deserialize MCP resource read response: {error}"
),
data: None,
},
)
.await;
}
},
let result = match read_mcp_resource_without_thread(
&mcp_config,
auth.as_ref(),
runtime_environment,
&server,
&uri,
)
.await
{
Ok(result) => serde_json::to_value(result).map_err(anyhow::Error::from),
Err(error) => Err(error),
};
Self::send_mcp_resource_read_response(outgoing, request_id, result).await;
});
}
async fn send_mcp_resource_read_response(
outgoing: Arc<OutgoingMessageSender>,
request_id: ConnectionRequestId,
result: anyhow::Result<serde_json::Value>,
) {
match result {
Ok(result) => match serde_json::from_value::<McpResourceReadResponse>(result) {
Ok(response) => {
outgoing.send_response(request_id, response).await;
}
Err(error) => {
outgoing
.send_error(
request_id,
JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("{error:#}"),
message: format!(
"failed to deserialize MCP resource read response: {error}"
),
data: None,
},
)
.await;
}
},
Err(error) => {
outgoing
.send_error(
request_id,
JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("{error:#}"),
data: None,
},
)
.await;
}
});
}
}
async fn call_mcp_server_tool(