mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
## Why MCP startup failures from spawned subagents were rendered as global notifications, so a child thread's failure could pollute the visible parent transcript. Routing the notification to the child exposed two related replay problems: session refresh could discard the buffered event, and a newly created child `ChatWidget` did not know the expected MCP server set, which could leave its startup spinner running after every server had settled. MCP startup diagnostics should remain visible in the thread that owns the startup without affecting other transcripts. The protocol also needs to support a future app-scoped MCP lifecycle where startup is not owned by any thread. ## Reported Behavior The [originating Slack report](https://openai.slack.com/archives/C08JZTV654K/p1780604538859939) called out that using subagents could turn MCP startup failures into a wall of yellow CLI warnings because repeated failures were not deduplicated. The intended behavior is for those diagnostics to remain visible once in the thread that owns the startup, without polluting the parent transcript. ## What Changed - add nullable `threadId` ownership to `mcpServer/startupStatus/updated` - populate it from the app-server conversation ID for the current thread-scoped lifecycle and regenerate the protocol schema and TypeScript artifacts - treat a missing or null `threadId` as app-scoped without injecting it into the active chat transcript - route and buffer thread-owned MCP startup notifications by thread in the TUI - preserve buffered MCP startup events across child session refresh - seed expected MCP servers before replaying a thread snapshot so startup reaches its terminal state - suppress an identical repeated failure warning for the same server within one startup round The owning thread still renders the detailed failure and final `MCP startup incomplete (...)` summary. ## How to Test 1. Configure an optional MCP server named `smoke` that exits during initialization. 2. Launch the TUI with multi-agent support enabled. 3. Confirm the main thread's own startup failure renders one detailed `smoke` warning and one incomplete-startup summary. 4. Spawn exactly one subagent. 5. Confirm the parent transcript does not receive the subagent's MCP startup failure. 6. Switch to the subagent thread and confirm it contains exactly one detailed `smoke` failure and one incomplete-startup summary. 7. Confirm the subagent's MCP startup spinner disappears and the thread remains usable. 8. Switch between the parent and subagent and confirm the warnings neither move nor duplicate. Targeted tests: - `just test -p codex-app-server-protocol` - `just test -p codex-app-server thread_start_emits_mcp_server_status_updated_notifications` - `just test -p codex-tui mcp_startup` The parent/child behavior and spinner completion were also exercised manually in tmux. `just argument-comment-lint` was attempted but blocked by an unrelated local Bazel LLVM empty-glob failure; touched Rust callsites were inspected manually.
710 lines
24 KiB
Rust
710 lines
24 KiB
Rust
use super::shared::v2_enum_from_core;
|
|
use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest;
|
|
use codex_protocol::items::McpToolCallError as CoreMcpToolCallError;
|
|
use codex_protocol::mcp::CallToolResult as CoreMcpCallToolResult;
|
|
use codex_protocol::mcp::McpServerInfo;
|
|
use codex_protocol::mcp::Resource as McpResource;
|
|
pub use codex_protocol::mcp::ResourceContent as McpResourceContent;
|
|
use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate;
|
|
use codex_protocol::mcp::Tool as McpTool;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use serde_json::Value as JsonValue;
|
|
use std::collections::BTreeMap;
|
|
use ts_rs::TS;
|
|
|
|
v2_enum_from_core!(
|
|
pub enum McpAuthStatus from codex_protocol::protocol::McpAuthStatus {
|
|
Unsupported,
|
|
NotLoggedIn,
|
|
BearerToken,
|
|
OAuth
|
|
}
|
|
);
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct ListMcpServerStatusParams {
|
|
/// Opaque pagination cursor returned by a previous call.
|
|
#[ts(optional = nullable)]
|
|
pub cursor: Option<String>,
|
|
/// Optional page size; defaults to a server-defined value.
|
|
#[ts(optional = nullable)]
|
|
pub limit: Option<u32>,
|
|
/// Controls how much MCP inventory data to fetch for each server.
|
|
/// Defaults to `Full` when omitted.
|
|
#[ts(optional = nullable)]
|
|
pub detail: Option<McpServerStatusDetail>,
|
|
#[ts(optional = nullable)]
|
|
pub thread_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
|
pub enum McpServerStatusDetail {
|
|
Full,
|
|
ToolsAndAuthOnly,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpServerStatus {
|
|
pub name: String,
|
|
pub server_info: Option<McpServerInfo>,
|
|
pub tools: std::collections::HashMap<String, McpTool>,
|
|
pub resources: Vec<McpResource>,
|
|
pub resource_templates: Vec<McpResourceTemplate>,
|
|
pub auth_status: McpAuthStatus,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct ListMcpServerStatusResponse {
|
|
pub data: Vec<McpServerStatus>,
|
|
/// Opaque cursor to pass to the next call to continue after the last item.
|
|
/// If None, there are no more items to return.
|
|
pub next_cursor: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpResourceReadParams {
|
|
#[ts(optional = nullable)]
|
|
pub thread_id: Option<String>,
|
|
pub server: String,
|
|
pub uri: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpResourceReadResponse {
|
|
pub contents: Vec<McpResourceContent>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpServerToolCallParams {
|
|
pub thread_id: String,
|
|
pub server: String,
|
|
pub tool: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub arguments: Option<JsonValue>,
|
|
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub meta: Option<JsonValue>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpServerToolCallResponse {
|
|
pub content: Vec<JsonValue>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub structured_content: Option<JsonValue>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub is_error: Option<bool>,
|
|
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub meta: Option<JsonValue>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpToolCallResult {
|
|
// NOTE: `rmcp::model::Content` (and its `RawContent` variants) would be a more precise Rust
|
|
// representation of MCP content blocks. We intentionally use `serde_json::Value` here because
|
|
// this crate exports JSON schema + TS types (`schemars`/`ts-rs`), and the rmcp model types
|
|
// aren't set up to be schema/TS friendly (and would introduce heavier coupling to rmcp's Rust
|
|
// representations). Using `JsonValue` keeps the payload wire-shaped and easy to export.
|
|
pub content: Vec<JsonValue>,
|
|
pub structured_content: Option<JsonValue>,
|
|
#[serde(rename = "_meta")]
|
|
#[ts(rename = "_meta")]
|
|
pub meta: Option<JsonValue>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpToolCallError {
|
|
pub message: String,
|
|
}
|
|
|
|
impl From<CoreMcpCallToolResult> for McpServerToolCallResponse {
|
|
fn from(result: CoreMcpCallToolResult) -> Self {
|
|
Self {
|
|
content: result.content,
|
|
structured_content: result.structured_content,
|
|
is_error: result.is_error,
|
|
meta: result.meta,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<CoreMcpCallToolResult> for McpToolCallResult {
|
|
fn from(result: CoreMcpCallToolResult) -> Self {
|
|
Self {
|
|
content: result.content,
|
|
structured_content: result.structured_content,
|
|
meta: result.meta,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<CoreMcpToolCallError> for McpToolCallError {
|
|
fn from(error: CoreMcpToolCallError) -> Self {
|
|
Self {
|
|
message: error.message,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpServerRefreshParams {}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpServerRefreshResponse {}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpServerOauthLoginParams {
|
|
pub name: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional = nullable)]
|
|
pub scopes: Option<Vec<String>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional = nullable)]
|
|
pub timeout_secs: Option<i64>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpServerOauthLoginResponse {
|
|
pub authorization_url: String,
|
|
}
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpToolCallProgressNotification {
|
|
pub thread_id: String,
|
|
pub turn_id: String,
|
|
pub item_id: String,
|
|
pub message: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpServerOauthLoginCompletedNotification {
|
|
pub name: String,
|
|
pub success: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpServerStartupState {
|
|
Starting,
|
|
Ready,
|
|
Failed,
|
|
Cancelled,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpServerStatusUpdatedNotification {
|
|
pub thread_id: Option<String>,
|
|
pub name: String,
|
|
pub status: McpServerStartupState,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpServerElicitationAction {
|
|
Accept,
|
|
Decline,
|
|
Cancel,
|
|
}
|
|
|
|
impl McpServerElicitationAction {
|
|
pub fn to_core(self) -> codex_protocol::approvals::ElicitationAction {
|
|
match self {
|
|
Self::Accept => codex_protocol::approvals::ElicitationAction::Accept,
|
|
Self::Decline => codex_protocol::approvals::ElicitationAction::Decline,
|
|
Self::Cancel => codex_protocol::approvals::ElicitationAction::Cancel,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<McpServerElicitationAction> for rmcp::model::ElicitationAction {
|
|
fn from(value: McpServerElicitationAction) -> Self {
|
|
match value {
|
|
McpServerElicitationAction::Accept => Self::Accept,
|
|
McpServerElicitationAction::Decline => Self::Decline,
|
|
McpServerElicitationAction::Cancel => Self::Cancel,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<rmcp::model::ElicitationAction> for McpServerElicitationAction {
|
|
fn from(value: rmcp::model::ElicitationAction) -> Self {
|
|
match value {
|
|
rmcp::model::ElicitationAction::Accept => Self::Accept,
|
|
rmcp::model::ElicitationAction::Decline => Self::Decline,
|
|
rmcp::model::ElicitationAction::Cancel => Self::Cancel,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpServerElicitationRequestParams {
|
|
pub thread_id: String,
|
|
/// Active Codex turn when this elicitation was observed, if app-server could correlate one.
|
|
///
|
|
/// This is nullable because MCP models elicitation as a standalone server-to-client request
|
|
/// identified by the MCP server request id. It may be triggered during a turn, but turn
|
|
/// context is app-server correlation rather than part of the protocol identity of the
|
|
/// elicitation itself.
|
|
pub turn_id: Option<String>,
|
|
pub server_name: String,
|
|
#[serde(flatten)]
|
|
pub request: McpServerElicitationRequest,
|
|
// TODO: When core can correlate an elicitation with an MCP tool call, expose the associated
|
|
// McpToolCall item id here as an optional field. The current core event does not carry that
|
|
// association.
|
|
}
|
|
|
|
/// Typed form schema for MCP `elicitation/create` requests.
|
|
///
|
|
/// This matches the `requestedSchema` shape from the MCP 2025-11-25
|
|
/// `ElicitRequestFormParams` schema.
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationSchema {
|
|
#[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional, rename = "$schema")]
|
|
pub schema_uri: Option<String>,
|
|
#[serde(rename = "type")]
|
|
#[ts(rename = "type")]
|
|
pub type_: McpElicitationObjectType,
|
|
pub properties: BTreeMap<String, McpElicitationPrimitiveSchema>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub required: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "lowercase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpElicitationObjectType {
|
|
Object,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(untagged)]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpElicitationPrimitiveSchema {
|
|
Enum(McpElicitationEnumSchema),
|
|
String(McpElicitationStringSchema),
|
|
Number(McpElicitationNumberSchema),
|
|
Boolean(McpElicitationBooleanSchema),
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationStringSchema {
|
|
#[serde(rename = "type")]
|
|
#[ts(rename = "type")]
|
|
pub type_: McpElicitationStringType,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub title: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub description: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub min_length: Option<u32>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub max_length: Option<u32>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub format: Option<McpElicitationStringFormat>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub default: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "lowercase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpElicitationStringType {
|
|
String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
#[ts(rename_all = "kebab-case", export_to = "v2/")]
|
|
pub enum McpElicitationStringFormat {
|
|
Email,
|
|
Uri,
|
|
Date,
|
|
DateTime,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationNumberSchema {
|
|
#[serde(rename = "type")]
|
|
#[ts(rename = "type")]
|
|
pub type_: McpElicitationNumberType,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub title: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub description: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub minimum: Option<f64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub maximum: Option<f64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub default: Option<f64>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "lowercase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpElicitationNumberType {
|
|
Number,
|
|
Integer,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationBooleanSchema {
|
|
#[serde(rename = "type")]
|
|
#[ts(rename = "type")]
|
|
pub type_: McpElicitationBooleanType,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub title: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub description: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub default: Option<bool>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "lowercase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpElicitationBooleanType {
|
|
Boolean,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(untagged)]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpElicitationEnumSchema {
|
|
SingleSelect(McpElicitationSingleSelectEnumSchema),
|
|
MultiSelect(McpElicitationMultiSelectEnumSchema),
|
|
Legacy(McpElicitationLegacyTitledEnumSchema),
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationLegacyTitledEnumSchema {
|
|
#[serde(rename = "type")]
|
|
#[ts(rename = "type")]
|
|
pub type_: McpElicitationStringType,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub title: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub description: Option<String>,
|
|
#[serde(rename = "enum")]
|
|
#[ts(rename = "enum")]
|
|
pub enum_: Vec<String>,
|
|
#[serde(rename = "enumNames", skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional, rename = "enumNames")]
|
|
pub enum_names: Option<Vec<String>>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub default: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(untagged)]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpElicitationSingleSelectEnumSchema {
|
|
Untitled(McpElicitationUntitledSingleSelectEnumSchema),
|
|
Titled(McpElicitationTitledSingleSelectEnumSchema),
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationUntitledSingleSelectEnumSchema {
|
|
#[serde(rename = "type")]
|
|
#[ts(rename = "type")]
|
|
pub type_: McpElicitationStringType,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub title: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub description: Option<String>,
|
|
#[serde(rename = "enum")]
|
|
#[ts(rename = "enum")]
|
|
pub enum_: Vec<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub default: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationTitledSingleSelectEnumSchema {
|
|
#[serde(rename = "type")]
|
|
#[ts(rename = "type")]
|
|
pub type_: McpElicitationStringType,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub title: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub description: Option<String>,
|
|
#[serde(rename = "oneOf")]
|
|
#[ts(rename = "oneOf")]
|
|
pub one_of: Vec<McpElicitationConstOption>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub default: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(untagged)]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpElicitationMultiSelectEnumSchema {
|
|
Untitled(McpElicitationUntitledMultiSelectEnumSchema),
|
|
Titled(McpElicitationTitledMultiSelectEnumSchema),
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationUntitledMultiSelectEnumSchema {
|
|
#[serde(rename = "type")]
|
|
#[ts(rename = "type")]
|
|
pub type_: McpElicitationArrayType,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub title: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub description: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub min_items: Option<u64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub max_items: Option<u64>,
|
|
pub items: McpElicitationUntitledEnumItems,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub default: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationTitledMultiSelectEnumSchema {
|
|
#[serde(rename = "type")]
|
|
#[ts(rename = "type")]
|
|
pub type_: McpElicitationArrayType,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub title: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub description: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub min_items: Option<u64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub max_items: Option<u64>,
|
|
pub items: McpElicitationTitledEnumItems,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[ts(optional)]
|
|
pub default: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "lowercase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpElicitationArrayType {
|
|
Array,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationUntitledEnumItems {
|
|
#[serde(rename = "type")]
|
|
#[ts(rename = "type")]
|
|
pub type_: McpElicitationStringType,
|
|
#[serde(rename = "enum")]
|
|
#[ts(rename = "enum")]
|
|
pub enum_: Vec<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationTitledEnumItems {
|
|
#[serde(rename = "anyOf", alias = "oneOf")]
|
|
#[ts(rename = "anyOf")]
|
|
pub any_of: Vec<McpElicitationConstOption>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(deny_unknown_fields)]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpElicitationConstOption {
|
|
#[serde(rename = "const")]
|
|
#[ts(rename = "const")]
|
|
pub const_: String,
|
|
pub title: String,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(tag = "mode", rename_all = "camelCase")]
|
|
#[ts(tag = "mode")]
|
|
#[ts(export_to = "v2/")]
|
|
pub enum McpServerElicitationRequest {
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(rename_all = "camelCase")]
|
|
Form {
|
|
#[serde(rename = "_meta")]
|
|
#[ts(rename = "_meta")]
|
|
meta: Option<JsonValue>,
|
|
message: String,
|
|
requested_schema: McpElicitationSchema,
|
|
},
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(rename_all = "camelCase")]
|
|
Url {
|
|
#[serde(rename = "_meta")]
|
|
#[ts(rename = "_meta")]
|
|
meta: Option<JsonValue>,
|
|
message: String,
|
|
url: String,
|
|
elicitation_id: String,
|
|
},
|
|
}
|
|
|
|
impl TryFrom<CoreElicitationRequest> for McpServerElicitationRequest {
|
|
type Error = serde_json::Error;
|
|
|
|
fn try_from(value: CoreElicitationRequest) -> Result<Self, Self::Error> {
|
|
match value {
|
|
CoreElicitationRequest::Form {
|
|
meta,
|
|
message,
|
|
requested_schema,
|
|
} => Ok(Self::Form {
|
|
meta,
|
|
message,
|
|
requested_schema: serde_json::from_value(requested_schema)?,
|
|
}),
|
|
CoreElicitationRequest::Url {
|
|
meta,
|
|
message,
|
|
url,
|
|
elicitation_id,
|
|
} => Ok(Self::Url {
|
|
meta,
|
|
message,
|
|
url,
|
|
elicitation_id,
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(export_to = "v2/")]
|
|
pub struct McpServerElicitationRequestResponse {
|
|
pub action: McpServerElicitationAction,
|
|
/// Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.
|
|
///
|
|
/// This is nullable because decline/cancel responses have no content.
|
|
pub content: Option<JsonValue>,
|
|
/// Optional client metadata for form-mode action handling.
|
|
#[serde(rename = "_meta")]
|
|
#[ts(rename = "_meta")]
|
|
pub meta: Option<JsonValue>,
|
|
}
|
|
|
|
impl From<McpServerElicitationRequestResponse> for rmcp::model::CreateElicitationResult {
|
|
fn from(value: McpServerElicitationRequestResponse) -> Self {
|
|
Self {
|
|
action: value.action.into(),
|
|
content: value.content,
|
|
meta: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<rmcp::model::CreateElicitationResult> for McpServerElicitationRequestResponse {
|
|
fn from(value: rmcp::model::CreateElicitationResult) -> Self {
|
|
Self {
|
|
action: value.action.into(),
|
|
content: value.content,
|
|
meta: None,
|
|
}
|
|
}
|
|
}
|