[elicitation] User-friendly tool call messages. (#14403)

- [x] Add a curated set of tool call messages and human-readable tool
param names.
This commit is contained in:
Matthew Zeng 2026-03-12 00:35:21 -07:00 committed by GitHub
parent 19d0949aab
commit 23e55d7668
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1782 additions and 81 deletions

View file

@ -0,0 +1,962 @@
{
"schema_version": 4,
"templates": [
{
"source_tool_index": 0,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "add_comment_to_issue",
"template_params": [
{
"name": "pr_number",
"label": "Pull request"
},
{
"name": "repo_full_name",
"label": "Repository"
},
{
"name": "comment",
"label": "Comment"
}
],
"template": "Allow {connector_name} to add a comment to a pull request?"
},
{
"source_tool_index": 1,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "add_reaction_to_issue_comment",
"template_params": [
{
"name": "reaction",
"label": "Reaction"
},
{
"name": "comment_id",
"label": "Comment"
},
{
"name": "repo_full_name",
"label": "Repository"
}
],
"template": "Allow {connector_name} to add a reaction to an issue comment?"
},
{
"source_tool_index": 2,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "add_reaction_to_pr",
"template_params": [
{
"name": "reaction",
"label": "Reaction"
},
{
"name": "pr_number",
"label": "Pull request"
},
{
"name": "repo_full_name",
"label": "Repository"
}
],
"template": "Allow {connector_name} to add a reaction to a pull request?"
},
{
"source_tool_index": 3,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "add_reaction_to_pr_review_comment",
"template_params": [
{
"name": "reaction",
"label": "Reaction"
},
{
"name": "comment_id",
"label": "Comment"
},
{
"name": "repo_full_name",
"label": "Repository"
}
],
"template": "Allow {connector_name} to add a reaction to a pull request review comment?"
},
{
"source_tool_index": 4,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "add_review_to_pr",
"template_params": [
{
"name": "action",
"label": "Action"
},
{
"name": "pr_number",
"label": "Pull request"
},
{
"name": "review",
"label": "Review"
}
],
"template": "Allow {connector_name} to submit a pull request review?"
},
{
"source_tool_index": 5,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "create_blob",
"template_params": [
{
"name": "repository_full_name",
"label": "Repository"
},
{
"name": "content",
"label": "Content"
}
],
"template": "Allow {connector_name} to create a Git blob?"
},
{
"source_tool_index": 6,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "create_branch",
"template_params": [
{
"name": "branch_name",
"label": "Branch"
},
{
"name": "repository_full_name",
"label": "Repository"
}
],
"template": "Allow {connector_name} to create a branch?"
},
{
"source_tool_index": 7,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "create_commit",
"template_params": [
{
"name": "repository_full_name",
"label": "Repository"
},
{
"name": "message",
"label": "Message"
}
],
"template": "Allow {connector_name} to create a commit?"
},
{
"source_tool_index": 8,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "create_pull_request",
"template_params": [
{
"name": "title",
"label": "Title"
},
{
"name": "head_branch",
"label": "Head branch"
},
{
"name": "base_branch",
"label": "Base branch"
}
],
"template": "Allow {connector_name} to create a pull request?"
},
{
"source_tool_index": 9,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "create_tree",
"template_params": [
{
"name": "repository_full_name",
"label": "Repository"
},
{
"name": "tree_elements",
"label": "Changes"
}
],
"template": "Allow {connector_name} to create a Git tree?"
},
{
"source_tool_index": 10,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "enable_auto_merge",
"template_params": [
{
"name": "pr_number",
"label": "Pull request"
},
{
"name": "repository_full_name",
"label": "Repository"
}
],
"template": "Allow {connector_name} to enable pull request auto-merge?"
},
{
"source_tool_index": 11,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "label_pr",
"template_params": [
{
"name": "label",
"label": "Label"
},
{
"name": "pr_number",
"label": "Pull request"
},
{
"name": "repository_full_name",
"label": "Repository"
}
],
"template": "Allow {connector_name} to add a label to a pull request?"
},
{
"source_tool_index": 12,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "remove_reaction_from_issue_comment",
"template_params": [
{
"name": "reaction_id",
"label": "Reaction"
},
{
"name": "comment_id",
"label": "Comment"
},
{
"name": "repo_full_name",
"label": "Repository"
}
],
"template": "Allow {connector_name} to remove a reaction from an issue comment?"
},
{
"source_tool_index": 13,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "remove_reaction_from_pr",
"template_params": [
{
"name": "reaction_id",
"label": "Reaction"
},
{
"name": "pr_number",
"label": "Pull request"
},
{
"name": "repo_full_name",
"label": "Repository"
}
],
"template": "Allow {connector_name} to remove a reaction from a pull request?"
},
{
"source_tool_index": 14,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "remove_reaction_from_pr_review_comment",
"template_params": [
{
"name": "reaction_id",
"label": "Reaction"
},
{
"name": "comment_id",
"label": "Comment"
},
{
"name": "repo_full_name",
"label": "Repository"
}
],
"template": "Allow {connector_name} to remove a reaction from a pull request review comment?"
},
{
"source_tool_index": 15,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "reply_to_review_comment",
"template_params": [
{
"name": "pr_number",
"label": "Pull request"
},
{
"name": "repo_full_name",
"label": "Repository"
},
{
"name": "comment",
"label": "Comment"
}
],
"template": "Allow {connector_name} to reply to a pull request review comment?"
},
{
"source_tool_index": 16,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "update_issue_comment",
"template_params": [
{
"name": "comment_id",
"label": "Comment ID"
},
{
"name": "repo_full_name",
"label": "Repository"
},
{
"name": "comment",
"label": "Comment"
}
],
"template": "Allow {connector_name} to update an issue comment?"
},
{
"source_tool_index": 17,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "update_ref",
"template_params": [
{
"name": "branch_name",
"label": "Branch"
},
{
"name": "repository_full_name",
"label": "Repository"
},
{
"name": "sha",
"label": "Commit"
}
],
"template": "Allow {connector_name} to update a branch reference?"
},
{
"source_tool_index": 18,
"connector_id": "connector_76869538009648d5b282a4bb21c3d157",
"server_name": "codex_apps",
"tool_title": "update_review_comment",
"template_params": [
{
"name": "comment_id",
"label": "Comment ID"
},
{
"name": "repo_full_name",
"label": "Repository"
},
{
"name": "comment",
"label": "Comment"
}
],
"template": "Allow {connector_name} to update a pull request review comment?"
},
{
"source_tool_index": 19,
"connector_id": "connector_947e0d954944416db111db556030eea6",
"server_name": "codex_apps",
"tool_title": "create_event",
"template_params": [
{
"name": "title",
"label": "Title"
},
{
"name": "start_time",
"label": "Start"
},
{
"name": "attendees",
"label": "Attendees"
}
],
"template": "Allow {connector_name} to create an event?"
},
{
"source_tool_index": 20,
"connector_id": "connector_947e0d954944416db111db556030eea6",
"server_name": "codex_apps",
"tool_title": "delete_event",
"template_params": [
{
"name": "event_id",
"label": "Event"
}
],
"template": "Allow {connector_name} to delete an event?"
},
{
"source_tool_index": 21,
"connector_id": "connector_947e0d954944416db111db556030eea6",
"server_name": "codex_apps",
"tool_title": "respond_event",
"template_params": [
{
"name": "response_status",
"label": "Response Status"
},
{
"name": "event_id",
"label": "Event"
}
],
"template": "Allow {connector_name} to respond to an event?"
},
{
"source_tool_index": 22,
"connector_id": "connector_947e0d954944416db111db556030eea6",
"server_name": "codex_apps",
"tool_title": "update_event",
"template_params": [
{
"name": "event_id",
"label": "Event"
}
],
"template": "Allow {connector_name} to update an event?"
},
{
"source_tool_index": 23,
"connector_id": "connector_9d7cfa34e6654a5f98d3387af34b2e1c",
"server_name": "codex_apps",
"tool_title": "batch_update",
"template_params": [
{
"name": "spreadsheet_url",
"label": "Spreadsheet"
},
{
"name": "requests",
"label": "Changes"
}
],
"template": "Allow {connector_name} to apply spreadsheet updates?"
},
{
"source_tool_index": 24,
"connector_id": "connector_9d7cfa34e6654a5f98d3387af34b2e1c",
"server_name": "codex_apps",
"tool_title": "create_spreadsheet",
"template_params": [
{
"name": "title",
"label": "Title"
}
],
"template": "Allow {connector_name} to create a spreadsheet?"
},
{
"source_tool_index": 25,
"connector_id": "connector_9d7cfa34e6654a5f98d3387af34b2e1c",
"server_name": "codex_apps",
"tool_title": "duplicate_sheet_in_new_file",
"template_params": [
{
"name": "source_sheet_name",
"label": "Source Sheet Name"
},
{
"name": "spreadsheet_url",
"label": "Spreadsheet"
},
{
"name": "new_file_name",
"label": "New File Name"
}
],
"template": "Allow {connector_name} to copy a sheet into a new spreadsheet?"
},
{
"source_tool_index": 26,
"connector_id": "connector_6f1ec045b8fa4ced8738e32c7f74514b",
"server_name": "codex_apps",
"tool_title": "batch_update",
"template_params": [
{
"name": "presentation_url",
"label": "Presentation"
},
{
"name": "requests",
"label": "Changes"
}
],
"template": "Allow {connector_name} to apply presentation updates?"
},
{
"source_tool_index": 27,
"connector_id": "connector_6f1ec045b8fa4ced8738e32c7f74514b",
"server_name": "codex_apps",
"tool_title": "create_presentation",
"template_params": [
{
"name": "title",
"label": "Title"
}
],
"template": "Allow {connector_name} to create a presentation?"
},
{
"source_tool_index": 28,
"connector_id": "connector_4964e3b22e3e427e9b4ae1acf2c1fa34",
"server_name": "codex_apps",
"tool_title": "batch_update",
"template_params": [
{
"name": "document_url",
"label": "Document"
},
{
"name": "requests",
"label": "Changes"
}
],
"template": "Allow {connector_name} to apply document updates?"
},
{
"source_tool_index": 29,
"connector_id": "connector_4964e3b22e3e427e9b4ae1acf2c1fa34",
"server_name": "codex_apps",
"tool_title": "create_document",
"template_params": [
{
"name": "title",
"label": "Title"
}
],
"template": "Allow {connector_name} to create a document?"
},
{
"source_tool_index": 30,
"connector_id": "connector_5f3c8c41a1e54ad7a76272c89e2554fa",
"server_name": "codex_apps",
"tool_title": "copy_document",
"template_params": [
{
"name": "url",
"label": "URL"
}
],
"template": "Allow {connector_name} to copy a file?"
},
{
"source_tool_index": 31,
"connector_id": "connector_5f3c8c41a1e54ad7a76272c89e2554fa",
"server_name": "codex_apps",
"tool_title": "share_document",
"template_params": [
{
"name": "url",
"label": "URL"
},
{
"name": "permission",
"label": "Permission"
}
],
"template": "Allow {connector_name} to change file sharing?"
},
{
"source_tool_index": 32,
"connector_id": "asdk_app_69a1d78e929881919bba0dbda1f6436d",
"server_name": "codex_apps",
"tool_title": "slack_send_message",
"template_params": [
{
"name": "channel_id",
"label": "Conversation"
},
{
"name": "message",
"label": "Message"
}
],
"template": "Allow {connector_name} to send a message?"
},
{
"source_tool_index": 33,
"connector_id": "asdk_app_69a1d78e929881919bba0dbda1f6436d",
"server_name": "codex_apps",
"tool_title": "slack_schedule_message",
"template_params": [
{
"name": "channel_id",
"label": "Conversation"
},
{
"name": "post_at",
"label": "Send at"
},
{
"name": "message",
"label": "Message"
}
],
"template": "Allow {connector_name} to schedule a message?"
},
{
"source_tool_index": 34,
"connector_id": "asdk_app_69a1d78e929881919bba0dbda1f6436d",
"server_name": "codex_apps",
"tool_title": "slack_create_canvas",
"template_params": [
{
"name": "title",
"label": "Title"
},
{
"name": "content",
"label": "Content"
}
],
"template": "Allow {connector_name} to create a canvas?"
},
{
"source_tool_index": 35,
"connector_id": "asdk_app_69a1d78e929881919bba0dbda1f6436d",
"server_name": "codex_apps",
"tool_title": "slack_send_message_draft",
"template_params": [
{
"name": "channel_id",
"label": "Conversation"
},
{
"name": "message",
"label": "Message"
}
],
"template": "Allow {connector_name} to create a message draft?"
},
{
"source_tool_index": 36,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "add_comment_to_issue",
"template_params": [
{
"name": "issue_id",
"label": "Issue"
},
{
"name": "body",
"label": "Body"
}
],
"template": "Allow {connector_name} to add a comment to an issue?"
},
{
"source_tool_index": 37,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "add_label_to_issue",
"template_params": [
{
"name": "label_id",
"label": "Label"
},
{
"name": "issue_id",
"label": "Issue"
}
],
"template": "Allow {connector_name} to add a label to an issue?"
},
{
"source_tool_index": 38,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "add_url_attachment_to_issue",
"template_params": [
{
"name": "url",
"label": "URL"
},
{
"name": "issue_id",
"label": "Issue"
},
{
"name": "title",
"label": "Title"
}
],
"template": "Allow {connector_name} to attach a link to an issue?"
},
{
"source_tool_index": 39,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "assign_issue",
"template_params": [
{
"name": "issue_id",
"label": "Issue"
},
{
"name": "user_id",
"label": "User"
}
],
"template": "Allow {connector_name} to assign an issue?"
},
{
"source_tool_index": 40,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "create_issue",
"template_params": [
{
"name": "title",
"label": "Title"
},
{
"name": "team_id",
"label": "Team"
}
],
"template": "Allow {connector_name} to create an issue?"
},
{
"source_tool_index": 41,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "create_label",
"template_params": [
{
"name": "label_name",
"label": "Label Name"
}
],
"template": "Allow {connector_name} to create a label?"
},
{
"source_tool_index": 42,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "create_project",
"template_params": [
{
"name": "name",
"label": "Name"
},
{
"name": "team_id",
"label": "Team"
}
],
"template": "Allow {connector_name} to create a project?"
},
{
"source_tool_index": 43,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "remove_label_from_issue",
"template_params": [
{
"name": "label_id",
"label": "Label"
},
{
"name": "issue_id",
"label": "Issue"
}
],
"template": "Allow {connector_name} to remove a label from an issue?"
},
{
"source_tool_index": 44,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "resolve_comment",
"template_params": [
{
"name": "comment_id",
"label": "Comment"
}
],
"template": "Allow {connector_name} to resolve a comment?"
},
{
"source_tool_index": 45,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "set_issue_state",
"template_params": [
{
"name": "issue_id",
"label": "Issue"
},
{
"name": "state_id",
"label": "State"
}
],
"template": "Allow {connector_name} to change issue state?"
},
{
"source_tool_index": 46,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "unassign_issue",
"template_params": [
{
"name": "issue_id",
"label": "Issue"
}
],
"template": "Allow {connector_name} to unassign an issue?"
},
{
"source_tool_index": 47,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "update_issue",
"template_params": [
{
"name": "issue_id",
"label": "Issue"
},
{
"name": "issue_update",
"label": "Changes"
}
],
"template": "Allow {connector_name} to update an issue?"
},
{
"source_tool_index": 48,
"connector_id": "connector_686fad9b54914a35b75be6d06a0f6f31",
"server_name": "codex_apps",
"tool_title": "update_project",
"template_params": [
{
"name": "project_id",
"label": "Project"
},
{
"name": "update_fields",
"label": "Changes"
}
],
"template": "Allow {connector_name} to update a project?"
},
{
"source_tool_index": 49,
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
"server_name": "codex_apps",
"tool_title": "apply_labels_to_emails",
"template_params": [],
"template": "Allow {connector_name} to apply label changes to messages?"
},
{
"source_tool_index": 50,
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
"server_name": "codex_apps",
"tool_title": "batch_modify_email",
"template_params": [],
"template": "Allow {connector_name} to update message labels?"
},
{
"source_tool_index": 51,
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
"server_name": "codex_apps",
"tool_title": "bulk_label_matching_emails",
"template_params": [
{
"name": "label_name",
"label": "Label Name"
},
{
"name": "query",
"label": "Query"
}
],
"template": "Allow {connector_name} to label matching messages?"
},
{
"source_tool_index": 52,
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
"server_name": "codex_apps",
"tool_title": "create_draft",
"template_params": [
{
"name": "to",
"label": "To"
},
{
"name": "subject",
"label": "Subject"
},
{
"name": "body",
"label": "Body"
}
],
"template": "Allow {connector_name} to create an email draft?"
},
{
"source_tool_index": 53,
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
"server_name": "codex_apps",
"tool_title": "create_label",
"template_params": [
{
"name": "name",
"label": "Name"
}
],
"template": "Allow {connector_name} to create a label?"
},
{
"source_tool_index": 54,
"connector_id": "connector_2128aebfecb84f64a069897515042a44",
"server_name": "codex_apps",
"tool_title": "send_email",
"template_params": [
{
"name": "to",
"label": "To"
},
{
"name": "subject",
"label": "Subject"
},
{
"name": "body",
"label": "Body"
}
],
"template": "Allow {connector_name} to send an email?"
}
]
}

View file

@ -47,6 +47,7 @@ pub mod instructions;
pub mod landlock;
pub mod mcp;
mod mcp_connection_manager;
mod mcp_tool_approval_templates;
pub mod models_manager;
mod network_policy_decision;
pub mod network_proxy_loader;

View file

@ -0,0 +1,365 @@
use std::collections::HashSet;
use std::sync::LazyLock;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Map;
use serde_json::Value;
use tracing::warn;
const CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION: u8 = 4;
const CONNECTOR_NAME_TEMPLATE_VAR: &str = "{connector_name}";
static CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES: LazyLock<
Option<Vec<ConsequentialToolMessageTemplate>>,
> = LazyLock::new(load_consequential_tool_message_templates);
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct RenderedMcpToolApprovalTemplate {
pub(crate) question: String,
pub(crate) elicitation_message: String,
pub(crate) tool_params: Option<Value>,
pub(crate) tool_params_display: Vec<RenderedMcpToolApprovalParam>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub(crate) struct RenderedMcpToolApprovalParam {
pub(crate) name: String,
pub(crate) value: Value,
}
#[derive(Debug, Deserialize)]
struct ConsequentialToolMessageTemplatesFile {
schema_version: u8,
templates: Vec<ConsequentialToolMessageTemplate>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
struct ConsequentialToolMessageTemplate {
connector_id: String,
server_name: String,
tool_title: String,
template: String,
template_params: Vec<ConsequentialToolTemplateParam>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
struct ConsequentialToolTemplateParam {
name: String,
label: String,
}
pub(crate) fn render_mcp_tool_approval_template(
server_name: &str,
connector_id: Option<&str>,
connector_name: Option<&str>,
tool_title: Option<&str>,
tool_params: Option<&Value>,
) -> Option<RenderedMcpToolApprovalTemplate> {
let templates = CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES.as_ref()?;
render_mcp_tool_approval_template_from_templates(
templates,
server_name,
connector_id,
connector_name,
tool_title,
tool_params,
)
}
fn load_consequential_tool_message_templates() -> Option<Vec<ConsequentialToolMessageTemplate>> {
let templates = match serde_json::from_str::<ConsequentialToolMessageTemplatesFile>(
include_str!("consequential_tool_message_templates.json"),
) {
Ok(templates) => templates,
Err(err) => {
warn!(error = %err, "failed to parse consequential tool approval templates");
return None;
}
};
if templates.schema_version != CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION {
warn!(
found_schema_version = templates.schema_version,
expected_schema_version = CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION,
"unexpected consequential tool approval templates schema version"
);
return None;
}
Some(templates.templates)
}
fn render_mcp_tool_approval_template_from_templates(
templates: &[ConsequentialToolMessageTemplate],
server_name: &str,
connector_id: Option<&str>,
connector_name: Option<&str>,
tool_title: Option<&str>,
tool_params: Option<&Value>,
) -> Option<RenderedMcpToolApprovalTemplate> {
let connector_id = connector_id?;
let tool_title = tool_title.map(str::trim).filter(|name| !name.is_empty())?;
let template = templates.iter().find(|template| {
template.server_name == server_name
&& template.connector_id == connector_id
&& template.tool_title == tool_title
})?;
let elicitation_message = render_question_template(&template.template, connector_name)?;
let (tool_params, tool_params_display) = match tool_params {
Some(Value::Object(tool_params)) => {
render_tool_params(tool_params, &template.template_params)?
}
Some(_) => return None,
None => (None, Vec::new()),
};
Some(RenderedMcpToolApprovalTemplate {
question: elicitation_message.clone(),
elicitation_message,
tool_params,
tool_params_display,
})
}
fn render_question_template(template: &str, connector_name: Option<&str>) -> Option<String> {
let template = template.trim();
if template.is_empty() {
return None;
}
if template.contains(CONNECTOR_NAME_TEMPLATE_VAR) {
let connector_name = connector_name
.map(str::trim)
.filter(|name| !name.is_empty())?;
return Some(template.replace(CONNECTOR_NAME_TEMPLATE_VAR, connector_name));
}
Some(template.to_string())
}
fn render_tool_params(
tool_params: &Map<String, Value>,
template_params: &[ConsequentialToolTemplateParam],
) -> Option<(Option<Value>, Vec<RenderedMcpToolApprovalParam>)> {
let mut relabeled = Map::new();
let mut display_params = Vec::new();
let mut handled_names = HashSet::new();
for template_param in template_params {
let label = template_param.label.trim();
if label.is_empty() {
return None;
}
let Some(value) = tool_params.get(&template_param.name) else {
continue;
};
if relabeled.insert(label.to_string(), value.clone()).is_some() {
return None;
}
display_params.push(RenderedMcpToolApprovalParam {
name: label.to_string(),
value: value.clone(),
});
handled_names.insert(template_param.name.as_str());
}
let mut remaining_params = tool_params
.iter()
.filter(|(name, _)| !handled_names.contains(name.as_str()))
.collect::<Vec<_>>();
remaining_params.sort_by(|(left_name, _), (right_name, _)| left_name.cmp(right_name));
for (name, value) in remaining_params {
if handled_names.contains(name.as_str()) {
continue;
}
if relabeled.insert(name.clone(), value.clone()).is_some() {
return None;
}
display_params.push(RenderedMcpToolApprovalParam {
name: name.clone(),
value: value.clone(),
});
}
Some((Some(Value::Object(relabeled)), display_params))
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use serde_json::json;
use super::*;
#[test]
fn renders_exact_match_with_readable_param_labels() {
let templates = vec![ConsequentialToolMessageTemplate {
connector_id: "calendar".to_string(),
server_name: "codex_apps".to_string(),
tool_title: "create_event".to_string(),
template: "Allow {connector_name} to create an event?".to_string(),
template_params: vec![
ConsequentialToolTemplateParam {
name: "calendar_id".to_string(),
label: "Calendar".to_string(),
},
ConsequentialToolTemplateParam {
name: "title".to_string(),
label: "Title".to_string(),
},
],
}];
let rendered = render_mcp_tool_approval_template_from_templates(
&templates,
"codex_apps",
Some("calendar"),
Some("Calendar"),
Some("create_event"),
Some(&json!({
"title": "Roadmap review",
"calendar_id": "primary",
"timezone": "UTC",
})),
);
assert_eq!(
rendered,
Some(RenderedMcpToolApprovalTemplate {
question: "Allow Calendar to create an event?".to_string(),
elicitation_message: "Allow Calendar to create an event?".to_string(),
tool_params: Some(json!({
"Calendar": "primary",
"Title": "Roadmap review",
"timezone": "UTC",
})),
tool_params_display: vec![
RenderedMcpToolApprovalParam {
name: "Calendar".to_string(),
value: json!("primary"),
},
RenderedMcpToolApprovalParam {
name: "Title".to_string(),
value: json!("Roadmap review"),
},
RenderedMcpToolApprovalParam {
name: "timezone".to_string(),
value: json!("UTC"),
},
],
})
);
}
#[test]
fn returns_none_when_no_exact_match_exists() {
let templates = vec![ConsequentialToolMessageTemplate {
connector_id: "calendar".to_string(),
server_name: "codex_apps".to_string(),
tool_title: "create_event".to_string(),
template: "Allow {connector_name} to create an event?".to_string(),
template_params: Vec::new(),
}];
assert_eq!(
render_mcp_tool_approval_template_from_templates(
&templates,
"codex_apps",
Some("calendar"),
Some("Calendar"),
Some("delete_event"),
Some(&json!({})),
),
None
);
}
#[test]
fn returns_none_when_relabeling_would_collide() {
let templates = vec![ConsequentialToolMessageTemplate {
connector_id: "calendar".to_string(),
server_name: "codex_apps".to_string(),
tool_title: "create_event".to_string(),
template: "Allow {connector_name} to create an event?".to_string(),
template_params: vec![ConsequentialToolTemplateParam {
name: "calendar_id".to_string(),
label: "timezone".to_string(),
}],
}];
assert_eq!(
render_mcp_tool_approval_template_from_templates(
&templates,
"codex_apps",
Some("calendar"),
Some("Calendar"),
Some("create_event"),
Some(&json!({
"calendar_id": "primary",
"timezone": "UTC",
})),
),
None
);
}
#[test]
fn bundled_templates_load() {
assert_eq!(CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES.is_some(), true);
}
#[test]
fn renders_literal_template_without_connector_substitution() {
let templates = vec![ConsequentialToolMessageTemplate {
connector_id: "github".to_string(),
server_name: "codex_apps".to_string(),
tool_title: "add_comment".to_string(),
template: "Allow GitHub to add a comment to a pull request?".to_string(),
template_params: Vec::new(),
}];
let rendered = render_mcp_tool_approval_template_from_templates(
&templates,
"codex_apps",
Some("github"),
None,
Some("add_comment"),
Some(&json!({})),
);
assert_eq!(
rendered,
Some(RenderedMcpToolApprovalTemplate {
question: "Allow GitHub to add a comment to a pull request?".to_string(),
elicitation_message: "Allow GitHub to add a comment to a pull request?".to_string(),
tool_params: Some(json!({})),
tool_params_display: Vec::new(),
})
);
}
#[test]
fn returns_none_when_connector_placeholder_has_no_value() {
let templates = vec![ConsequentialToolMessageTemplate {
connector_id: "calendar".to_string(),
server_name: "codex_apps".to_string(),
tool_title: "create_event".to_string(),
template: "Allow {connector_name} to create an event?".to_string(),
template_params: Vec::new(),
}];
assert_eq!(
render_mcp_tool_approval_template_from_templates(
&templates,
"codex_apps",
Some("calendar"),
None,
Some("create_event"),
Some(&json!({})),
),
None
);
}
}

View file

@ -26,6 +26,8 @@ use crate::guardian::guardian_approval_request_to_json;
use crate::guardian::review_approval_request;
use crate::guardian::routes_approval_to_guardian;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam;
use crate::mcp_tool_approval_templates::render_mcp_tool_approval_template;
use crate::protocol::EventMsg;
use crate::protocol::McpInvocation;
use crate::protocol::McpToolCallBeginEvent;
@ -385,6 +387,16 @@ struct McpToolApprovalPromptOptions {
allow_persistent_approval: bool,
}
struct McpToolApprovalElicitationRequest<'a> {
server: &'a str,
metadata: Option<&'a McpToolApprovalMetadata>,
tool_params: Option<&'a serde_json::Value>,
tool_params_display: Option<&'a [RenderedMcpToolApprovalParam]>,
question: RequestUserInputQuestion,
message_override: Option<&'a str>,
prompt_options: McpToolApprovalPromptOptions,
}
const MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX: &str = "mcp_tool_call_approval";
const MCP_TOOL_APPROVAL_ACCEPT: &str = "Allow";
const MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION: &str = "Allow for this session";
@ -403,6 +415,7 @@ const MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY: &str = "connector_description
const MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: &str = "tool_title";
const MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY: &str = "tool_description";
const MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY: &str = "tool_params";
const MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY: &str = "tool_params_display";
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct McpToolApprovalKey {
@ -503,14 +516,26 @@ async fn maybe_request_mcp_tool_approval(
tool_call_mcp_elicitation_enabled,
);
let question_id = format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}");
let rendered_template = render_mcp_tool_approval_template(
&invocation.server,
metadata.and_then(|metadata| metadata.connector_id.as_deref()),
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
metadata.and_then(|metadata| metadata.tool_title.as_deref()),
invocation.arguments.as_ref(),
);
let tool_params_display = rendered_template
.as_ref()
.map(|rendered_template| rendered_template.tool_params_display.clone())
.or_else(|| build_mcp_tool_approval_display_params(invocation.arguments.as_ref()));
let mut question = build_mcp_tool_approval_question(
question_id.clone(),
&invocation.server,
&invocation.tool,
metadata.and_then(|metadata| metadata.tool_title.as_deref()),
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
annotations,
prompt_options,
rendered_template
.as_ref()
.map(|rendered_template| rendered_template.question.as_str()),
);
question.question =
mcp_tool_approval_question_text(question.question, monitor_reason.as_deref());
@ -521,11 +546,22 @@ async fn maybe_request_mcp_tool_approval(
let params = build_mcp_tool_approval_elicitation_request(
sess.as_ref(),
turn_context.as_ref(),
&invocation.server,
metadata,
invocation.arguments.as_ref(),
question.clone(),
prompt_options,
McpToolApprovalElicitationRequest {
server: &invocation.server,
metadata,
tool_params: rendered_template
.as_ref()
.and_then(|rendered_template| rendered_template.tool_params.as_ref())
.or(invocation.arguments.as_ref()),
tool_params_display: tool_params_display.as_deref(),
question,
message_override: rendered_template.as_ref().and_then(|rendered_template| {
monitor_reason
.is_none()
.then_some(rendered_template.elicitation_message.as_str())
}),
prompt_options,
},
);
let decision = parse_mcp_tool_approval_elicitation_response(
sess.request_mcp_server_elicitation(turn_context.as_ref(), request_id, params)
@ -738,34 +774,16 @@ fn build_mcp_tool_approval_question(
question_id: String,
server: &str,
tool_name: &str,
tool_title: Option<&str>,
connector_name: Option<&str>,
annotations: Option<&ToolAnnotations>,
prompt_options: McpToolApprovalPromptOptions,
question_override: Option<&str>,
) -> RequestUserInputQuestion {
let destructive =
annotations.and_then(|annotations| annotations.destructive_hint) == Some(true);
let open_world = annotations.and_then(|annotations| annotations.open_world_hint) == Some(true);
let reason = match (destructive, open_world) {
(true, true) => "may modify data and access external systems",
(true, false) => "may modify or delete data",
(false, true) => "may access external systems",
(false, false) => "may have side effects",
};
let tool_label = tool_title.unwrap_or(tool_name);
let app_label = connector_name
.map(|name| format!("The {name} app"))
let question = question_override
.map(ToString::to_string)
.unwrap_or_else(|| {
if server == CODEX_APPS_MCP_SERVER_NAME {
"This app".to_string()
} else {
format!("The {server} MCP server")
}
build_mcp_tool_approval_fallback_message(server, tool_name, connector_name)
});
let question = format!(
"{app_label} wants to run the tool \"{tool_label}\", which {reason}. Allow this action?"
);
let question = format!("{}?", question.trim_end_matches('?'));
let mut options = vec![RequestUserInputQuestionOption {
label: MCP_TOOL_APPROVAL_ACCEPT.to_string(),
@ -798,6 +816,25 @@ fn build_mcp_tool_approval_question(
}
}
fn build_mcp_tool_approval_fallback_message(
server: &str,
tool_name: &str,
connector_name: Option<&str>,
) -> String {
let actor = connector_name
.map(str::trim)
.filter(|name| !name.is_empty())
.map(ToString::to_string)
.unwrap_or_else(|| {
if server == CODEX_APPS_MCP_SERVER_NAME {
"this app".to_string()
} else {
format!("the {server} MCP server")
}
});
format!("Allow {actor} to run tool \"{tool_name}\"?")
}
fn mcp_tool_approval_question_text(question: String, monitor_reason: Option<&str>) -> String {
match monitor_reason.map(str::trim) {
Some(reason) if !reason.is_empty() => {
@ -819,30 +856,24 @@ fn arc_monitor_interrupt_message(reason: &str) -> String {
fn build_mcp_tool_approval_elicitation_request(
sess: &Session,
turn_context: &TurnContext,
server: &str,
metadata: Option<&McpToolApprovalMetadata>,
tool_params: Option<&serde_json::Value>,
question: RequestUserInputQuestion,
prompt_options: McpToolApprovalPromptOptions,
request: McpToolApprovalElicitationRequest<'_>,
) -> McpServerElicitationRequestParams {
let message = if question.header.trim().is_empty() {
question.question
} else {
let header = question.header;
let prompt = question.question;
format!("{header}\n\n{prompt}")
};
let message = request
.message_override
.map(ToString::to_string)
.unwrap_or_else(|| request.question.question.clone());
McpServerElicitationRequestParams {
thread_id: sess.conversation_id.to_string(),
turn_id: Some(turn_context.sub_id.clone()),
server_name: server.to_string(),
server_name: request.server.to_string(),
request: McpServerElicitationRequest::Form {
meta: build_mcp_tool_approval_elicitation_meta(
server,
metadata,
tool_params,
prompt_options,
request.server,
request.metadata,
request.tool_params,
request.tool_params_display,
request.prompt_options,
),
message,
requested_schema: McpElicitationSchema {
@ -859,6 +890,7 @@ fn build_mcp_tool_approval_elicitation_meta(
server: &str,
metadata: Option<&McpToolApprovalMetadata>,
tool_params: Option<&serde_json::Value>,
tool_params_display: Option<&[RenderedMcpToolApprovalParam]>,
prompt_options: McpToolApprovalPromptOptions,
) -> Option<serde_json::Value> {
let mut meta = serde_json::Map::new();
@ -941,9 +973,34 @@ fn build_mcp_tool_approval_elicitation_meta(
tool_params.clone(),
);
}
if let Some(tool_params_display) = tool_params_display
&& let Ok(tool_params_display) = serde_json::to_value(tool_params_display)
{
meta.insert(
MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY.to_string(),
tool_params_display,
);
}
(!meta.is_empty()).then_some(serde_json::Value::Object(meta))
}
fn build_mcp_tool_approval_display_params(
tool_params: Option<&serde_json::Value>,
) -> Option<Vec<crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam>> {
let tool_params = tool_params?.as_object()?;
let mut display_params = tool_params
.iter()
.map(
|(name, value)| crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam {
name: name.clone(),
value: value.clone(),
},
)
.collect::<Vec<_>>();
display_params.sort_by(|left, right| left.name.cmp(&right.name));
Some(display_params)
}
fn parse_mcp_tool_approval_elicitation_response(
response: Option<ElicitationResponse>,
question_id: &str,
@ -1280,22 +1337,92 @@ mod tests {
);
}
#[tokio::test]
async fn approval_elicitation_request_uses_message_override_and_readable_tool_params() {
let (session, turn_context) = make_session_and_context().await;
let question = build_mcp_tool_approval_question(
"q".to_string(),
CODEX_APPS_MCP_SERVER_NAME,
"create_event",
Some("Calendar"),
prompt_options(true, true),
Some("Allow Calendar to create an event?"),
);
let request = build_mcp_tool_approval_elicitation_request(
&session,
&turn_context,
McpToolApprovalElicitationRequest {
server: CODEX_APPS_MCP_SERVER_NAME,
metadata: Some(&approval_metadata(
Some("calendar"),
Some("Calendar"),
Some("Manage events and schedules."),
Some("Create Event"),
Some("Create a calendar event."),
)),
tool_params: Some(&serde_json::json!({
"Calendar": "primary",
"Title": "Roadmap review",
})),
tool_params_display: None,
question,
message_override: Some("Allow Calendar to create an event?"),
prompt_options: prompt_options(true, true),
},
);
assert_eq!(
request,
McpServerElicitationRequestParams {
thread_id: session.conversation_id.to_string(),
turn_id: Some(turn_context.sub_id),
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
request: McpServerElicitationRequest::Form {
meta: Some(serde_json::json!({
MCP_TOOL_APPROVAL_KIND_KEY: MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL,
MCP_TOOL_APPROVAL_PERSIST_KEY: [
MCP_TOOL_APPROVAL_PERSIST_SESSION,
MCP_TOOL_APPROVAL_PERSIST_ALWAYS,
],
MCP_TOOL_APPROVAL_SOURCE_KEY: MCP_TOOL_APPROVAL_SOURCE_CONNECTOR,
MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY: "calendar",
MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY: "Calendar",
MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY: "Manage events and schedules.",
MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: "Create Event",
MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY: "Create a calendar event.",
MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY: {
"Calendar": "primary",
"Title": "Roadmap review",
},
})),
message: "Allow Calendar to create an event?".to_string(),
requested_schema: McpElicitationSchema {
schema_uri: None,
type_: McpElicitationObjectType::Object,
properties: BTreeMap::new(),
required: None,
},
},
}
);
}
#[test]
fn custom_mcp_tool_question_mentions_server_name() {
let question = build_mcp_tool_approval_question(
"q".to_string(),
"custom_server",
"run_action",
Some("Run Action"),
None,
Some(&annotations(Some(false), Some(true), None)),
prompt_options(false, false),
None,
);
assert_eq!(question.header, "Approve app tool call?");
assert_eq!(
question.question,
"The custom_server MCP server wants to run the tool \"Run Action\", which may modify or delete data. Allow this action?"
"Allow the custom_server MCP server to run tool \"run_action\"?"
);
assert!(
!question
@ -1308,21 +1435,19 @@ mod tests {
}
#[test]
fn codex_apps_tool_question_keeps_legacy_app_label() {
fn codex_apps_tool_question_uses_fallback_app_label() {
let question = build_mcp_tool_approval_question(
"q".to_string(),
CODEX_APPS_MCP_SERVER_NAME,
"run_action",
Some("Run Action"),
None,
Some(&annotations(Some(false), Some(true), None)),
prompt_options(true, true),
None,
);
assert!(
question
.question
.starts_with("This app wants to run the tool \"Run Action\"")
assert_eq!(
question.question,
"Allow this app to run tool \"run_action\"?"
);
}
@ -1332,10 +1457,9 @@ mod tests {
"q".to_string(),
CODEX_APPS_MCP_SERVER_NAME,
"run_action",
Some("Run Action"),
Some("Calendar"),
Some(&annotations(Some(false), Some(true), None)),
prompt_options(true, true),
None,
);
let options = question.options.expect("options");
@ -1374,10 +1498,9 @@ mod tests {
"q".to_string(),
CODEX_APPS_MCP_SERVER_NAME,
"run_action",
Some("Run Action"),
Some("Calendar"),
Some(&annotations(Some(false), Some(true), None)),
mcp_tool_approval_prompt_options(Some(&session_key), Some(&persistent_key), false),
None,
);
assert_eq!(
@ -1401,10 +1524,9 @@ mod tests {
"q".to_string(),
"custom_server",
"run_action",
Some("Run Action"),
None,
Some(&annotations(Some(false), Some(true), None)),
prompt_options(true, false),
None,
);
assert_eq!(
@ -1553,6 +1675,7 @@ mod tests {
"custom_server",
None,
None,
None,
prompt_options(false, false),
),
Some(serde_json::json!({
@ -1574,6 +1697,7 @@ mod tests {
Some("Runs the selected action."),
)),
Some(&serde_json::json!({"id": 1})),
None,
prompt_options(true, false),
),
Some(serde_json::json!({
@ -1732,6 +1856,7 @@ mod tests {
Some(&serde_json::json!({
"calendar_id": "primary",
})),
None,
prompt_options(false, false),
),
Some(serde_json::json!({
@ -1764,6 +1889,7 @@ mod tests {
Some(&serde_json::json!({
"calendar_id": "primary",
})),
None,
prompt_options(true, true),
),
Some(serde_json::json!({

View file

@ -40,6 +40,8 @@ use crate::bottom_pane::selection_popup_common::menu_surface_padding_height;
use crate::bottom_pane::selection_popup_common::render_menu_surface;
use crate::bottom_pane::selection_popup_common::render_rows;
use crate::render::renderable::Renderable;
use crate::text_formatting::format_json_compact;
use crate::text_formatting::truncate_text;
const ANSWER_PLACEHOLDER: &str = "Type your answer";
const OPTIONAL_ANSWER_PLACEHOLDER: &str = "Type your answer (optional)";
@ -58,6 +60,10 @@ const APPROVAL_META_KIND_TOOL_SUGGESTION: &str = "tool_suggestion";
const APPROVAL_PERSIST_KEY: &str = "persist";
const APPROVAL_PERSIST_SESSION_VALUE: &str = "session";
const APPROVAL_PERSIST_ALWAYS_VALUE: &str = "always";
const APPROVAL_TOOL_PARAMS_KEY: &str = "tool_params";
const APPROVAL_TOOL_PARAMS_DISPLAY_KEY: &str = "tool_params_display";
const APPROVAL_TOOL_PARAM_DISPLAY_LIMIT: usize = 3;
const APPROVAL_TOOL_PARAM_VALUE_TRUNCATE_GRAPHEMES: usize = 60;
const TOOL_TYPE_KEY: &str = "tool_type";
const TOOL_ID_KEY: &str = "tool_id";
const TOOL_NAME_KEY: &str = "tool_name";
@ -146,12 +152,19 @@ pub(crate) struct ToolSuggestionRequest {
pub(crate) install_url: String,
}
#[derive(Clone, Debug, PartialEq)]
struct McpToolApprovalDisplayParam {
name: String,
value: Value,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct McpServerElicitationFormRequest {
thread_id: ThreadId,
server_name: String,
request_id: McpRequestId,
message: String,
approval_display_params: Vec<McpToolApprovalDisplayParam>,
response_mode: McpServerElicitationResponseMode,
fields: Vec<McpServerElicitationField>,
tool_suggestion: Option<ToolSuggestionRequest>,
@ -216,6 +229,11 @@ impl McpServerElicitationFormRequest {
});
let is_tool_approval_action =
is_tool_approval && (requested_schema.is_null() || is_empty_object_schema);
let approval_display_params = if is_tool_approval_action {
parse_tool_approval_display_params(meta.as_ref())
} else {
Vec::new()
};
let (response_mode, fields) = if tool_suggestion.is_some()
&& (requested_schema.is_null() || is_empty_object_schema)
@ -297,6 +315,7 @@ impl McpServerElicitationFormRequest {
server_name: request.server_name,
request_id: request.id,
message,
approval_display_params,
response_mode,
fields,
tool_suggestion,
@ -376,6 +395,99 @@ fn tool_approval_supports_persist_mode(meta: Option<&Value>, expected_mode: &str
}
}
fn parse_tool_approval_display_params(meta: Option<&Value>) -> Vec<McpToolApprovalDisplayParam> {
let Some(meta) = meta.and_then(Value::as_object) else {
return Vec::new();
};
let display_params = meta
.get(APPROVAL_TOOL_PARAMS_DISPLAY_KEY)
.and_then(Value::as_array)
.map(|display_params| {
display_params
.iter()
.filter_map(parse_tool_approval_display_param)
.collect::<Vec<_>>()
})
.unwrap_or_default();
if !display_params.is_empty() {
return display_params;
}
let mut fallback_params = meta
.get(APPROVAL_TOOL_PARAMS_KEY)
.and_then(Value::as_object)
.map(|tool_params| {
tool_params
.iter()
.map(|(name, value)| McpToolApprovalDisplayParam {
name: name.clone(),
value: value.clone(),
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
fallback_params.sort_by(|left, right| left.name.cmp(&right.name));
fallback_params
}
fn parse_tool_approval_display_param(value: &Value) -> Option<McpToolApprovalDisplayParam> {
let value = value.as_object()?;
let name = value.get("name")?.as_str()?.trim();
if name.is_empty() {
return None;
}
Some(McpToolApprovalDisplayParam {
name: name.to_string(),
value: value.get("value")?.clone(),
})
}
fn format_tool_approval_display_message(
message: &str,
approval_display_params: &[McpToolApprovalDisplayParam],
) -> String {
let message = message.trim();
if approval_display_params.is_empty() {
return message.to_string();
}
let mut sections = Vec::new();
if !message.is_empty() {
sections.push(message.to_string());
}
let param_lines = approval_display_params
.iter()
.take(APPROVAL_TOOL_PARAM_DISPLAY_LIMIT)
.map(format_tool_approval_display_param_line)
.collect::<Vec<_>>();
if !param_lines.is_empty() {
sections.push(param_lines.join("\n"));
}
let mut message = sections.join("\n\n");
message.push('\n');
message
}
fn format_tool_approval_display_param_line(param: &McpToolApprovalDisplayParam) -> String {
format!(
"{}: {}",
param.name,
format_tool_approval_display_param_value(&param.value)
)
}
fn format_tool_approval_display_param_value(value: &Value) -> String {
let formatted = match value {
Value::String(text) => text.split_whitespace().collect::<Vec<_>>().join(" "),
_ => {
let compact_json = value.to_string();
format_json_compact(&compact_json).unwrap_or(compact_json)
}
};
truncate_text(&formatted, APPROVAL_TOOL_PARAM_VALUE_TRUNCATE_GRAPHEMES)
}
fn parse_fields_from_schema(requested_schema: &Value) -> Option<Vec<McpServerElicitationField>> {
let schema = requested_schema.as_object()?;
if schema.get("type").and_then(Value::as_str) != Some("object") {
@ -779,12 +891,16 @@ impl McpServerElicitationOverlay {
}
fn current_prompt_text(&self) -> String {
let request_message = format_tool_approval_display_message(
&self.request.message,
&self.request.approval_display_params,
);
let Some(field) = self.current_field() else {
return self.request.message.clone();
return request_message;
};
let mut sections = Vec::new();
if !self.request.message.trim().is_empty() {
sections.push(self.request.message.trim().to_string());
if !request_message.trim().is_empty() {
sections.push(request_message);
}
let field_prompt = if field.label.trim().is_empty()
|| field.prompt.trim().is_empty()
@ -1549,7 +1665,11 @@ mod tests {
})
}
fn tool_approval_meta(persist_modes: &[&str]) -> Option<Value> {
fn tool_approval_meta(
persist_modes: &[&str],
tool_params: Option<Value>,
tool_params_display: Option<Vec<(&str, Value)>>,
) -> Option<Value> {
let mut meta = serde_json::Map::from_iter([(
APPROVAL_META_KIND_KEY.to_string(),
Value::String(APPROVAL_META_KIND_MCP_TOOL_CALL.to_string()),
@ -1565,6 +1685,25 @@ mod tests {
),
);
}
if let Some(tool_params) = tool_params {
meta.insert(APPROVAL_TOOL_PARAMS_KEY.to_string(), tool_params);
}
if let Some(tool_params_display) = tool_params_display {
meta.insert(
APPROVAL_TOOL_PARAMS_DISPLAY_KEY.to_string(),
Value::Array(
tool_params_display
.into_iter()
.map(|(name, value)| {
serde_json::json!({
"name": name,
"value": value,
})
})
.collect(),
),
);
}
Some(Value::Object(meta))
}
@ -1616,6 +1755,7 @@ mod tests {
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
message: "Allow this request?".to_string(),
approval_display_params: Vec::new(),
response_mode: McpServerElicitationResponseMode::FormContent,
fields: vec![McpServerElicitationField {
id: "confirmed".to_string(),
@ -1681,6 +1821,7 @@ mod tests {
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
message: "Allow this request?".to_string(),
approval_display_params: Vec::new(),
response_mode: McpServerElicitationResponseMode::ApprovalAction,
fields: vec![McpServerElicitationField {
id: APPROVAL_FIELD_ID.to_string(),
@ -1723,7 +1864,7 @@ mod tests {
form_request(
"Allow this request?",
empty_object_schema(),
tool_approval_meta(&[]),
tool_approval_meta(&[], None, None),
),
)
.expect("expected approval fallback");
@ -1735,6 +1876,7 @@ mod tests {
server_name: "server-1".to_string(),
request_id: McpRequestId::String("request-1".to_string()),
message: "Allow this request?".to_string(),
approval_display_params: Vec::new(),
response_mode: McpServerElicitationResponseMode::ApprovalAction,
fields: vec![McpServerElicitationField {
id: APPROVAL_FIELD_ID.to_string(),
@ -1805,6 +1947,43 @@ mod tests {
assert_eq!(request, None);
}
#[test]
fn tool_approval_display_params_prefer_explicit_display_order() {
let request = McpServerElicitationFormRequest::from_event(
ThreadId::default(),
form_request(
"Allow Calendar to create an event",
empty_object_schema(),
tool_approval_meta(
&[],
Some(serde_json::json!({
"zeta": 3,
"alpha": 1,
})),
Some(vec![
("Calendar", Value::String("primary".to_string())),
("Title", Value::String("Roadmap review".to_string())),
]),
),
),
)
.expect("expected approval fallback");
assert_eq!(
request.approval_display_params,
vec![
McpToolApprovalDisplayParam {
name: "Calendar".to_string(),
value: Value::String("primary".to_string()),
},
McpToolApprovalDisplayParam {
name: "Title".to_string(),
value: Value::String("Roadmap review".to_string()),
},
]
);
}
#[test]
fn submit_sends_accept_with_typed_content() {
let (tx, mut rx) = test_sender();
@ -1865,10 +2044,14 @@ mod tests {
form_request(
"Allow this request?",
empty_object_schema(),
tool_approval_meta(&[
APPROVAL_PERSIST_SESSION_VALUE,
APPROVAL_PERSIST_ALWAYS_VALUE,
]),
tool_approval_meta(
&[
APPROVAL_PERSIST_SESSION_VALUE,
APPROVAL_PERSIST_ALWAYS_VALUE,
],
None,
None,
),
),
)
.expect("expected approval fallback");
@ -1912,10 +2095,14 @@ mod tests {
form_request(
"Allow this request?",
empty_object_schema(),
tool_approval_meta(&[
APPROVAL_PERSIST_SESSION_VALUE,
APPROVAL_PERSIST_ALWAYS_VALUE,
]),
tool_approval_meta(
&[
APPROVAL_PERSIST_SESSION_VALUE,
APPROVAL_PERSIST_ALWAYS_VALUE,
],
None,
None,
),
),
)
.expect("expected approval fallback");
@ -2105,7 +2292,7 @@ mod tests {
form_request(
"Allow this request?",
empty_object_schema(),
tool_approval_meta(&[]),
tool_approval_meta(&[], None, None),
),
)
.expect("expected approval fallback");
@ -2125,10 +2312,14 @@ mod tests {
form_request(
"Allow this request?",
empty_object_schema(),
tool_approval_meta(&[
APPROVAL_PERSIST_SESSION_VALUE,
APPROVAL_PERSIST_ALWAYS_VALUE,
]),
tool_approval_meta(
&[
APPROVAL_PERSIST_SESSION_VALUE,
APPROVAL_PERSIST_ALWAYS_VALUE,
],
None,
None,
),
),
)
.expect("expected approval fallback");
@ -2139,4 +2330,41 @@ mod tests {
render_snapshot(&overlay, Rect::new(0, 0, 120, 16))
);
}
#[test]
fn approval_form_tool_approval_with_param_summary_snapshot() {
let (tx, _rx) = test_sender();
let request = McpServerElicitationFormRequest::from_event(
ThreadId::default(),
form_request(
"Allow Calendar to create an event",
empty_object_schema(),
tool_approval_meta(
&[],
Some(serde_json::json!({
"calendar_id": "primary",
"title": "Roadmap review",
"notes": "This is a deliberately long note that should truncate before it turns the approval body into a giant wall of text in the TUI overlay.",
"ignored_after_limit": "fourth param",
})),
Some(vec![
("Calendar", Value::String("primary".to_string())),
("Title", Value::String("Roadmap review".to_string())),
(
"Notes",
Value::String("This is a deliberately long note that should truncate before it turns the approval body into a giant wall of text in the TUI overlay.".to_string()),
),
("Ignored", Value::String("fourth param".to_string())),
]),
),
),
)
.expect("expected approval fallback");
let overlay = McpServerElicitationOverlay::new(request, tx, true, false, false);
insta::assert_snapshot!(
"mcp_server_elicitation_approval_form_with_param_summary",
render_snapshot(&overlay, Rect::new(0, 0, 120, 16))
);
}
}

View file

@ -0,0 +1,19 @@
---
source: tui/src/bottom_pane/mcp_server_elicitation.rs
expression: "render_snapshot(&overlay, Rect::new(0, 0, 120, 16))"
---
Field 1/1
Allow Calendar to create an event
Calendar: primary
Title: Roadmap review
Notes: This is a deliberately long note that should truncate bef...
1. Allow Run the tool and continue.
2. Cancel Cancel this tool call
enter to submit | esc to cancel