mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Forward caller metadata on MCP resource reads (#59509)
## What changed Accept an optional `_meta` object on `mcpServer/resource/read` and forward it to MCP `resources/read`, including reads bound to an originating tool call. Update the protocol schemas and generated TypeScript and Python types. Reject caller metadata containing host-owned `threadId`, `x-codex-turn-metadata`, or `codex/connectionLease` keys. For `codex_apps`, also reserve `connector_id`, `link_id`, and `_codex_apps.resource_uri` to preserve app and account routing. ## Testing Add integration coverage for metadata forwarding through stdio, hosted app reads with and without a thread, and origin-bound widget reads. Cover rejection of reserved keys and non-object metadata. GitOrigin-RevId: 227ee0fb5fe15fa2ff2ec66d4d5ae16de8935a8c
This commit is contained in:
@@ -2173,6 +2173,14 @@
|
||||
},
|
||||
"McpResourceReadParams": {
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"additionalProperties": true,
|
||||
"description": "Additional MCP request metadata. Account and turn routing keys are reserved.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"connectorId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
+8
@@ -15299,6 +15299,14 @@
|
||||
"McpResourceReadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"additionalProperties": true,
|
||||
"description": "Additional MCP request metadata. Account and turn routing keys are reserved.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"connectorId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
+8
@@ -11260,6 +11260,14 @@
|
||||
"McpResourceReadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"additionalProperties": true,
|
||||
"description": "Additional MCP request metadata. Account and turn routing keys are reserved.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"connectorId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
@@ -22,6 +22,14 @@
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"_meta": {
|
||||
"additionalProperties": true,
|
||||
"description": "Additional MCP request metadata. Account and turn routing keys are reserved.",
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"connectorId": {
|
||||
"type": [
|
||||
"string",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+6
-1
@@ -1,9 +1,14 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { McpResourceReadTarget } from "./McpResourceReadTarget";
|
||||
|
||||
export type McpResourceReadParams = { threadId?: string | null,
|
||||
export type McpResourceReadParams = {
|
||||
/**
|
||||
* Additional MCP request metadata. Account and turn routing keys are reserved.
|
||||
*/
|
||||
_meta?: { [key in string]?: JsonValue } | null, threadId?: string | null,
|
||||
/**
|
||||
* Originating MCP tool call used to select the resource's app.
|
||||
*/
|
||||
|
||||
@@ -2569,6 +2569,7 @@ mod tests {
|
||||
let mcp_resource_read = ClientRequest::McpResourceRead {
|
||||
request_id: request_id(),
|
||||
params: v2::McpResourceReadParams {
|
||||
meta: None,
|
||||
thread_id: Some("thread-1".to_string()),
|
||||
origin_call_id: None,
|
||||
server: "server-a".to_string(),
|
||||
@@ -2763,6 +2764,7 @@ mod tests {
|
||||
let mcp_resource_read = ClientRequest::McpResourceRead {
|
||||
request_id: request_id(),
|
||||
params: v2::McpResourceReadParams {
|
||||
meta: None,
|
||||
thread_id: None,
|
||||
origin_call_id: None,
|
||||
server: "server-a".to_string(),
|
||||
|
||||
@@ -112,6 +112,10 @@ pub struct ListMcpServerStatusResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct McpResourceReadParams {
|
||||
/// Additional MCP request metadata. Account and turn routing keys are reserved.
|
||||
#[serde(rename = "_meta")]
|
||||
#[ts(rename = "_meta", optional = nullable)]
|
||||
pub meta: Option<serde_json::Map<String, JsonValue>>,
|
||||
#[ts(optional = nullable)]
|
||||
pub thread_id: Option<String>,
|
||||
/// Originating MCP tool call used to select the resource's app.
|
||||
|
||||
@@ -141,6 +141,14 @@ The field is null for older history and tools that declare widgets only in
|
||||
result metadata; clients retain catalog discovery for those calls. Existing
|
||||
resource URI fields remain available for older clients.
|
||||
|
||||
Clients can pass an optional `_meta` object on `mcpServer/resource/read`.
|
||||
The app server forwards its entries to MCP `resources/read`, including reads
|
||||
bound to an originating tool call. The keys `x-codex-turn-metadata`, `threadId`,
|
||||
and `codex/connectionLease` are reserved for host context on every MCP server. Reads to `codex_apps`
|
||||
additionally reserve `connector_id`, `link_id`, and the nested
|
||||
`_codex_apps.resource_uri` field for hosted app routing. A request that includes
|
||||
a reserved field in `_meta` is rejected before the MCP read is sent.
|
||||
|
||||
# Initial Daybreak choice (experimental)
|
||||
|
||||
Persistent threads accept `daybreakEnabled` on `thread/start` with the
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::future::Future;
|
||||
|
||||
use crate::thread_state::ThreadStateManager;
|
||||
|
||||
const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId";
|
||||
const MCP_THREAD_ID_META_KEY: &str = "threadId";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct McpRequestProcessor {
|
||||
@@ -611,6 +611,7 @@ impl McpRequestProcessor {
|
||||
) -> Result<(), JSONRPCErrorError> {
|
||||
let outgoing = Arc::clone(&self.outgoing);
|
||||
let McpResourceReadParams {
|
||||
meta,
|
||||
thread_id,
|
||||
origin_call_id,
|
||||
server,
|
||||
@@ -619,7 +620,24 @@ impl McpRequestProcessor {
|
||||
target,
|
||||
} = params;
|
||||
let mut resource_params = ReadResourceRequestParams::new(uri);
|
||||
let mut meta = serde_json::Map::new();
|
||||
let mut meta = meta.unwrap_or_default();
|
||||
let is_codex_apps = server == codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
let reserved_key = meta.iter().find_map(|(key, value)| match key.as_str() {
|
||||
"x-codex-turn-metadata"
|
||||
| MCP_THREAD_ID_META_KEY
|
||||
| codex_mcp::MCP_CONNECTION_LEASE_META_KEY => Some(key.as_str()),
|
||||
"connector_id" | "link_id" if is_codex_apps => Some(key.as_str()),
|
||||
// This legacy routing hint can override an explicit app and account target.
|
||||
"_codex_apps" if is_codex_apps && value.get("resource_uri").is_some() => {
|
||||
Some("_codex_apps.resource_uri")
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
if let Some(key) = reserved_key {
|
||||
return Err(invalid_request(format!(
|
||||
"reserved resource read metadata key: {key}"
|
||||
)));
|
||||
}
|
||||
if let Some(connector_id) = connector_id {
|
||||
meta.insert(
|
||||
"x-codex-turn-metadata".to_string(),
|
||||
@@ -631,7 +649,7 @@ impl McpRequestProcessor {
|
||||
if origin_call_id.is_none()
|
||||
&& let Some(target) = target
|
||||
{
|
||||
if server != codex_mcp::CODEX_APPS_MCP_SERVER_NAME
|
||||
if !is_codex_apps
|
||||
|| target.connector_id.trim().is_empty()
|
||||
|| target
|
||||
.link_id
|
||||
@@ -657,12 +675,11 @@ impl McpRequestProcessor {
|
||||
let request_id = request_id.clone();
|
||||
|
||||
let task = async move {
|
||||
let origin_call_id =
|
||||
origin_call_id.filter(|_| server == codex_mcp::CODEX_APPS_MCP_SERVER_NAME);
|
||||
let origin_call_id = origin_call_id.filter(|_| is_codex_apps);
|
||||
let result = match origin_call_id.as_deref() {
|
||||
Some(call_id) => {
|
||||
thread
|
||||
.read_mcp_resource_for_call(call_id, &resource_params.uri)
|
||||
.read_mcp_resource_for_call(call_id, resource_params)
|
||||
.await
|
||||
}
|
||||
None => thread.read_mcp_resource(&server, resource_params).await,
|
||||
@@ -778,7 +795,7 @@ fn with_mcp_tool_call_thread_id_meta(
|
||||
match meta {
|
||||
Some(serde_json::Value::Object(mut map)) => {
|
||||
map.insert(
|
||||
MCP_TOOL_THREAD_ID_META_KEY.to_string(),
|
||||
MCP_THREAD_ID_META_KEY.to_string(),
|
||||
serde_json::Value::String(thread_id.to_string()),
|
||||
);
|
||||
Some(serde_json::Value::Object(map))
|
||||
@@ -786,7 +803,7 @@ fn with_mcp_tool_call_thread_id_meta(
|
||||
None => {
|
||||
let mut map = serde_json::Map::new();
|
||||
map.insert(
|
||||
MCP_TOOL_THREAD_ID_META_KEY.to_string(),
|
||||
MCP_THREAD_ID_META_KEY.to_string(),
|
||||
serde_json::Value::String(thread_id.to_string()),
|
||||
);
|
||||
Some(serde_json::Value::Object(map))
|
||||
|
||||
@@ -121,6 +121,8 @@ use tokio::sync::Notify;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::timeout;
|
||||
|
||||
#[path = "mcp_resource/caller_metadata.rs"]
|
||||
mod caller_metadata;
|
||||
#[path = "mcp_resource/plugin_metadata_refresh.rs"]
|
||||
mod plugin_metadata_refresh;
|
||||
|
||||
@@ -170,22 +172,28 @@ async fn mcp_resource_read_returns_resource_contents() -> Result<()> {
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
for thread_id in [Some(thread.id), None] {
|
||||
for target in [
|
||||
None,
|
||||
Some(McpResourceReadTarget {
|
||||
connector_id: "calendar".to_string(),
|
||||
link_id: Some("link_calendar".to_string()),
|
||||
}),
|
||||
Some(McpResourceReadTarget {
|
||||
connector_id: "calendar".to_string(),
|
||||
link_id: None,
|
||||
}),
|
||||
] {
|
||||
let metadata_value = json!({"sequence": [1, 2]});
|
||||
let targets = [
|
||||
None,
|
||||
Some(McpResourceReadTarget {
|
||||
connector_id: "calendar".to_string(),
|
||||
link_id: Some("link_calendar".to_string()),
|
||||
}),
|
||||
Some(McpResourceReadTarget {
|
||||
connector_id: "calendar".to_string(),
|
||||
link_id: None,
|
||||
}),
|
||||
];
|
||||
for thread_id in [Some(thread.id.clone()), None] {
|
||||
for target in &targets {
|
||||
let read_response: McpResourceReadResponse = mcp
|
||||
.request(|request_id| ClientRequest::McpResourceRead {
|
||||
request_id,
|
||||
params: McpResourceReadParams {
|
||||
meta: Some(serde_json::Map::from_iter([(
|
||||
"example/trace".into(),
|
||||
metadata_value.clone(),
|
||||
)])),
|
||||
thread_id: thread_id.clone(),
|
||||
origin_call_id: None,
|
||||
server: "codex_apps".to_string(),
|
||||
@@ -203,12 +211,13 @@ async fn mcp_resource_read_returns_resource_contents() -> Result<()> {
|
||||
let metadata = metadata
|
||||
.last()
|
||||
.expect("resource read reached the MCP server");
|
||||
assert_eq!(metadata.get("example/trace"), Some(&metadata_value));
|
||||
assert_eq!(
|
||||
(
|
||||
metadata.get("connector_id").cloned(),
|
||||
metadata.get("link_id").cloned()
|
||||
),
|
||||
match &target {
|
||||
match target {
|
||||
Some(target) => (
|
||||
Some(json!(target.connector_id)),
|
||||
Some(json!(target.link_id)),
|
||||
@@ -219,6 +228,40 @@ async fn mcp_resource_read_returns_resource_contents() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let reads_before_invalid_requests = apps_server_calls.read_metadata.lock().unwrap().len();
|
||||
let other_route = json!({"resource_uri": "/calendar/link_other/events"});
|
||||
let [_, pinned, no_auth] = targets;
|
||||
for (key, value, origin_call_id, target) in [
|
||||
("connector_id", json!("spoofed"), None, None),
|
||||
("link_id", json!("spoofed"), Some("call".into()), None),
|
||||
("_codex_apps", other_route.clone(), None, pinned),
|
||||
("_codex_apps", other_route, None, no_auth),
|
||||
] {
|
||||
let request_id = mcp
|
||||
.send_mcp_resource_read_request(McpResourceReadParams {
|
||||
thread_id: Some(thread.id.clone()),
|
||||
origin_call_id,
|
||||
meta: Some(serde_json::Map::from_iter([(key.into(), value)])),
|
||||
server: "codex_apps".into(),
|
||||
uri: TEST_RESOURCE_URI.into(),
|
||||
connector_id: None,
|
||||
target,
|
||||
})
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert!(error.error.message.contains(key));
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
apps_server_calls.read_metadata.lock().unwrap().len(),
|
||||
reads_before_invalid_requests
|
||||
);
|
||||
|
||||
apps_server_handle.abort();
|
||||
let _ = apps_server_handle.await;
|
||||
Ok(())
|
||||
@@ -271,6 +314,7 @@ async fn mcp_resource_read_preserves_protocol_errors(protocol: ProtocolVersion)
|
||||
for thread_id in [Some(thread.id.clone()), None] {
|
||||
let request_id = mcp
|
||||
.send_mcp_resource_read_request(McpResourceReadParams {
|
||||
meta: None,
|
||||
thread_id,
|
||||
origin_call_id: None,
|
||||
server: server.to_string(),
|
||||
@@ -487,6 +531,7 @@ apps = true
|
||||
.request(|request_id| ClientRequest::McpResourceRead {
|
||||
request_id,
|
||||
params: McpResourceReadParams {
|
||||
meta: None,
|
||||
thread_id: None,
|
||||
origin_call_id: None,
|
||||
server: "codex_apps".to_string(),
|
||||
@@ -501,6 +546,7 @@ apps = true
|
||||
.request(|request_id| ClientRequest::McpResourceRead {
|
||||
request_id,
|
||||
params: McpResourceReadParams {
|
||||
meta: None,
|
||||
thread_id: None,
|
||||
origin_call_id: None,
|
||||
server: "codex_apps".to_string(),
|
||||
@@ -543,6 +589,7 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> {
|
||||
.request(ClientRequest::McpResourceRead {
|
||||
request_id: RequestId::Integer(1),
|
||||
params: McpResourceReadParams {
|
||||
meta: None,
|
||||
thread_id: Some("00000000-0000-4000-8000-000000000000".to_string()),
|
||||
origin_call_id: None,
|
||||
server: "codex_apps".to_string(),
|
||||
@@ -670,6 +717,7 @@ async fn metadata_and_mcp_requests_complete_while_unrelated_resume_loads_config(
|
||||
sender.request(ClientRequest::McpResourceRead {
|
||||
request_id: RequestId::Integer(7),
|
||||
params: McpResourceReadParams {
|
||||
meta: None,
|
||||
thread_id: Some(thread.id.clone()),
|
||||
origin_call_id: None,
|
||||
server: "resource_server".to_string(),
|
||||
@@ -1403,7 +1451,7 @@ fn expected_resource_read_response() -> McpResourceReadResponse {
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct ResourceAppsMcpCalls {
|
||||
read_metadata: Mutex<Vec<serde_json::Map<String, serde_json::Value>>>,
|
||||
pub(super) read_metadata: Mutex<Vec<serde_json::Map<String, serde_json::Value>>>,
|
||||
list_resources: AtomicUsize,
|
||||
main_prompt_reads: AtomicUsize,
|
||||
reference_reads: AtomicUsize,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//! Public JSON-RPC coverage for caller metadata on a thread's stdio resource read.
|
||||
|
||||
use super::*;
|
||||
use core_test_support::stdio_server_bin;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn stdio_resource_receives_caller_metadata() -> Result<()> {
|
||||
let responses_server = responses::start_mock_server().await;
|
||||
let codex_home = TempDir::new()?;
|
||||
MockResponsesConfig::new(&responses_server.uri())
|
||||
.with_extra_config(&format!(
|
||||
"[mcp_servers.custom]\ncommand = {}",
|
||||
toml::Value::String(stdio_server_bin()?),
|
||||
))
|
||||
.write(codex_home.path())?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let ThreadStartResponse { thread, .. } = mcp
|
||||
.request(|request_id| ClientRequest::ThreadStart {
|
||||
request_id,
|
||||
params: ThreadStartParams::default(),
|
||||
})
|
||||
.await?;
|
||||
let mut params = json!({
|
||||
"threadId": thread.id,
|
||||
"server": "custom",
|
||||
"uri": "test://codex/request-metadata",
|
||||
});
|
||||
let metadata = json!({
|
||||
"com.openai/codex-statusline-state": {
|
||||
"config": {"statusline_items": ["model", "context-remaining"]},
|
||||
"thread": {"id": thread.id, "name": null},
|
||||
},
|
||||
"connector_id": "custom-connector",
|
||||
"link_id": "custom-link",
|
||||
"_codex_apps": {"resource_uri": "custom://route"},
|
||||
});
|
||||
for caller_metadata in [None, Some(&metadata)] {
|
||||
if let Some(meta) = caller_metadata {
|
||||
params["_meta"] = meta.clone();
|
||||
}
|
||||
let request = mcp
|
||||
.send_raw_request("mcpServer/resource/read", Some(params.clone()))
|
||||
.await?;
|
||||
let response: McpResourceReadResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request)).await??;
|
||||
let [McpResourceContent::Text { text, .. }] = response.contents.as_slice() else {
|
||||
anyhow::bail!("expected the metadata echoed by the stdio server");
|
||||
};
|
||||
let received: serde_json::Value = serde_json::from_str(text)?;
|
||||
for key in metadata.as_object().unwrap().keys() {
|
||||
assert_eq!(
|
||||
received.get(key),
|
||||
caller_metadata.and_then(|meta| meta.get(key))
|
||||
);
|
||||
}
|
||||
}
|
||||
for (invalid_meta, expected_message) in [
|
||||
(json!({"threadId": "spoofed"}), "threadId"),
|
||||
(
|
||||
json!({"x-codex-turn-metadata": {}}),
|
||||
"x-codex-turn-metadata",
|
||||
),
|
||||
(
|
||||
json!({"codex/connectionLease": "spoofed"}),
|
||||
"codex/connectionLease",
|
||||
),
|
||||
(json!([]), "map"),
|
||||
] {
|
||||
params["_meta"] = invalid_meta;
|
||||
let request = mcp
|
||||
.send_raw_request("mcpServer/resource/read", Some(params.clone()))
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert!(
|
||||
error.error.message.contains(expected_message),
|
||||
"{}",
|
||||
error.error.message
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -247,6 +247,7 @@ async fn list_plugin_skills(
|
||||
async fn read_mcp_skill_resource(app_server: &mut TestAppServer, thread_id: &str) -> Result<()> {
|
||||
let request = app_server
|
||||
.send_mcp_resource_read_request(McpResourceReadParams {
|
||||
meta: None,
|
||||
thread_id: Some(thread_id.to_string()),
|
||||
origin_call_id: None,
|
||||
server: "codex_apps".to_string(),
|
||||
|
||||
@@ -121,6 +121,16 @@ async fn widget_reads_survive_history_modes_compaction_restarts_and_app_only_vis
|
||||
for (call_id, connector_id) in [("best-buy-call", "best_buy"), ("walmart-call", "walmart")]
|
||||
{
|
||||
let response = read_widget(&mut app_server, &thread.id, call_id).await?;
|
||||
assert_eq!(
|
||||
calls
|
||||
.read_metadata
|
||||
.lock()
|
||||
.unwrap()
|
||||
.last()
|
||||
.unwrap()
|
||||
.get("example/trace"),
|
||||
Some(&json!(call_id))
|
||||
);
|
||||
assert_eq!(
|
||||
response,
|
||||
McpResourceReadResponse {
|
||||
@@ -163,6 +173,7 @@ async fn widget_reads_survive_history_modes_compaction_restarts_and_app_only_vis
|
||||
] {
|
||||
let request_id = app_server
|
||||
.send_mcp_resource_read_request(McpResourceReadParams {
|
||||
meta: None,
|
||||
thread_id,
|
||||
origin_call_id: Some(call_id.to_string()),
|
||||
server: "codex_apps".to_string(),
|
||||
@@ -287,6 +298,10 @@ async fn read_widget(
|
||||
.request(|request_id| ClientRequest::McpResourceRead {
|
||||
request_id,
|
||||
params: McpResourceReadParams {
|
||||
meta: Some(serde_json::Map::from_iter([(
|
||||
"example/trace".into(),
|
||||
json!(call_id),
|
||||
)])),
|
||||
thread_id: Some(thread_id.to_string()),
|
||||
origin_call_id: Some(call_id.to_string()),
|
||||
server: "codex_apps".to_string(),
|
||||
|
||||
@@ -39,7 +39,8 @@ use crate::tool_catalog_cache::McpToolCatalogCacheContext;
|
||||
/// Opt-in resource lifecycle for negotiated stdio connections. Tool calls and resource reads
|
||||
/// carry a host-owned lease ID. Catalog requests remain independent of a particular lease.
|
||||
pub(crate) const LEASE_CAPABILITY: &str = "codex/connection-lease";
|
||||
pub(crate) const LEASE_META_KEY: &str = "codex/connectionLease";
|
||||
/// Request metadata set by the host when a connection lease is negotiated.
|
||||
pub const LEASE_META_KEY: &str = "codex/connectionLease";
|
||||
const LEASE_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -10,6 +10,7 @@ pub use codex_rmcp_client::McpProtocolMode;
|
||||
pub use connection_manager::NetworkPolicyMetadata;
|
||||
pub use connection_manager::NetworkPolicyMetadataProvider;
|
||||
pub use connection_manager::tool_is_model_visible;
|
||||
pub use connection_pool::LEASE_META_KEY as MCP_CONNECTION_LEASE_META_KEY;
|
||||
pub use connection_pool::McpConnectionPool;
|
||||
pub use elicitation::ElicitationLifecycle;
|
||||
pub use elicitation::ElicitationReviewRequest;
|
||||
|
||||
@@ -249,9 +249,9 @@ impl ResourceOrigin {
|
||||
&self,
|
||||
binding: &McpBinding,
|
||||
thread_id: ThreadId,
|
||||
uri: &str,
|
||||
mut params: ReadResourceRequestParams,
|
||||
) -> anyhow::Result<ReadResourceResult> {
|
||||
if self.uri != uri {
|
||||
if self.uri != params.uri {
|
||||
anyhow::bail!("originating MCP tool call does not match the requested resource");
|
||||
}
|
||||
if self.ambiguous_account {
|
||||
@@ -297,20 +297,19 @@ impl ResourceOrigin {
|
||||
anyhow::bail!("originating MCP tool is disabled by app configuration");
|
||||
}
|
||||
|
||||
let meta = serde_json::from_value(serde_json::json!({
|
||||
"threadId": thread_id,
|
||||
"x-codex-turn-metadata": {
|
||||
let meta = params.meta.get_or_insert_default();
|
||||
meta.insert("threadId".into(), serde_json::json!(thread_id));
|
||||
meta.insert(
|
||||
"x-codex-turn-metadata".into(),
|
||||
serde_json::json!({
|
||||
"mcp_request_meta": {
|
||||
"selected_connector_ids": [&self.connector_id],
|
||||
"link_id": &self.link_id,
|
||||
}
|
||||
},
|
||||
}))?;
|
||||
}),
|
||||
);
|
||||
binding
|
||||
.read_resource(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
ReadResourceRequestParams::new(uri).with_meta(meta),
|
||||
)
|
||||
.read_resource(CODEX_APPS_MCP_SERVER_NAME, params)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ impl McpRuntime {
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
call_id: &str,
|
||||
uri: &str,
|
||||
params: ReadResourceRequestParams,
|
||||
) -> anyhow::Result<ReadResourceResult> {
|
||||
let origin = self
|
||||
.resource_origins
|
||||
@@ -341,7 +341,7 @@ impl McpRuntime {
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("codex_apps MCP server is unavailable"))?;
|
||||
|
||||
origin.read(&binding, thread_id, uri).await
|
||||
origin.read(&binding, thread_id, params).await
|
||||
}
|
||||
|
||||
pub async fn new(input: McpRuntimeInput) -> Self {
|
||||
|
||||
@@ -1380,14 +1380,14 @@ impl CodexThread {
|
||||
pub async fn read_mcp_resource_for_call(
|
||||
&self,
|
||||
call_id: &str,
|
||||
uri: &str,
|
||||
params: ReadResourceRequestParams,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
self.session.refresh_mcp_if_dirty().await;
|
||||
let result = self
|
||||
.session
|
||||
.services
|
||||
.mcp_runtime
|
||||
.read_resource_for_call(self.session.thread_id, call_id, uri)
|
||||
.read_resource_for_call(self.session.thread_id, call_id, params)
|
||||
.await?;
|
||||
|
||||
Ok(serde_json::to_value(result)?)
|
||||
|
||||
@@ -697,8 +697,16 @@ impl ServerHandler for TestToolServer {
|
||||
async fn read_resource(
|
||||
&self,
|
||||
ReadResourceRequestParams { uri, .. }: ReadResourceRequestParams,
|
||||
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
|
||||
context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
|
||||
) -> Result<rmcp::model::ReadResourceResponse, McpError> {
|
||||
if uri == "test://codex/request-metadata" {
|
||||
let metadata = serde_json::Value::Object(context.meta.0.0);
|
||||
return Ok(ReadResourceResult::new(vec![ResourceContents::text(
|
||||
metadata.to_string(),
|
||||
uri,
|
||||
)])
|
||||
.into());
|
||||
}
|
||||
if uri == MEMO_URI {
|
||||
Ok(
|
||||
ReadResourceResult::new(vec![ResourceContents::TextResourceContents {
|
||||
|
||||
@@ -9197,6 +9197,13 @@ class McpResourceReadParams(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
populate_by_name=True,
|
||||
)
|
||||
field_meta: Annotated[
|
||||
dict[str, Any] | None,
|
||||
Field(
|
||||
alias="_meta",
|
||||
description="Additional MCP request metadata. Account and turn routing keys are reserved.",
|
||||
),
|
||||
] = None
|
||||
connector_id: Annotated[str | None, Field(alias="connectorId")] = None
|
||||
origin_call_id: Annotated[
|
||||
str | None,
|
||||
|
||||
Reference in New Issue
Block a user