mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Emit Trusted MCP App Identity on Tool-Call Items (#27132)
## Summary
- Add optional `appContext` to app-server MCP tool-call items with
trusted `connectorId`, `linkId`, and `mcpAppResourceUri` metadata.
- Preserve that context across tool-call events, persisted history,
reconnects, and thread resume.
- Keep the deprecated top-level `mcpAppResourceUri` temporarily for
client migration.
The consumer contract is `{ appContext: { connectorId, linkId,
mcpAppResourceUri }, tool }`.
## Validation
- Full GitHub Actions suite passes, including CLA, Bazel tests, clippy,
release builds, and argument-comment lint.
---------
Co-authored-by: martinauyeung-oai <280153141+martinauyeung-oai@users.noreply.github.com>
This commit is contained in:
co-authored by
martinauyeung-oai
parent
9bcc09f9f7
commit
765309d5a6
@@ -10,6 +10,7 @@ use crate::protocol::v2::CollabAgentToolCallStatus;
|
||||
use crate::protocol::v2::CommandExecutionStatus;
|
||||
use crate::protocol::v2::DynamicToolCallOutputContentItem;
|
||||
use crate::protocol::v2::DynamicToolCallStatus;
|
||||
use crate::protocol::v2::McpToolCallAppContext;
|
||||
use crate::protocol::v2::McpToolCallError;
|
||||
use crate::protocol::v2::McpToolCallResult;
|
||||
use crate::protocol::v2::McpToolCallStatus;
|
||||
@@ -765,6 +766,14 @@ impl ThreadHistoryBuilder {
|
||||
.arguments
|
||||
.clone()
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
app_context: payload
|
||||
.connector_id
|
||||
.clone()
|
||||
.map(|connector_id| McpToolCallAppContext {
|
||||
connector_id,
|
||||
link_id: payload.link_id.clone(),
|
||||
resource_uri: payload.mcp_app_resource_uri.clone(),
|
||||
}),
|
||||
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
|
||||
plugin_id: payload.plugin_id.clone(),
|
||||
result: None,
|
||||
@@ -807,6 +816,14 @@ impl ThreadHistoryBuilder {
|
||||
.arguments
|
||||
.clone()
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
app_context: payload
|
||||
.connector_id
|
||||
.clone()
|
||||
.map(|connector_id| McpToolCallAppContext {
|
||||
connector_id,
|
||||
link_id: payload.link_id.clone(),
|
||||
resource_uri: payload.mcp_app_resource_uri.clone(),
|
||||
}),
|
||||
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
|
||||
plugin_id: payload.plugin_id.clone(),
|
||||
result,
|
||||
@@ -1567,7 +1584,6 @@ mod tests {
|
||||
use codex_protocol::protocol::DynamicToolCallResponseEvent;
|
||||
use codex_protocol::protocol::ExecCommandEndEvent;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_protocol::protocol::ItemCompletedEvent;
|
||||
use codex_protocol::protocol::ItemStartedEvent;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
@@ -2397,7 +2413,9 @@ mod tests {
|
||||
tool: "lookup".into(),
|
||||
arguments: Some(serde_json::json!({"id":"123"})),
|
||||
},
|
||||
connector_id: None,
|
||||
mcp_app_resource_uri: None,
|
||||
link_id: None,
|
||||
plugin_id: None,
|
||||
duration: Duration::from_millis(8),
|
||||
result: Err("boom".into()),
|
||||
@@ -2447,6 +2465,7 @@ mod tests {
|
||||
tool: "lookup".into(),
|
||||
status: McpToolCallStatus::Failed,
|
||||
arguments: serde_json::json!({"id":"123"}),
|
||||
app_context: None,
|
||||
mcp_app_resource_uri: None,
|
||||
plugin_id: None,
|
||||
result: None,
|
||||
@@ -2475,7 +2494,9 @@ mod tests {
|
||||
tool: "lookup".into(),
|
||||
arguments: Some(serde_json::json!({"id":"123"})),
|
||||
},
|
||||
connector_id: Some("calendar".into()),
|
||||
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
|
||||
link_id: Some("link_calendar".into()),
|
||||
plugin_id: Some("sample@test".into()),
|
||||
duration: Duration::from_millis(8),
|
||||
result: Ok(CallToolResult {
|
||||
@@ -2506,6 +2527,11 @@ mod tests {
|
||||
tool: "lookup".into(),
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: serde_json::json!({"id":"123"}),
|
||||
app_context: Some(McpToolCallAppContext {
|
||||
connector_id: "calendar".into(),
|
||||
link_id: Some("link_calendar".into()),
|
||||
resource_uri: Some("ui://widget/lookup.html".into()),
|
||||
}),
|
||||
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
|
||||
plugin_id: Some("sample@test".into()),
|
||||
result: Some(Box::new(McpToolCallResult {
|
||||
|
||||
@@ -290,8 +290,10 @@ pub enum ThreadItem {
|
||||
tool: String,
|
||||
status: McpToolCallStatus,
|
||||
arguments: JsonValue,
|
||||
app_context: Option<McpToolCallAppContext>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
/// Deprecated: use `appContext.resourceUri` instead.
|
||||
mcp_app_resource_uri: Option<String>,
|
||||
plugin_id: Option<String>,
|
||||
result: Option<Box<McpToolCallResult>>,
|
||||
@@ -384,6 +386,15 @@ pub enum ThreadItem {
|
||||
ContextCompaction { id: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub struct McpToolCallAppContext {
|
||||
pub connector_id: String,
|
||||
pub link_id: Option<String>,
|
||||
pub resource_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
@@ -877,6 +888,11 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
tool: mcp.tool,
|
||||
status: McpToolCallStatus::from(mcp.status),
|
||||
arguments: mcp.arguments,
|
||||
app_context: mcp.connector_id.map(|connector_id| McpToolCallAppContext {
|
||||
connector_id,
|
||||
link_id: mcp.link_id,
|
||||
resource_uri: mcp.mcp_app_resource_uri.clone(),
|
||||
}),
|
||||
mcp_app_resource_uri: mcp.mcp_app_resource_uri,
|
||||
plugin_id: mcp.plugin_id,
|
||||
result: mcp.result.map(McpToolCallResult::from).map(Box::new),
|
||||
|
||||
@@ -2615,7 +2615,9 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
server: "server".to_string(),
|
||||
tool: "tool".to_string(),
|
||||
arguments: json!({"arg": "value"}),
|
||||
connector_id: Some("calendar".to_string()),
|
||||
mcp_app_resource_uri: Some("app://connector".to_string()),
|
||||
link_id: Some("link_calendar".to_string()),
|
||||
plugin_id: Some("sample@test".to_string()),
|
||||
status: CoreMcpToolCallStatus::InProgress,
|
||||
result: None,
|
||||
@@ -2631,6 +2633,11 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
tool: "tool".to_string(),
|
||||
status: McpToolCallStatus::InProgress,
|
||||
arguments: json!({"arg": "value"}),
|
||||
app_context: Some(McpToolCallAppContext {
|
||||
connector_id: "calendar".to_string(),
|
||||
link_id: Some("link_calendar".to_string()),
|
||||
resource_uri: Some("app://connector".to_string()),
|
||||
}),
|
||||
mcp_app_resource_uri: Some("app://connector".to_string()),
|
||||
plugin_id: Some("sample@test".to_string()),
|
||||
result: None,
|
||||
@@ -2644,7 +2651,9 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
server: "server".to_string(),
|
||||
tool: "tool".to_string(),
|
||||
arguments: JsonValue::Null,
|
||||
connector_id: None,
|
||||
mcp_app_resource_uri: None,
|
||||
link_id: None,
|
||||
plugin_id: None,
|
||||
status: CoreMcpToolCallStatus::Completed,
|
||||
result: Some(CallToolResult {
|
||||
@@ -2665,6 +2674,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
tool: "tool".to_string(),
|
||||
status: McpToolCallStatus::Completed,
|
||||
arguments: JsonValue::Null,
|
||||
app_context: None,
|
||||
mcp_app_resource_uri: None,
|
||||
plugin_id: None,
|
||||
result: Some(Box::new(McpToolCallResult {
|
||||
@@ -2678,6 +2688,49 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_call_app_context_serializes_connector_id() {
|
||||
let item = ThreadItem::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
server: "codex_apps".to_string(),
|
||||
tool: "calendar.create_event".to_string(),
|
||||
status: McpToolCallStatus::InProgress,
|
||||
arguments: json!({}),
|
||||
app_context: Some(McpToolCallAppContext {
|
||||
connector_id: "calendar".to_string(),
|
||||
link_id: Some("link_calendar".to_string()),
|
||||
resource_uri: Some("app://connector".to_string()),
|
||||
}),
|
||||
mcp_app_resource_uri: Some("app://connector".to_string()),
|
||||
plugin_id: None,
|
||||
result: None,
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(item).expect("MCP tool call should serialize"),
|
||||
json!({
|
||||
"type": "mcpToolCall",
|
||||
"id": "mcp-1",
|
||||
"server": "codex_apps",
|
||||
"tool": "calendar.create_event",
|
||||
"status": "inProgress",
|
||||
"arguments": {},
|
||||
"appContext": {
|
||||
"connectorId": "calendar",
|
||||
"linkId": "link_calendar",
|
||||
"resourceUri": "app://connector",
|
||||
},
|
||||
"mcpAppResourceUri": "app://connector",
|
||||
"pluginId": null,
|
||||
"result": null,
|
||||
"error": null,
|
||||
"durationMs": null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_input_into_core_preserves_image_detail() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user