Expose MCP read-only hints in tool call items (#36055)

## What changed

- Propagate MCP tool `readOnlyHint` annotations through tool-call start and
  completion events.
- Include the optional hint in persisted thread history and app-server
  `mcpToolCall` items, preserving compatibility when the annotation is absent.
- Document that the hint describes tool capability, not the outcome of a
  particular invocation.

## Testing

- Cover read-only and write-capable tools in live events and persisted rollout
  data.
- Verify the hint survives thread reads and resumes for both in-progress and
  completed MCP calls.

GitOrigin-RevId: dddfe905146075e5137a1094da485b86b99807f0
This commit is contained in:
bear-oai
2026-07-30 01:37:41 +00:00
committed by copyberry
parent 1c5f336c40
commit 406dc92394
38 changed files with 406 additions and 2 deletions
@@ -787,6 +787,7 @@ impl ThreadHistoryBuilder {
}),
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
plugin_id: payload.plugin_id.clone(),
read_only_hint: payload.read_only_hint,
result: None,
error: None,
duration_ms: None,
@@ -839,6 +840,7 @@ impl ThreadHistoryBuilder {
}),
mcp_app_resource_uri: payload.mcp_app_resource_uri.clone(),
plugin_id: payload.plugin_id.clone(),
read_only_hint: payload.read_only_hint,
result,
error,
duration_ms,
@@ -2782,6 +2784,7 @@ mod tests {
app_name: None,
action_name: None,
plugin_id: None,
read_only_hint: None,
duration: Duration::from_millis(8),
result: Err("boom".into()),
}),
@@ -2840,6 +2843,7 @@ mod tests {
app_context: None,
mcp_app_resource_uri: None,
plugin_id: None,
read_only_hint: None,
result: None,
error: Some(McpToolCallError {
message: "boom".into(),
@@ -2872,6 +2876,7 @@ mod tests {
app_name: Some("Calendar".into()),
action_name: Some("lookup".into()),
plugin_id: Some("sample@test".into()),
read_only_hint: Some(false),
duration: Duration::from_millis(8),
result: Ok(CallToolResult {
content: vec![serde_json::json!({
@@ -2910,6 +2915,7 @@ mod tests {
}),
mcp_app_resource_uri: Some("ui://widget/lookup.html".into()),
plugin_id: Some("sample@test".into()),
read_only_hint: Some(false),
result: Some(Box::new(McpToolCallResult {
content: vec![serde_json::json!({
"type": "text",
@@ -317,6 +317,7 @@ pub enum ThreadItem {
/// Deprecated: use `appContext.resourceUri` instead.
mcp_app_resource_uri: Option<String>,
plugin_id: Option<String>,
read_only_hint: Option<bool>,
result: Option<Box<McpToolCallResult>>,
error: Option<McpToolCallError>,
/// The duration of the MCP tool call in milliseconds.
@@ -961,6 +962,7 @@ impl From<CoreTurnItem> for ThreadItem {
}),
mcp_app_resource_uri: mcp.mcp_app_resource_uri,
plugin_id: mcp.plugin_id,
read_only_hint: mcp.read_only_hint,
result: mcp.result.map(McpToolCallResult::from).map(Box::new),
error: mcp.error.map(McpToolCallError::from),
duration_ms,
@@ -3144,6 +3144,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
app_name: Some("Calendar".to_string()),
action_name: Some("create_event".to_string()),
plugin_id: Some("sample@test".to_string()),
read_only_hint: Some(true),
status: CoreMcpToolCallStatus::InProgress,
result: None,
error: None,
@@ -3167,6 +3168,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
}),
mcp_app_resource_uri: Some("app://connector".to_string()),
plugin_id: Some("sample@test".to_string()),
read_only_hint: Some(true),
result: None,
error: None,
duration_ms: None,
@@ -3184,6 +3186,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
app_name: None,
action_name: None,
plugin_id: None,
read_only_hint: Some(false),
status: CoreMcpToolCallStatus::Completed,
result: Some(CallToolResult {
content: vec![json!({"type": "text", "text": "ok"})],
@@ -3206,6 +3209,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
app_context: None,
mcp_app_resource_uri: None,
plugin_id: None,
read_only_hint: Some(false),
result: Some(Box::new(McpToolCallResult {
content: vec![json!({"type": "text", "text": "ok"})],
structured_content: Some(json!({"ok": true})),
@@ -3234,6 +3238,7 @@ fn mcp_tool_call_app_context_serializes_connector_id() {
}),
mcp_app_resource_uri: Some("app://connector".to_string()),
plugin_id: None,
read_only_hint: Some(false),
result: None,
error: None,
duration_ms: None,
@@ -3257,6 +3262,7 @@ fn mcp_tool_call_app_context_serializes_connector_id() {
},
"mcpAppResourceUri": "app://connector",
"pluginId": null,
"readOnlyHint": false,
"result": null,
"error": null,
"durationMs": null,