Expose MCP app identity in app context (#29934)

## Why

MCP tool-call events need to expose trusted app identity and action
metadata directly so v2 clients do not have to infer it from tool names
or resource URIs.

## What changed

- Add optional `appName`, `templateId`, and `actionName` fields to MCP
tool-call `appContext`.
- Populate `appName` and `templateId` from trusted Codex Apps metadata,
and derive `actionName` from the trusted app resource metadata.
- Preserve all three fields through core events, legacy protocol events,
persisted thread history, resume redaction, and app-server v2 responses.
- Document the public `appContext` fields in
`codex-rs/app-server/README.md`.
- Regenerate app-server JSON and TypeScript schemas and add coverage for
serialization, persistence, redaction, and metadata propagation.

## Validation

- `just test -p codex-app-server-protocol mcp_tool_call`
- `just test -p codex-core
mcp_tool_call_item_metadata_only_trusts_codex_apps_identity
mcp_tool_call_item_includes_app_identity`
- `just write-app-server-schema`

---------

Co-authored-by: Martin Au-Yeung <280153141+martinauyeung-oai@users.noreply.github.com>
This commit is contained in:
Martin Au-Yeung
2026-06-25 18:31:10 -07:00
committed by GitHub
co-authored by Martin Au-Yeung
parent fb8598df3f
commit ec300bc7bd
32 changed files with 516 additions and 4 deletions
+26
View File
@@ -326,6 +326,9 @@ struct McpToolCallItemMetadata {
connector_id: Option<String>,
link_id: Option<String>,
mcp_app_resource_uri: Option<String>,
app_name: Option<String>,
template_id: Option<String>,
action_name: Option<String>,
plugin_id: Option<String>,
}
@@ -342,6 +345,15 @@ impl McpToolCallItemMetadata {
link_id: trusted_mcp_app_metadata.and_then(|metadata| metadata.link_id.clone()),
mcp_app_resource_uri: metadata
.and_then(|metadata| metadata.mcp_app_resource_uri.clone()),
app_name: trusted_mcp_app_metadata.and_then(|metadata| metadata.connector_name.clone()),
template_id: trusted_mcp_app_metadata.and_then(|metadata| metadata.template_id.clone()),
action_name: trusted_mcp_app_metadata
.and_then(|metadata| metadata.codex_apps_meta.as_ref())
.and_then(|meta| meta.get(MCP_TOOL_RESOURCE_URI_META_KEY))
.and_then(serde_json::Value::as_str)
.and_then(|resource_uri| resource_uri.trim_matches('/').rsplit('/').next())
.filter(|action_name| !action_name.is_empty())
.map(str::to_string),
plugin_id: metadata.and_then(|metadata| metadata.plugin_id.clone()),
}
}
@@ -881,6 +893,9 @@ async fn notify_mcp_tool_call_started(
connector_id: item_metadata.connector_id,
mcp_app_resource_uri: item_metadata.mcp_app_resource_uri,
link_id: item_metadata.link_id,
app_name: item_metadata.app_name,
template_id: item_metadata.template_id,
action_name: item_metadata.action_name,
plugin_id: item_metadata.plugin_id,
status: McpToolCallStatus::InProgress,
result: None,
@@ -923,6 +938,9 @@ async fn notify_mcp_tool_call_completed(
connector_id: item_metadata.connector_id,
mcp_app_resource_uri: item_metadata.mcp_app_resource_uri,
link_id: item_metadata.link_id,
app_name: item_metadata.app_name,
template_id: item_metadata.template_id,
action_name: item_metadata.action_name,
plugin_id: item_metadata.plugin_id,
status,
result,
@@ -999,6 +1017,7 @@ pub(crate) struct McpToolApprovalMetadata {
tool_title: Option<String>,
tool_description: Option<String>,
mcp_app_resource_uri: Option<String>,
template_id: Option<String>,
codex_apps_meta: Option<serde_json::Map<String, serde_json::Value>>,
openai_file_input_params: Option<Vec<String>>,
}
@@ -1009,6 +1028,8 @@ const MCP_TOOL_LINK_ID_META_KEY: &str = "link_id";
const MCP_TOOL_PLUGIN_ID_META_KEY: &str = "plugin_id";
const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId";
const MCP_TOOL_CONNECTED_ACCOUNT_EMAIL_META_KEY: &str = "connected_account_email";
const MCP_TOOL_TEMPLATE_ID_META_KEY: &str = "template_id";
const MCP_TOOL_RESOURCE_URI_META_KEY: &str = "resource_uri";
async fn custom_mcp_tool_approval_mode(
sess: &Session,
@@ -1531,6 +1552,11 @@ pub(crate) async fn lookup_mcp_tool_metadata(
tool_title: tool_info.tool.title,
tool_description: tool_info.tool.description.map(std::borrow::Cow::into_owned),
mcp_app_resource_uri: get_mcp_app_resource_uri(tool_info.tool.meta.as_deref()),
template_id: codex_apps_meta
.as_ref()
.and_then(|meta| meta.get(MCP_TOOL_TEMPLATE_ID_META_KEY))
.and_then(serde_json::Value::as_str)
.map(str::to_string),
codex_apps_meta,
// Disallow custom MCPs from uploading files via fileParams.
openai_file_input_params: openai_file_input_params_for_server(
+34 -2
View File
@@ -87,6 +87,7 @@ fn approval_metadata(
tool_title: tool_title.map(str::to_string),
tool_description: tool_description.map(str::to_string),
mcp_app_resource_uri: None,
template_id: None,
codex_apps_meta: None,
openai_file_input_params: None,
}
@@ -1195,12 +1196,22 @@ async fn plugin_mcp_tool_call_request_meta_includes_plugin_id() {
fn mcp_tool_call_item_metadata_only_trusts_codex_apps_identity() {
let mut metadata = approval_metadata(
Some("asdk_app_0123456789abcdef0123456789abcdef"),
/*connector_name*/ None,
Some("Calendar"),
/*connector_description*/ None,
/*tool_title*/ None,
Some("Create a calendar event"),
/*tool_description*/ None,
);
metadata.link_id = Some("link_fedcba9876543210fedcba9876543210".to_string());
metadata.template_id = Some("calendar_template".to_string());
metadata.codex_apps_meta = Some(
serde_json::json!({
"resource_uri": "/asdk_app_0123456789abcdef0123456789abcdef/link_fedcba9876543210fedcba9876543210/create_event",
"template_id": "calendar_template",
})
.as_object()
.cloned()
.expect("_codex_apps metadata should be an object"),
);
assert_eq!(
McpToolCallItemMetadata::from_tool_metadata(CODEX_APPS_MCP_SERVER_NAME, Some(&metadata),),
@@ -1208,6 +1219,9 @@ fn mcp_tool_call_item_metadata_only_trusts_codex_apps_identity() {
connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()),
link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()),
mcp_app_resource_uri: None,
app_name: Some("Calendar".to_string()),
template_id: Some("calendar_template".to_string()),
action_name: Some("create_event".to_string()),
plugin_id: None,
}
);
@@ -1217,6 +1231,9 @@ fn mcp_tool_call_item_metadata_only_trusts_codex_apps_identity() {
connector_id: None,
link_id: None,
mcp_app_resource_uri: None,
app_name: None,
template_id: None,
action_name: None,
plugin_id: None,
}
);
@@ -1239,6 +1256,9 @@ async fn mcp_tool_call_item_includes_app_identity() {
connector_id: Some("asdk_app_0123456789abcdef0123456789abcdef".to_string()),
link_id: Some("link_fedcba9876543210fedcba9876543210".to_string()),
mcp_app_resource_uri: None,
app_name: Some("Calendar".to_string()),
template_id: Some("calendar_template".to_string()),
action_name: Some("create_event".to_string()),
plugin_id: Some("sample@test".to_string()),
},
)
@@ -1264,6 +1284,8 @@ async fn mcp_tool_call_item_includes_app_identity() {
Some("link_fedcba9876543210fedcba9876543210")
);
assert_eq!(item.plugin_id.as_deref(), Some("sample@test"));
assert_eq!(item.app_name.as_deref(), Some("Calendar"));
assert_eq!(item.action_name.as_deref(), Some("create_event"));
}
#[tokio::test]
@@ -1284,6 +1306,7 @@ async fn codex_apps_tool_call_request_meta_includes_turn_metadata_and_codex_apps
tool_title: Some("Create Event".to_string()),
tool_description: Some("Create a calendar event.".to_string()),
mcp_app_resource_uri: None,
template_id: None,
codex_apps_meta: Some(
serde_json::json!({
"resource_uri": "connector://calendar/tools/calendar_create_event",
@@ -1771,6 +1794,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() {
tool_title: None,
tool_description: None,
mcp_app_resource_uri: None,
template_id: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -2473,6 +2497,7 @@ async fn approve_mode_skips_when_annotations_do_not_require_approval() {
tool_title: Some("Read Only Tool".to_string()),
tool_description: None,
mcp_app_resource_uri: None,
template_id: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -2549,6 +2574,7 @@ async fn guardian_mode_skips_auto_when_annotations_do_not_require_approval() {
tool_title: Some("Read Only Tool".to_string()),
tool_description: None,
mcp_app_resource_uri: None,
template_id: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -2608,6 +2634,7 @@ async fn permission_request_hook_allows_mcp_tool_call() {
tool_title: Some("Create entities".to_string()),
tool_description: None,
mcp_app_resource_uri: None,
template_id: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -2746,6 +2773,7 @@ async fn permission_request_hook_runs_after_remembered_mcp_approval() {
tool_title: Some("Create entities".to_string()),
tool_description: None,
mcp_app_resource_uri: None,
template_id: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -2835,6 +2863,7 @@ async fn guardian_mode_mcp_denial_returns_rationale_message() {
tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Reads calendar data.".to_string()),
mcp_app_resource_uri: None,
template_id: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -2891,6 +2920,7 @@ async fn prompt_mode_waits_for_approval_when_annotations_do_not_require_approval
tool_title: Some("Read Only Tool".to_string()),
tool_description: None,
mcp_app_resource_uri: None,
template_id: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -2948,6 +2978,7 @@ async fn full_access_mode_skips_mcp_tool_approval_for_all_approval_modes() {
tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Performs a risky action.".to_string()),
mcp_app_resource_uri: None,
template_id: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -3003,6 +3034,7 @@ async fn approve_mode_skips_guardian_in_every_permission_mode() {
tool_title: Some("Dangerous Tool".to_string()),
tool_description: Some("Performs a risky action.".to_string()),
mcp_app_resource_uri: None,
template_id: None,
codex_apps_meta: None,
openai_file_input_params: None,
};
@@ -224,6 +224,9 @@ async fn emit_tool_call_begin(
connector_id: None,
mcp_app_resource_uri: None,
link_id: None,
app_name: None,
template_id: None,
action_name: None,
plugin_id: None,
status: McpToolCallStatus::InProgress,
result: None,
@@ -265,6 +268,9 @@ async fn emit_tool_call_end(
connector_id: None,
mcp_app_resource_uri: None,
link_id: None,
app_name: None,
template_id: None,
action_name: None,
plugin_id: None,
status,
result,
+3
View File
@@ -1359,6 +1359,9 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
connector_id: None,
mcp_app_resource_uri: None,
link_id: None,
app_name: None,
template_id: None,
action_name: None,
plugin_id: None,
},
);
+4
View File
@@ -545,6 +545,8 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
unreachable!("event guard guarantees McpToolCallBegin");
};
assert_eq!(begin.call_id, "calendar-call-1");
assert_eq!(begin.app_name.as_deref(), Some("Calendar"));
assert_eq!(begin.action_name.as_deref(), Some("calendar_create_event"));
assert_eq!(
begin.mcp_app_resource_uri.as_deref(),
Some(CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI)
@@ -559,6 +561,8 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() -
};
assert_eq!(end.call_id, "calendar-call-1");
assert_eq!(end.connector_id.as_deref(), Some("calendar"));
assert_eq!(end.app_name.as_deref(), Some("Calendar"));
assert_eq!(end.action_name.as_deref(), Some("calendar_create_event"));
assert_eq!(
end.mcp_app_resource_uri.as_deref(),
Some(CALENDAR_CREATE_EVENT_MCP_APP_RESOURCE_URI)