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
Unverified
parent fb8598df3f
commit ec300bc7bd
32 changed files with 516 additions and 4 deletions
+15
View File
@@ -202,6 +202,15 @@ pub struct McpToolCallItem {
pub link_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub app_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub template_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub action_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub plugin_id: Option<String>,
pub status: McpToolCallStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -560,6 +569,9 @@ impl McpToolCallItem {
connector_id: self.connector_id.clone(),
mcp_app_resource_uri: self.mcp_app_resource_uri.clone(),
link_id: self.link_id.clone(),
app_name: self.app_name.clone(),
template_id: self.template_id.clone(),
action_name: self.action_name.clone(),
plugin_id: self.plugin_id.clone(),
})
}
@@ -581,6 +593,9 @@ impl McpToolCallItem {
mcp_app_resource_uri: self.mcp_app_resource_uri.clone(),
connector_id: self.connector_id.clone(),
link_id: self.link_id.clone(),
app_name: self.app_name.clone(),
template_id: self.template_id.clone(),
action_name: self.action_name.clone(),
plugin_id: self.plugin_id.clone(),
duration: self.duration?,
result,
+28
View File
@@ -2357,6 +2357,15 @@ pub struct McpToolCallBeginEvent {
pub link_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub app_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub template_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub action_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub plugin_id: Option<String>,
}
@@ -2376,6 +2385,15 @@ pub struct McpToolCallEndEvent {
pub link_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub app_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub template_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub action_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub plugin_id: Option<String>,
#[ts(type = "string")]
pub duration: Duration,
@@ -5140,6 +5158,9 @@ mod tests {
connector_id: Some("connector".into()),
mcp_app_resource_uri: Some("app://connector".into()),
link_id: Some("link_123".into()),
app_name: Some("Calendar".into()),
template_id: Some("calendar_template".into()),
action_name: Some("create_event".into()),
plugin_id: Some("sample@test".into()),
status: McpToolCallStatus::InProgress,
result: None,
@@ -5161,6 +5182,8 @@ mod tests {
Some("app://connector")
);
assert_eq!(event.link_id.as_deref(), Some("link_123"));
assert_eq!(event.app_name.as_deref(), Some("Calendar"));
assert_eq!(event.action_name.as_deref(), Some("create_event"));
assert_eq!(event.plugin_id.as_deref(), Some("sample@test"));
}
_ => panic!("expected McpToolCallBegin event"),
@@ -5251,6 +5274,9 @@ mod tests {
connector_id: Some("connector".into()),
mcp_app_resource_uri: Some("app://connector".into()),
link_id: Some("link_123".into()),
app_name: Some("Calendar".into()),
template_id: Some("calendar_template".into()),
action_name: Some("create_event".into()),
plugin_id: Some("sample@test".into()),
status: McpToolCallStatus::Completed,
result: Some(CallToolResult {
@@ -5277,6 +5303,8 @@ mod tests {
Some("app://connector")
);
assert_eq!(event.link_id.as_deref(), Some("link_123"));
assert_eq!(event.app_name.as_deref(), Some("Calendar"));
assert_eq!(event.action_name.as_deref(), Some("create_event"));
assert_eq!(event.plugin_id.as_deref(), Some("sample@test"));
assert_eq!(event.duration, Duration::from_millis(42));
assert!(event.is_success());