From 2eac180e5388f24ee2487ecd52eba04d73c8c172 Mon Sep 17 00:00:00 2001 From: nikhillakra2007-tech Date: Sat, 31 Oct 2026 17:06:43 +0530 Subject: [PATCH 1/2] fix(schema): add method-aware deserialization for routing enums The protocol's aggregated enums (ClientRequest, AgentRequest, ClientResponse, AgentResponse, ClientNotification, AgentNotification, ProtocolLevelNotification, and their v2 counterparts) derive Deserialize as #[serde(untagged)] for JSON Schema generation. serde tries untagged variants in declaration order, so a variant whose fields are all optional (e.g. LogoutRequest, ListProvidersRequest) silently matches every payload: parsing a session/new request through JsonRpcMessage> succeeded and returned LogoutRequest with an empty payload, dropping cwd and mcpServers without any error. The crate docs describe these enums as the routing types used to dispatch incoming JSON-RPC messages, so the only obvious parse API corrupted requests and responses silently. Add ClientRequest::from_method_and_params and the matching helpers on every routing enum (from_method_and_params for requests and notifications, from_method_and_result for responses). They dispatch on the JSON-RPC method name first and then deserialize the payload into the matching variant, so unknown non-extension methods fail with MethodDispatchError::UnknownMethod, mismatched payloads fail with MethodDispatchError::InvalidPayload, and _-prefixed extension methods route to the Ext variants with their raw payload preserved. The derived implementations are kept for schema generation, with doc warnings on each enum pointing at the safe entrypoints. --- agent-client-protocol-schema/src/dispatch.rs | 484 +++++++++++++++ agent-client-protocol-schema/src/lib.rs | 2 + agent-client-protocol-schema/src/v1/agent.rs | 21 + agent-client-protocol-schema/src/v1/client.rs | 21 + .../src/v1/protocol_level.rs | 7 + agent-client-protocol-schema/src/v2/agent.rs | 21 + agent-client-protocol-schema/src/v2/client.rs | 21 + .../src/v2/protocol_level.rs | 7 + .../tests/method_dispatch.rs | 555 ++++++++++++++++++ 9 files changed, 1139 insertions(+) create mode 100644 agent-client-protocol-schema/src/dispatch.rs create mode 100644 agent-client-protocol-schema/tests/method_dispatch.rs diff --git a/agent-client-protocol-schema/src/dispatch.rs b/agent-client-protocol-schema/src/dispatch.rs new file mode 100644 index 000000000..b073a13ee --- /dev/null +++ b/agent-client-protocol-schema/src/dispatch.rs @@ -0,0 +1,484 @@ +//! Method-aware deserialization for the protocol request, notification, and +//! response enums. +//! +//! The wire enums ([`v1::ClientRequest`], [`v1::AgentRequest`], +//! [`v1::ClientResponse`], [`v1::AgentResponse`], [`v1::ClientNotification`], +//! [`v1::AgentNotification`], [`v1::ProtocolLevelNotification`], and their +//! [`v2`] counterparts) are `#[serde(untagged)]` so that the JSON schema +//! generator can describe each variant's payload shape. That representation +//! is **not** suitable for parsing incoming messages: serde tries the +//! variants in declaration order, so any variant whose fields all happen to +//! be optional matches *every* payload, silently dropping the data that +//! belonged to the intended variant. For example, a `session/new` request +//! used to deserialize as [`v1::ClientRequest::LogoutRequest`] with an empty +//! payload instead of failing or routing correctly. +//! +//! The `from_method_and_params` / `from_method_and_result` helpers added here +//! dispatch on the JSON-RPC method name first and then deserialize the +//! payload into the matching variant, mirroring how conformant +//! implementations route messages. Extension methods (names starting with +//! `_`) are routed to the `Ext*` variants with their raw payload preserved. + +/// Error returned by the `from_method_and_params` and `from_method_and_result` +/// deserialization helpers. +#[derive(Debug)] +#[non_exhaustive] +pub enum MethodDispatchError { + /// The method name is not part of the protocol and, where the enum has an + /// extension variant, does not start with the extension prefix `_`. + UnknownMethod { + /// The unrecognized method name. + method: String, + }, + /// The payload did not match the shape required by the method's variant. + InvalidPayload { + /// The method that was dispatched. + method: String, + /// The underlying deserialization error. + source: serde_json::Error, + }, +} + +impl std::fmt::Display for MethodDispatchError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UnknownMethod { method } => { + write!(f, "unknown or unsupported method `{method}`") + } + Self::InvalidPayload { method, source } => { + write!(f, "invalid payload for method `{method}`: {source}") + } + } + } +} + +impl std::error::Error for MethodDispatchError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::UnknownMethod { .. } => None, + Self::InvalidPayload { source, .. } => Some(source), + } + } +} + +macro_rules! impl_acp_from_method_and_params { + ( + enum = $Enum:ty, + $(ext = { $ExtVariant:ident, $ext_wrap:ident, $ExtCtor:path },)? + arms = { $( $(#[$meta:meta])* $Variant:ident ($wrap:ident) => $method:expr ),* $(,)? } + ) => { + impl $Enum { + /// Deserializes a wire payload by dispatching on the JSON-RPC + /// method name. + /// + /// Unlike the derived `Deserialize` implementation (which is + /// `#[serde(untagged)]` and exists for schema generation), this + /// never silently selects the wrong variant: unknown non-extension + /// methods fail with [`MethodDispatchError::UnknownMethod`], and + /// payloads that do not match the method's shape fail with + /// [`MethodDispatchError::InvalidPayload`]. + /// + /// # Errors + /// + /// Returns [`MethodDispatchError::UnknownMethod`] for method names + /// that are not part of the protocol and do not start with the + /// extension prefix `_`, and [`MethodDispatchError::InvalidPayload`] + /// when the payload does not match the dispatched variant's shape. + /// + /// `params` is the optional `params` member of the JSON-RPC + /// request or notification; `None` and `null` are treated as an + /// empty object. Extension methods (names starting with `_`) are + /// routed to the enum's extension variant with the raw payload + /// preserved. + pub fn from_method_and_params( + method: &str, + params: Option, + ) -> ::std::result::Result { + match method { + $( + $(#[$meta])* + m if m == $method => ::serde_json::from_value( + params.filter(|p| !p.is_null()).unwrap_or_else(|| ::serde_json::json!({})), + ) + .map(|payload| Self::$Variant(impl_acp_from_method_and_params!(@wrap $wrap payload))) + .map_err(|source| $crate::dispatch::MethodDispatchError::InvalidPayload { + method: method.to_owned(), + source, + }), + )* + _ => { + $( + if method.starts_with('_') { + let raw_params = params.unwrap_or(::serde_json::Value::Null); + return ::serde_json::value::RawValue::from_string(raw_params.to_string()) + .map_err(|source| $crate::dispatch::MethodDispatchError::InvalidPayload { + method: method.to_owned(), + source, + }) + .map(|raw| { + Self::$ExtVariant(impl_acp_from_method_and_params!(@wrap $ext_wrap ($ExtCtor( + ::std::sync::Arc::from(method), + ::std::sync::Arc::from(raw), + )))) + }); + } + )? + Err($crate::dispatch::MethodDispatchError::UnknownMethod { + method: method.to_owned(), + }) + } + } + } + } + }; + (@wrap plain $payload:expr) => { + $payload + }; + (@wrap boxed $payload:expr) => { + Box::new($payload) + }; +} + +macro_rules! impl_acp_from_method_and_result { + ( + enum = $Enum:ty, + ext = { $ExtVariant:ident, $ext_wrap:ident, $ExtCtor:path }, + arms = { $( $(#[$meta:meta])* $Variant:ident ($wrap:ident) => $method:expr ),* $(,)? } + ) => { + impl $Enum { + /// Deserializes the `result` payload of a successful JSON-RPC + /// response by dispatching on the method name of the originating + /// request. + /// + /// Unlike the derived `Deserialize` implementation (which is + /// `#[serde(untagged)]` and exists for schema generation), this + /// never silently selects the wrong variant: unknown non-extension + /// methods fail with [`MethodDispatchError::UnknownMethod`], and + /// payloads that do not match the response's shape fail with + /// [`MethodDispatchError::InvalidPayload`]. + /// + /// # Errors + /// + /// Returns [`MethodDispatchError::UnknownMethod`] for method names + /// that are not part of the protocol and do not start with the + /// extension prefix `_`, and [`MethodDispatchError::InvalidPayload`] + /// when the payload does not match the dispatched variant's shape. + /// + /// `result` is the optional `result` member of the JSON-RPC + /// response; `None` and `null` are treated as an empty object. + /// Extension methods (names starting with `_`) are routed to the + /// enum's extension variant with the raw payload preserved. + pub fn from_method_and_result( + method: &str, + result: Option, + ) -> ::std::result::Result { + match method { + $( + $(#[$meta])* + m if m == $method => ::serde_json::from_value( + result.filter(|p| !p.is_null()).unwrap_or_else(|| ::serde_json::json!({})), + ) + .map(|payload| Self::$Variant(impl_acp_from_method_and_result!(@wrap $wrap payload))) + .map_err(|source| $crate::dispatch::MethodDispatchError::InvalidPayload { + method: method.to_owned(), + source, + }), + )* + _ => { + if method.starts_with('_') { + let raw_result = result.unwrap_or(::serde_json::Value::Null); + return ::serde_json::value::RawValue::from_string(raw_result.to_string()) + .map_err(|source| $crate::dispatch::MethodDispatchError::InvalidPayload { + method: method.to_owned(), + source, + }) + .map(|raw| { + Self::$ExtVariant(impl_acp_from_method_and_result!(@wrap $ext_wrap ($ExtCtor( + ::std::sync::Arc::from(raw), + )))) + }); + } + Err($crate::dispatch::MethodDispatchError::UnknownMethod { + method: method.to_owned(), + }) + } + } + } + } + }; + (@wrap plain $payload:expr) => { + $payload + }; + (@wrap boxed $payload:expr) => { + Box::new($payload) + }; +} + +impl_acp_from_method_and_params! { + enum = crate::v1::ClientRequest, + ext = { ExtMethodRequest, plain, crate::v1::ExtRequest::new }, + arms = { + InitializeRequest (plain) => crate::v1::AGENT_METHOD_NAMES.initialize, + AuthenticateRequest (plain) => crate::v1::AGENT_METHOD_NAMES.authenticate, + #[cfg(feature = "unstable_llm_providers")] + ListProvidersRequest (plain) => crate::v1::AGENT_METHOD_NAMES.providers_list, + #[cfg(feature = "unstable_llm_providers")] + SetProviderRequest (plain) => crate::v1::AGENT_METHOD_NAMES.providers_set, + #[cfg(feature = "unstable_llm_providers")] + DisableProviderRequest (plain) => crate::v1::AGENT_METHOD_NAMES.providers_disable, + LogoutRequest (plain) => crate::v1::AGENT_METHOD_NAMES.logout, + #[cfg(feature = "unstable_auth_status")] + AuthStatusRequest (plain) => crate::v1::AGENT_METHOD_NAMES.auth_status, + NewSessionRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_new, + LoadSessionRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_load, + ListSessionsRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_list, + DeleteSessionRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_delete, + #[cfg(feature = "unstable_session_fork")] + ForkSessionRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_fork, + #[cfg(feature = "unstable_session_archive")] + ArchiveSessionRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_archive, + #[cfg(feature = "unstable_session_archive")] + UnarchiveSessionRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_unarchive, + #[cfg(feature = "unstable_session_status")] + SessionStatusRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_status, + #[cfg(feature = "unstable_session_set_title")] + SetSessionTitleRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_set_title, + ResumeSessionRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_resume, + CloseSessionRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_close, + SetSessionModeRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_set_mode, + SetSessionConfigOptionRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_set_config_option, + PromptRequest (plain) => crate::v1::AGENT_METHOD_NAMES.session_prompt, + } +} + +impl_acp_from_method_and_params! { + enum = crate::v1::AgentRequest, + ext = { ExtMethodRequest, plain, crate::v1::ExtRequest::new }, + arms = { + WriteTextFileRequest (plain) => crate::v1::CLIENT_METHOD_NAMES.fs_write_text_file, + ReadTextFileRequest (plain) => crate::v1::CLIENT_METHOD_NAMES.fs_read_text_file, + RequestPermissionRequest (plain) => crate::v1::CLIENT_METHOD_NAMES.session_request_permission, + CreateTerminalRequest (plain) => crate::v1::CLIENT_METHOD_NAMES.terminal_create, + TerminalOutputRequest (plain) => crate::v1::CLIENT_METHOD_NAMES.terminal_output, + ReleaseTerminalRequest (plain) => crate::v1::CLIENT_METHOD_NAMES.terminal_release, + WaitForTerminalExitRequest (plain) => crate::v1::CLIENT_METHOD_NAMES.terminal_wait_for_exit, + KillTerminalRequest (plain) => crate::v1::CLIENT_METHOD_NAMES.terminal_kill, + CreateElicitationRequest (plain) => crate::v1::CLIENT_METHOD_NAMES.elicitation_create, + #[cfg(feature = "unstable_mcp_over_acp")] + MessageMcpRequest (plain) => crate::v1::CLIENT_METHOD_NAMES.mcp_message, + } +} + +impl_acp_from_method_and_params! { + enum = crate::v1::ClientNotification, + ext = { ExtNotification, plain, crate::v1::ExtNotification::new }, + arms = { + CancelNotification (plain) => crate::v1::AGENT_METHOD_NAMES.session_cancel, + #[cfg(feature = "unstable_mcp_over_acp")] + MessageMcpNotification (plain) => crate::v1::AGENT_METHOD_NAMES.mcp_message, + } +} + +impl_acp_from_method_and_params! { + enum = crate::v1::AgentNotification, + ext = { ExtNotification, plain, crate::v1::ExtNotification::new }, + arms = { + SessionNotification (plain) => crate::v1::CLIENT_METHOD_NAMES.session_update, + CompleteElicitationNotification (plain) => crate::v1::CLIENT_METHOD_NAMES.elicitation_complete, + #[cfg(feature = "unstable_auth_status")] + AuthStatusUpdateNotification (plain) => crate::v1::CLIENT_METHOD_NAMES.auth_status_update, + } +} + +impl_acp_from_method_and_params! { + enum = crate::v1::ProtocolLevelNotification, + arms = { + CancelRequestNotification (plain) => crate::v1::PROTOCOL_LEVEL_METHOD_NAMES.cancel_request, + } +} + +impl_acp_from_method_and_result! { + enum = crate::v1::AgentResponse, + ext = { ExtMethodResponse, plain, crate::v1::ExtResponse::new }, + arms = { + InitializeResponse (plain) => crate::v1::AGENT_METHOD_NAMES.initialize, + AuthenticateResponse (plain) => crate::v1::AGENT_METHOD_NAMES.authenticate, + #[cfg(feature = "unstable_llm_providers")] + ListProvidersResponse (plain) => crate::v1::AGENT_METHOD_NAMES.providers_list, + #[cfg(feature = "unstable_llm_providers")] + SetProviderResponse (plain) => crate::v1::AGENT_METHOD_NAMES.providers_set, + #[cfg(feature = "unstable_llm_providers")] + DisableProviderResponse (plain) => crate::v1::AGENT_METHOD_NAMES.providers_disable, + LogoutResponse (plain) => crate::v1::AGENT_METHOD_NAMES.logout, + #[cfg(feature = "unstable_auth_status")] + AuthStatusResponse (plain) => crate::v1::AGENT_METHOD_NAMES.auth_status, + NewSessionResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_new, + LoadSessionResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_load, + ListSessionsResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_list, + DeleteSessionResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_delete, + #[cfg(feature = "unstable_session_fork")] + ForkSessionResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_fork, + #[cfg(feature = "unstable_session_archive")] + ArchiveSessionResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_archive, + #[cfg(feature = "unstable_session_archive")] + UnarchiveSessionResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_unarchive, + #[cfg(feature = "unstable_session_status")] + SessionStatusResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_status, + #[cfg(feature = "unstable_session_set_title")] + SetSessionTitleResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_set_title, + ResumeSessionResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_resume, + CloseSessionResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_close, + SetSessionModeResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_set_mode, + SetSessionConfigOptionResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_set_config_option, + PromptResponse (plain) => crate::v1::AGENT_METHOD_NAMES.session_prompt, + } +} + +impl_acp_from_method_and_result! { + enum = crate::v1::ClientResponse, + ext = { ExtMethodResponse, plain, crate::v1::ExtResponse::new }, + arms = { + WriteTextFileResponse (plain) => crate::v1::CLIENT_METHOD_NAMES.fs_write_text_file, + ReadTextFileResponse (plain) => crate::v1::CLIENT_METHOD_NAMES.fs_read_text_file, + RequestPermissionResponse (plain) => crate::v1::CLIENT_METHOD_NAMES.session_request_permission, + CreateTerminalResponse (plain) => crate::v1::CLIENT_METHOD_NAMES.terminal_create, + TerminalOutputResponse (plain) => crate::v1::CLIENT_METHOD_NAMES.terminal_output, + ReleaseTerminalResponse (plain) => crate::v1::CLIENT_METHOD_NAMES.terminal_release, + WaitForTerminalExitResponse (plain) => crate::v1::CLIENT_METHOD_NAMES.terminal_wait_for_exit, + KillTerminalResponse (plain) => crate::v1::CLIENT_METHOD_NAMES.terminal_kill, + CreateElicitationResponse (plain) => crate::v1::CLIENT_METHOD_NAMES.elicitation_create, + #[cfg(feature = "unstable_mcp_over_acp")] + MessageMcpResponse (plain) => crate::v1::CLIENT_METHOD_NAMES.mcp_message, + } +} + +#[cfg(feature = "unstable_protocol_v2")] +impl_acp_from_method_and_params! { + enum = crate::v2::ClientRequest, + ext = { ExtMethodRequest, boxed, crate::v2::ExtRequest::new }, + arms = { + InitializeRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.initialize, + LoginAuthRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.auth_login, + #[cfg(feature = "unstable_llm_providers")] + ListProvidersRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.providers_list, + #[cfg(feature = "unstable_llm_providers")] + SetProviderRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.providers_set, + #[cfg(feature = "unstable_llm_providers")] + DisableProviderRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.providers_disable, + LogoutAuthRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.auth_logout, + #[cfg(feature = "unstable_auth_status")] + AuthStatusRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.auth_status, + NewSessionRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_new, + ListSessionsRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_list, + DeleteSessionRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_delete, + #[cfg(feature = "unstable_session_fork")] + ForkSessionRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_fork, + #[cfg(feature = "unstable_session_archive")] + ArchiveSessionRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_archive, + #[cfg(feature = "unstable_session_archive")] + UnarchiveSessionRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_unarchive, + #[cfg(feature = "unstable_session_status")] + SessionStatusRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_status, + #[cfg(feature = "unstable_session_set_title")] + SetSessionTitleRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_set_title, + ResumeSessionRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_resume, + CloseSessionRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_close, + CancelToolCallRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.tool_call_cancel, + CancelTerminalRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.terminal_cancel, + SetSessionConfigOptionRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_set_config_option, + PromptRequest (boxed) => crate::v2::AGENT_METHOD_NAMES.session_prompt, + } +} + +#[cfg(feature = "unstable_protocol_v2")] +impl_acp_from_method_and_params! { + enum = crate::v2::AgentRequest, + ext = { ExtMethodRequest, boxed, crate::v2::ExtRequest::new }, + arms = { + RequestPermissionRequest (boxed) => crate::v2::CLIENT_METHOD_NAMES.session_request_permission, + CreateElicitationRequest (boxed) => crate::v2::CLIENT_METHOD_NAMES.elicitation_create, + #[cfg(feature = "unstable_mcp_over_acp")] + MessageMcpRequest (boxed) => crate::v2::CLIENT_METHOD_NAMES.mcp_message, + } +} + +#[cfg(feature = "unstable_protocol_v2")] +impl_acp_from_method_and_params! { + enum = crate::v2::ClientNotification, + ext = { ExtNotification, boxed, crate::v2::ExtNotification::new }, + arms = { + CancelSessionNotification (boxed) => crate::v2::AGENT_METHOD_NAMES.session_cancel, + #[cfg(feature = "unstable_mcp_over_acp")] + MessageMcpNotification (boxed) => crate::v2::AGENT_METHOD_NAMES.mcp_message, + } +} + +#[cfg(feature = "unstable_protocol_v2")] +impl_acp_from_method_and_params! { + enum = crate::v2::AgentNotification, + ext = { ExtNotification, boxed, crate::v2::ExtNotification::new }, + arms = { + UpdateSessionNotification (boxed) => crate::v2::CLIENT_METHOD_NAMES.session_update, + CompleteElicitationNotification (boxed) => crate::v2::CLIENT_METHOD_NAMES.elicitation_complete, + #[cfg(feature = "unstable_auth_status")] + AuthStatusUpdateNotification (boxed) => crate::v2::CLIENT_METHOD_NAMES.auth_status_update, + } +} + +#[cfg(feature = "unstable_protocol_v2")] +impl_acp_from_method_and_params! { + enum = crate::v2::ProtocolLevelNotification, + arms = { + CancelRequestNotification (plain) => crate::v2::PROTOCOL_LEVEL_METHOD_NAMES.cancel_request, + } +} + +#[cfg(feature = "unstable_protocol_v2")] +impl_acp_from_method_and_result! { + enum = crate::v2::AgentResponse, + ext = { ExtMethodResponse, boxed, crate::v2::ExtResponse::new }, + arms = { + InitializeResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.initialize, + LoginAuthResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.auth_login, + #[cfg(feature = "unstable_llm_providers")] + ListProvidersResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.providers_list, + #[cfg(feature = "unstable_llm_providers")] + SetProviderResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.providers_set, + #[cfg(feature = "unstable_llm_providers")] + DisableProviderResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.providers_disable, + LogoutAuthResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.auth_logout, + #[cfg(feature = "unstable_auth_status")] + AuthStatusResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.auth_status, + NewSessionResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_new, + ListSessionsResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_list, + DeleteSessionResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_delete, + #[cfg(feature = "unstable_session_fork")] + ForkSessionResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_fork, + #[cfg(feature = "unstable_session_archive")] + ArchiveSessionResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_archive, + #[cfg(feature = "unstable_session_archive")] + UnarchiveSessionResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_unarchive, + #[cfg(feature = "unstable_session_status")] + SessionStatusResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_status, + #[cfg(feature = "unstable_session_set_title")] + SetSessionTitleResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_set_title, + ResumeSessionResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_resume, + CloseSessionResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_close, + CancelToolCallResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.tool_call_cancel, + CancelTerminalResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.terminal_cancel, + SetSessionConfigOptionResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_set_config_option, + PromptResponse (boxed) => crate::v2::AGENT_METHOD_NAMES.session_prompt, + } +} + +#[cfg(feature = "unstable_protocol_v2")] +impl_acp_from_method_and_result! { + enum = crate::v2::ClientResponse, + ext = { ExtMethodResponse, boxed, crate::v2::ExtResponse::new }, + arms = { + RequestPermissionResponse (boxed) => crate::v2::CLIENT_METHOD_NAMES.session_request_permission, + CreateElicitationResponse (boxed) => crate::v2::CLIENT_METHOD_NAMES.elicitation_create, + #[cfg(feature = "unstable_mcp_over_acp")] + MessageMcpResponse (boxed) => crate::v2::CLIENT_METHOD_NAMES.mcp_message, + } +} diff --git a/agent-client-protocol-schema/src/lib.rs b/agent-client-protocol-schema/src/lib.rs index 8d765aeef..f847be683 100644 --- a/agent-client-protocol-schema/src/lib.rs +++ b/agent-client-protocol-schema/src/lib.rs @@ -45,6 +45,7 @@ //! For the complete protocol specification and documentation, visit //! . +mod dispatch; pub mod rpc; mod serde_util; pub mod v1; @@ -52,6 +53,7 @@ pub mod v1; pub mod v2; mod version; +pub use dispatch::MethodDispatchError; pub use serde_util::{IntoMaybeUndefined, IntoOption, MaybeUndefined}; pub use version::*; diff --git a/agent-client-protocol-schema/src/v1/agent.rs b/agent-client-protocol-schema/src/v1/agent.rs index b6bc45eff..cdf33d52b 100644 --- a/agent-client-protocol-schema/src/v1/agent.rs +++ b/agent-client-protocol-schema/src/v1/agent.rs @@ -6403,6 +6403,13 @@ pub(crate) const AUTH_STATUS_METHOD_NAME: &str = "auth/status"; /// to use this directly. /// /// This enum encompasses all method calls from client to agent. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`ClientRequest::from_method_and_params`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] @@ -6655,6 +6662,13 @@ impl ClientRequest { /// to use this directly - the responses are handled automatically by the connection. /// /// These are responses to the corresponding `ClientRequest` variants. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`AgentResponse::from_method_and_result`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] @@ -6723,6 +6737,13 @@ pub enum AgentResponse { /// to use this directly. /// /// Notifications do not expect a response. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`ClientNotification::from_method_and_params`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] diff --git a/agent-client-protocol-schema/src/v1/client.rs b/agent-client-protocol-schema/src/v1/client.rs index ba2233a49..51159a173 100644 --- a/agent-client-protocol-schema/src/v1/client.rs +++ b/agent-client-protocol-schema/src/v1/client.rs @@ -3992,6 +3992,13 @@ pub(crate) const AUTH_STATUS_UPDATE_NOTIFICATION: &str = "auth/status_update"; /// to use this directly. /// /// This enum encompasses all method calls from agent to client. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`AgentRequest::from_method_and_params`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] @@ -4124,6 +4131,13 @@ impl AgentRequest { /// to use this directly - the responses are handled automatically by the connection. /// /// These are responses to the corresponding `AgentRequest` variants. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`ClientResponse::from_method_and_result`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] @@ -4161,6 +4175,13 @@ pub enum ClientResponse { /// to use this directly. /// /// Notifications do not expect a response. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`AgentNotification::from_method_and_params`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] diff --git a/agent-client-protocol-schema/src/v1/protocol_level.rs b/agent-client-protocol-schema/src/v1/protocol_level.rs index 0e29f13ec..3f03f9c9b 100644 --- a/agent-client-protocol-schema/src/v1/protocol_level.rs +++ b/agent-client-protocol-schema/src/v1/protocol_level.rs @@ -83,6 +83,13 @@ pub(crate) const CANCEL_REQUEST_METHOD_NAME: &str = "$/cancel_request"; /// notifications starting with '$/' it is free to ignore the notification. /// /// Notifications do not expect a response. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`ProtocolLevelNotification::from_method_and_params`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] diff --git a/agent-client-protocol-schema/src/v2/agent.rs b/agent-client-protocol-schema/src/v2/agent.rs index d8d2487f0..4dd5d1267 100644 --- a/agent-client-protocol-schema/src/v2/agent.rs +++ b/agent-client-protocol-schema/src/v2/agent.rs @@ -7129,6 +7129,13 @@ pub(crate) const AUTH_STATUS_METHOD_NAME: &str = "auth/status"; /// to use this directly. /// /// This enum encompasses all method calls from client to agent. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`ClientRequest::from_method_and_params`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] @@ -7384,6 +7391,13 @@ impl ClientRequest { /// to use this directly - the responses are handled automatically by the connection. /// /// These are responses to the corresponding `ClientRequest` variants. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`AgentResponse::from_method_and_result`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] @@ -7451,6 +7465,13 @@ pub enum AgentResponse { /// to use this directly. /// /// Notifications do not expect a response. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`ClientNotification::from_method_and_params`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] diff --git a/agent-client-protocol-schema/src/v2/client.rs b/agent-client-protocol-schema/src/v2/client.rs index 9021d3510..efae48007 100644 --- a/agent-client-protocol-schema/src/v2/client.rs +++ b/agent-client-protocol-schema/src/v2/client.rs @@ -3965,6 +3965,13 @@ pub(crate) const AUTH_STATUS_UPDATE_NOTIFICATION: &str = "auth/status_update"; /// to use this directly. /// /// This enum encompasses all method calls from agent to client. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`AgentRequest::from_method_and_params`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] @@ -4023,6 +4030,13 @@ impl AgentRequest { /// to use this directly - the responses are handled automatically by the connection. /// /// These are responses to the corresponding `AgentRequest` variants. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`ClientResponse::from_method_and_result`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] @@ -4046,6 +4060,13 @@ pub enum ClientResponse { /// to use this directly. /// /// Notifications do not expect a response. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`AgentNotification::from_method_and_params`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] diff --git a/agent-client-protocol-schema/src/v2/protocol_level.rs b/agent-client-protocol-schema/src/v2/protocol_level.rs index a2ff093c9..35b79ec74 100644 --- a/agent-client-protocol-schema/src/v2/protocol_level.rs +++ b/agent-client-protocol-schema/src/v2/protocol_level.rs @@ -83,6 +83,13 @@ pub(crate) const CANCEL_REQUEST_METHOD_NAME: &str = "$/cancel_request"; /// notifications starting with '$/' it is free to ignore the notification. /// /// Notifications do not expect a response. +/// +/// # Deserialization +/// +/// Do not parse incoming messages with the derived untagged `Deserialize` +/// (it exists for schema generation): variants are tried in order, so +/// all-optional variants silently swallow any payload. Use +/// [`ProtocolLevelNotification::from_method_and_params`]. #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] diff --git a/agent-client-protocol-schema/tests/method_dispatch.rs b/agent-client-protocol-schema/tests/method_dispatch.rs new file mode 100644 index 000000000..dd7d41a5d --- /dev/null +++ b/agent-client-protocol-schema/tests/method_dispatch.rs @@ -0,0 +1,555 @@ +//! Tests for method-aware deserialization of the protocol enums. +//! +//! The enums' derived `Deserialize` implementations are `#[serde(untagged)]` +//! and exist for schema generation: serde tries variants in declaration +//! order, so a variant with all-optional fields silently matches any payload. +//! The `from_method_and_params` / `from_method_and_result` helpers dispatch +//! on the JSON-RPC method name instead and must route every documented +//! method to its intended variant. + +use agent_client_protocol_schema::{MethodDispatchError, v1}; +use serde_json::json; + +#[test] +fn v1_session_new_routes_to_new_session_request() { + // Regression test: with the derived untagged deserialization this exact + // payload silently parsed as `ClientRequest::LogoutRequest`, dropping + // `cwd` and `mcpServers` without any error. + let parsed = v1::ClientRequest::from_method_and_params( + "session/new", + Some(json!({ + "cwd": "/home/user/project", + "mcpServers": [{"name": "filesystem", "command": "/path/to/mcp-server", "args": ["--stdio"], "env": []}], + })), + ) + .unwrap(); + match parsed { + v1::ClientRequest::NewSessionRequest(req) => { + assert_eq!(req.cwd, std::path::PathBuf::from("/home/user/project")); + assert_eq!(req.mcp_servers.len(), 1); + } + other => panic!("expected NewSessionRequest, got {}", other.method()), + } +} + +#[test] +fn v1_stable_methods_route_to_their_variant() { + // Client-to-agent methods. + let client_cases: &[(&str, serde_json::Value)] = &[ + ("initialize", json!({"protocolVersion": 1})), + ("authenticate", json!({"methodId": "oauth"})), + ("logout", json!({})), + ("session/new", json!({"cwd": "/tmp", "mcpServers": []})), + ( + "session/load", + json!({"sessionId": "s1", "cwd": "/tmp", "mcpServers": []}), + ), + ("session/list", json!({})), + ("session/delete", json!({"sessionId": "s1"})), + ("session/close", json!({"sessionId": "s1"})), + ("session/resume", json!({"sessionId": "s1", "cwd": "/tmp"})), + ( + "session/set_mode", + json!({"sessionId": "s1", "modeId": "code"}), + ), + ( + "session/set_config_option", + json!({"sessionId": "s1", "configId": "mode", "type": "id", "value": "code"}), + ), + ( + "session/prompt", + json!({"sessionId": "s1", "prompt": [{"type": "text", "text": "hi"}]}), + ), + ]; + for (method, params) in client_cases { + let parsed = v1::ClientRequest::from_method_and_params(method, Some(params.clone())) + .unwrap_or_else(|e| panic!("{method} failed to parse: {e}")); + assert_eq!( + parsed.method(), + *method, + "{method} routed to the wrong variant", + ); + } + + // Agent-to-client methods. + let agent_cases: &[(&str, serde_json::Value)] = &[ + ( + "session/request_permission", + json!({ + "sessionId": "s1", + "toolCall": {"toolCallId": "t1", "title": "Run tests"}, + "options": [{"optionId": "allow", "name": "Allow", "kind": "allow_once"}], + }), + ), + ( + "fs/read_text_file", + json!({"sessionId": "s1", "path": "/tmp/a"}), + ), + ( + "fs/write_text_file", + json!({"sessionId": "s1", "path": "/tmp/a", "content": "x"}), + ), + ( + "terminal/create", + json!({"sessionId": "s1", "command": "ls"}), + ), + ( + "terminal/output", + json!({"sessionId": "s1", "terminalId": "t1"}), + ), + ( + "terminal/wait_for_exit", + json!({"sessionId": "s1", "terminalId": "t1"}), + ), + ( + "terminal/kill", + json!({"sessionId": "s1", "terminalId": "t1"}), + ), + ( + "terminal/release", + json!({"sessionId": "s1", "terminalId": "t1"}), + ), + ( + "elicitation/create", + json!({"sessionId": "s1", "mode": "form", "message": "pick one", "requestedSchema": {"type": "object", "properties": {}}}), + ), + ]; + for (method, params) in agent_cases { + let parsed = v1::AgentRequest::from_method_and_params(method, Some(params.clone())) + .unwrap_or_else(|e| panic!("{method} failed to parse: {e}")); + assert_eq!( + parsed.method(), + *method, + "{method} routed to the wrong variant", + ); + } +} + +#[test] +fn v1_extension_request_preserves_method_and_raw_params() { + let parsed = v1::ClientRequest::from_method_and_params( + "_com.example/workspace/buffers", + Some(json!({"operation": "list"})), + ) + .unwrap(); + match parsed { + v1::ClientRequest::ExtMethodRequest(ext) => { + assert_eq!(&*ext.method, "_com.example/workspace/buffers"); + let params: serde_json::Value = serde_json::from_str(ext.params.get()).unwrap(); + assert_eq!(params, json!({"operation": "list"})); + } + other => panic!("expected ExtMethodRequest, got {}", other.method()), + } +} + +#[test] +fn v1_unknown_method_is_an_error_not_a_silent_misroute() { + let err = + v1::ClientRequest::from_method_and_params("session/nope", Some(json!({}))).unwrap_err(); + assert!( + matches!(err, MethodDispatchError::UnknownMethod { .. }), + "{err}" + ); +} + +#[test] +fn v1_mismatched_payload_is_an_error_not_a_silent_default() { + // `session/new` requires `cwd`; the derived untagged deserialization used + // to swallow this into an unrelated all-optional variant instead. + let err = + v1::ClientRequest::from_method_and_params("session/new", Some(json!({}))).unwrap_err(); + assert!( + matches!(err, MethodDispatchError::InvalidPayload { .. }), + "{err}" + ); +} + +#[test] +fn v1_notifications_route_and_preserve_payload() { + let parsed = v1::AgentNotification::from_method_and_params( + "session/update", + Some(json!({ + "sessionId": "s1", + "update": {"sessionUpdate": "tool_call_update", "toolCallId": "t1", "status": "in_progress"}, + })), + ) + .unwrap(); + match parsed { + v1::AgentNotification::SessionNotification(notification) => { + assert!(matches!( + notification.update, + v1::SessionUpdate::ToolCallUpdate(_) + )); + } + other => panic!("expected SessionNotification, got {}", other.method()), + } + + let parsed = v1::ClientNotification::from_method_and_params( + "session/cancel", + Some(json!({"sessionId": "s1"})), + ) + .unwrap(); + assert!(matches!( + parsed, + v1::ClientNotification::CancelNotification(_) + )); +} + +#[test] +fn v1_responses_route_by_originating_method() { + let parsed = + v1::AgentResponse::from_method_and_result("session/new", Some(json!({"sessionId": "s1"}))) + .unwrap(); + match parsed { + v1::AgentResponse::NewSessionResponse(response) => { + assert_eq!(&*response.session_id.0, "s1"); + } + other => panic!("expected NewSessionResponse, got {other:?}"), + } + + let parsed = v1::ClientResponse::from_method_and_result( + "fs/read_text_file", + Some(json!({"content": "hello"})), + ) + .unwrap(); + assert!(matches!( + parsed, + v1::ClientResponse::ReadTextFileResponse(_) + )); +} + +#[cfg(feature = "unstable_protocol_v2")] +#[test] +fn v2_requests_notifications_and_responses_route() { + use agent_client_protocol_schema::v2; + + let parsed = v2::ClientRequest::from_method_and_params( + "session/new", + Some(json!({"cwd": "/home/user/project", "mcpServers": []})), + ) + .unwrap(); + assert!(matches!(parsed, v2::ClientRequest::NewSessionRequest(_))); + + let parsed = v2::AgentNotification::from_method_and_params( + "session/update", + Some(json!({ + "sessionId": "s1", + "update": {"sessionUpdate": "tool_call_update", "toolCallId": "t1", "status": "in_progress"}, + })), + ) + .unwrap(); + assert!(matches!( + parsed, + v2::AgentNotification::UpdateSessionNotification(_) + )); + + let parsed = + v2::AgentResponse::from_method_and_result("session/new", Some(json!({"sessionId": "s1"}))) + .unwrap(); + assert!(matches!(parsed, v2::AgentResponse::NewSessionResponse(_))); + + let parsed = + v2::ClientRequest::from_method_and_params("_com.example/thing", Some(json!({"x": 1}))) + .unwrap(); + assert!(matches!(parsed, v2::ClientRequest::ExtMethodRequest(_))); + + let err = v2::ClientRequest::from_method_and_params("session/nope", None).unwrap_err(); + assert!( + matches!(err, MethodDispatchError::UnknownMethod { .. }), + "{err}" + ); +} + +#[test] +fn v1_protocol_level_cancel_request_routes() { + let parsed = v1::ProtocolLevelNotification::from_method_and_params( + "$/cancel_request", + Some(json!({"requestId": 7})), + ) + .unwrap(); + assert!(matches!( + parsed, + v1::ProtocolLevelNotification::CancelRequestNotification(_) + )); + let err = + v1::ProtocolLevelNotification::from_method_and_params("session/new", None).unwrap_err(); + assert!( + matches!(err, MethodDispatchError::UnknownMethod { .. }), + "{err}" + ); +} + +#[test] +fn v1_extension_notification_and_response_preserve_raw_payload() { + let parsed = v1::AgentNotification::from_method_and_params( + "_com.example/progress", + Some(json!({"percent": 42, "stage": "tests"})), + ) + .unwrap(); + match parsed { + v1::AgentNotification::ExtNotification(ext) => { + assert_eq!(&*ext.method, "_com.example/progress"); + let params: serde_json::Value = serde_json::from_str(ext.params.get()).unwrap(); + assert_eq!(params, json!({"percent": 42, "stage": "tests"})); + } + other => panic!("expected ExtNotification, got {}", other.method()), + } + + let parsed = v1::AgentResponse::from_method_and_result( + "_com.example/query", + Some(json!({"answer": [1, 2, 3]})), + ) + .unwrap(); + match parsed { + v1::AgentResponse::ExtMethodResponse(ext) => { + let result: serde_json::Value = serde_json::from_str(ext.0.get()).unwrap(); + assert_eq!(result, json!({"answer": [1, 2, 3]})); + } + other => panic!("expected ExtMethodResponse, got {other:?}"), + } +} + +#[test] +fn v1_null_result_for_ack_style_response_is_accepted() { + // Agents commonly answer ack-style requests with `result: null`; the + // response structs for these methods have all-optional fields. + let parsed = v1::AgentResponse::from_method_and_result("logout", None).unwrap(); + assert!(matches!(parsed, v1::AgentResponse::LogoutResponse(_))); +} + +#[cfg(feature = "unstable_llm_providers")] +#[test] +fn v1_gated_provider_methods_route() { + let parsed = v1::ClientRequest::from_method_and_params("providers/list", None).unwrap(); + assert!(matches!(parsed, v1::ClientRequest::ListProvidersRequest(_))); + // Regression: before method-aware dispatch, providers/list was the + // all-optional variant that silently swallowed session/new payloads. + let parsed = v1::ClientRequest::from_method_and_params( + "session/new", + Some(json!({"cwd": "/tmp", "mcpServers": []})), + ) + .unwrap(); + assert!(matches!(parsed, v1::ClientRequest::NewSessionRequest(_))); +} + +#[cfg(feature = "unstable_session_fork")] +#[test] +fn v1_gated_session_fork_routes() { + let parsed = v1::ClientRequest::from_method_and_params( + "session/fork", + Some(json!({"sessionId": "s1", "cwd": "/tmp", "mcpServers": []})), + ) + .unwrap(); + assert!(matches!(parsed, v1::ClientRequest::ForkSessionRequest(_))); +} + +#[cfg(feature = "unstable_session_archive")] +#[test] +fn v1_gated_session_archive_unarchive_route() { + let parsed = v1::ClientRequest::from_method_and_params( + "session/archive", + Some(json!({"sessionId": "s1"})), + ) + .unwrap(); + assert!(matches!( + parsed, + v1::ClientRequest::ArchiveSessionRequest(_) + )); + let parsed = v1::ClientRequest::from_method_and_params( + "session/unarchive", + Some(json!({"sessionId": "s1"})), + ) + .unwrap(); + assert!(matches!( + parsed, + v1::ClientRequest::UnarchiveSessionRequest(_) + )); +} + +#[cfg(feature = "unstable_session_status")] +#[test] +fn v1_gated_session_status_routes() { + let parsed = v1::ClientRequest::from_method_and_params( + "session/status", + Some(json!({"sessionId": "s1"})), + ) + .unwrap(); + assert!(matches!(parsed, v1::ClientRequest::SessionStatusRequest(_))); +} + +#[cfg(feature = "unstable_session_set_title")] +#[test] +fn v1_gated_session_set_title_routes() { + let parsed = v1::ClientRequest::from_method_and_params( + "session/set_title", + Some(json!({"sessionId": "s1", "title": "Fix the bug"})), + ) + .unwrap(); + assert!(matches!( + parsed, + v1::ClientRequest::SetSessionTitleRequest(_) + )); +} + +#[cfg(feature = "unstable_auth_status")] +#[test] +fn v1_gated_auth_status_routes() { + let parsed = v1::ClientRequest::from_method_and_params("auth/status", None).unwrap(); + assert!(matches!(parsed, v1::ClientRequest::AuthStatusRequest(_))); +} + +#[cfg(feature = "unstable_mcp_over_acp")] +#[test] +fn v1_gated_mcp_message_routes_both_directions() { + let params = json!({ + "serverId": "srv1", + "requestId": "req1", + "method": "tools/list", + "params": null + }); + // `mcp/message` is an agent-to-client request in v1. + let parsed = v1::AgentRequest::from_method_and_params("mcp/message", Some(params)).unwrap(); + assert!(matches!(parsed, v1::AgentRequest::MessageMcpRequest(_))); +} + +#[cfg(feature = "unstable_protocol_v2")] +#[test] +fn v2_gated_and_boxed_variants_route() { + use agent_client_protocol_schema::v2; + + let parsed = v2::ClientRequest::from_method_and_params( + "tool_call/cancel", + Some(json!({"sessionId": "s1", "toolCallId": "t1"})), + ) + .unwrap(); + assert!(matches!( + parsed, + v2::ClientRequest::CancelToolCallRequest(_) + )); + + let parsed = v2::ClientRequest::from_method_and_params( + "terminal/cancel", + Some(json!({"sessionId": "s1", "terminalId": "t1"})), + ) + .unwrap(); + assert!(matches!( + parsed, + v2::ClientRequest::CancelTerminalRequest(_) + )); + + let parsed = v2::ClientNotification::from_method_and_params( + "session/cancel", + Some(json!({"sessionId": "s1"})), + ) + .unwrap(); + assert!(matches!( + parsed, + v2::ClientNotification::CancelSessionNotification(_) + )); + + // Boxed response variants keep their payload. + let parsed = v2::ClientResponse::from_method_and_result( + "session/request_permission", + Some(json!({"outcome": {"outcome": "selected", "optionId": "allow"}})), + ) + .unwrap(); + assert!(matches!( + parsed, + v2::ClientResponse::RequestPermissionResponse(_) + )); +} + +#[cfg(feature = "unstable_protocol_v2")] +#[test] +fn v2_stable_methods_route_to_their_variant() { + use agent_client_protocol_schema::v2; + + // Client-to-agent methods. + let client_cases: &[(&str, serde_json::Value)] = &[ + ( + "initialize", + json!({"protocolVersion": 2, "info": {"name": "c", "version": "1"}}), + ), + ("auth/login", json!({"methodId": "oauth"})), + ("auth/logout", json!({})), + ("session/new", json!({"cwd": "/tmp", "mcpServers": []})), + ("session/list", json!({})), + ("session/delete", json!({"sessionId": "s1"})), + ("session/close", json!({"sessionId": "s1"})), + ("session/resume", json!({"sessionId": "s1", "cwd": "/tmp"})), + ( + "session/set_config_option", + json!({"sessionId": "s1", "configId": "mode", "type": "id", "value": "code"}), + ), + ( + "session/prompt", + json!({"sessionId": "s1", "prompt": [{"type": "text", "text": "hi"}]}), + ), + ]; + for (method, params) in client_cases { + let parsed = v2::ClientRequest::from_method_and_params(method, Some(params.clone())) + .unwrap_or_else(|e| panic!("{method} failed to parse: {e}")); + assert_eq!( + parsed.method(), + *method, + "{method} routed to the wrong variant" + ); + } + + // Agent-to-client methods. + let agent_cases: &[(&str, serde_json::Value)] = &[ + ( + "session/request_permission", + json!({ + "sessionId": "s1", + "title": "Run this script?", + "subject": {"type": "tool_call", "toolCall": {"toolCallId": "t1", "title": "Execute setup script", "kind": "execute", "status": "pending"}}, + "options": [{"optionId": "allow", "name": "Allow", "kind": "allow_once"}], + }), + ), + ( + "elicitation/create", + json!({"sessionId": "s1", "mode": "form", "message": "pick one", "requestedSchema": {"type": "object", "properties": {}}}), + ), + ]; + for (method, params) in agent_cases { + let parsed = v2::AgentRequest::from_method_and_params(method, Some(params.clone())) + .unwrap_or_else(|e| panic!("{method} failed to parse: {e}")); + assert_eq!( + parsed.method(), + *method, + "{method} routed to the wrong variant" + ); + } + + // Notifications. + let parsed = v2::AgentNotification::from_method_and_params( + "session/update", + Some(json!({"sessionId": "s1", "update": {"sessionUpdate": "plan_update", "plan": {"type": "items", "planId": "p1", "entries": []}}})), + ) + .unwrap(); + assert_eq!(parsed.method(), "session/update"); + let parsed = v2::ClientNotification::from_method_and_params( + "session/cancel", + Some(json!({"sessionId": "s1"})), + ) + .unwrap(); + assert_eq!(parsed.method(), "session/cancel"); + + // Responses route by the originating method. + let parsed = v2::AgentResponse::from_method_and_result( + "session/prompt", + Some(json!({"messageId": "m1", "stopReason": "end_turn"})), + ) + .unwrap(); + assert!(matches!(parsed, v2::AgentResponse::PromptResponse(_))); + let parsed = v2::ClientResponse::from_method_and_result( + "elicitation/create", + Some(json!({"action": "cancel"})), + ) + .unwrap(); + assert!(matches!( + parsed, + v2::ClientResponse::CreateElicitationResponse(_) + )); +} From 31ddbd9bc7c980d1072c1024c62fcbe148e3d003 Mon Sep 17 00:00:00 2001 From: nikhillakra2007-tech Date: Sat, 31 Oct 2026 18:48:09 +0530 Subject: [PATCH 2/2] chore(schema): regenerate schema files with deserialization warnings --- schema/v1/schema.json | 10 +++++----- schema/v1/schema.unstable.json | 10 +++++----- schema/v2/schema.json | 10 +++++----- schema/v2/schema.unstable.json | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/schema/v1/schema.json b/schema/v1/schema.json index 776c23150..ee6d5f02c 100644 --- a/schema/v1/schema.json +++ b/schema/v1/schema.json @@ -95,7 +95,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "General protocol-level notifications that all sides are expected to\nimplement.\n\nNotifications whose methods start with '$/' are messages which\nare protocol implementation dependent and might not be implementable in all\nclients or agents. For example if the implementation uses a single threaded\nsynchronous programming language then there is little it can do to react to\na `$/cancel_request` notification. If an agent or client receives\nnotifications starting with '$/' it is free to ignore the notification.\n\nNotifications do not expect a response.", + "description": "General protocol-level notifications that all sides are expected to\nimplement.\n\nNotifications whose methods start with '$/' are messages which\nare protocol implementation dependent and might not be implementable in all\nclients or agents. For example if the implementation uses a single threaded\nsynchronous programming language then there is little it can do to react to\na `$/cancel_request` notification. If an agent or client receives\nnotifications starting with '$/' it is free to ignore the notification.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ProtocolLevelNotification::from_method_and_params`].", "anyOf": [ { "title": "CancelRequestNotification", @@ -139,7 +139,7 @@ "description": "Method-specific request parameters.", "anyOf": [ { - "description": "All possible requests that an agent can send to a client.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from agent to client.", + "description": "All possible requests that an agent can send to a client.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from agent to client.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`AgentRequest::from_method_and_params`].", "anyOf": [ { "title": "WriteTextFileRequest", @@ -3572,7 +3572,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "All possible notifications that an agent can send to a client.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.", + "description": "All possible notifications that an agent can send to a client.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`AgentNotification::from_method_and_params`].", "anyOf": [ { "title": "SessionNotification", @@ -4499,7 +4499,7 @@ "description": "Method-specific request parameters.", "anyOf": [ { - "description": "All possible requests that a client can send to an agent.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from client to agent.", + "description": "All possible requests that a client can send to an agent.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from client to agent.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ClientRequest::from_method_and_params`].", "anyOf": [ { "title": "InitializeRequest", @@ -6007,7 +6007,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "All possible notifications that a client can send to an agent.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.", + "description": "All possible notifications that a client can send to an agent.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ClientNotification::from_method_and_params`].", "anyOf": [ { "title": "CancelNotification", diff --git a/schema/v1/schema.unstable.json b/schema/v1/schema.unstable.json index 658800aea..cac6e4696 100644 --- a/schema/v1/schema.unstable.json +++ b/schema/v1/schema.unstable.json @@ -95,7 +95,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "General protocol-level notifications that all sides are expected to\nimplement.\n\nNotifications whose methods start with '$/' are messages which\nare protocol implementation dependent and might not be implementable in all\nclients or agents. For example if the implementation uses a single threaded\nsynchronous programming language then there is little it can do to react to\na `$/cancel_request` notification. If an agent or client receives\nnotifications starting with '$/' it is free to ignore the notification.\n\nNotifications do not expect a response.", + "description": "General protocol-level notifications that all sides are expected to\nimplement.\n\nNotifications whose methods start with '$/' are messages which\nare protocol implementation dependent and might not be implementable in all\nclients or agents. For example if the implementation uses a single threaded\nsynchronous programming language then there is little it can do to react to\na `$/cancel_request` notification. If an agent or client receives\nnotifications starting with '$/' it is free to ignore the notification.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ProtocolLevelNotification::from_method_and_params`].", "anyOf": [ { "title": "CancelRequestNotification", @@ -177,7 +177,7 @@ "description": "Method-specific request parameters.", "anyOf": [ { - "description": "All possible requests that an agent can send to a client.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from agent to client.", + "description": "All possible requests that an agent can send to a client.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from agent to client.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`AgentRequest::from_method_and_params`].", "anyOf": [ { "title": "WriteTextFileRequest", @@ -4433,7 +4433,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "All possible notifications that an agent can send to a client.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.", + "description": "All possible notifications that an agent can send to a client.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`AgentNotification::from_method_and_params`].", "anyOf": [ { "title": "SessionNotification", @@ -6241,7 +6241,7 @@ "description": "Method-specific request parameters.", "anyOf": [ { - "description": "All possible requests that a client can send to an agent.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from client to agent.", + "description": "All possible requests that a client can send to an agent.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from client to agent.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ClientRequest::from_method_and_params`].", "anyOf": [ { "title": "InitializeRequest", @@ -8354,7 +8354,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "All possible notifications that a client can send to an agent.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.", + "description": "All possible notifications that a client can send to an agent.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ClientNotification::from_method_and_params`].", "anyOf": [ { "title": "CancelNotification", diff --git a/schema/v2/schema.json b/schema/v2/schema.json index 2643385ad..9981d1ea1 100644 --- a/schema/v2/schema.json +++ b/schema/v2/schema.json @@ -95,7 +95,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "General protocol-level notifications that all sides are expected to\nimplement.\n\nNotifications whose methods start with '$/' are messages which\nare protocol implementation dependent and might not be implementable in all\nclients or agents. For example if the implementation uses a single threaded\nsynchronous programming language then there is little it can do to react to\na `$/cancel_request` notification. If an agent or client receives\nnotifications starting with '$/' it is free to ignore the notification.\n\nNotifications do not expect a response.", + "description": "General protocol-level notifications that all sides are expected to\nimplement.\n\nNotifications whose methods start with '$/' are messages which\nare protocol implementation dependent and might not be implementable in all\nclients or agents. For example if the implementation uses a single threaded\nsynchronous programming language then there is little it can do to react to\na `$/cancel_request` notification. If an agent or client receives\nnotifications starting with '$/' it is free to ignore the notification.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ProtocolLevelNotification::from_method_and_params`].", "anyOf": [ { "title": "CancelRequestNotification", @@ -139,7 +139,7 @@ "description": "Method-specific request parameters.", "anyOf": [ { - "description": "All possible requests that an agent can send to a client.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from agent to client.", + "description": "All possible requests that an agent can send to a client.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from agent to client.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`AgentRequest::from_method_and_params`].", "anyOf": [ { "title": "RequestPermissionRequest", @@ -4252,7 +4252,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "All possible notifications that an agent can send to a client.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.", + "description": "All possible notifications that an agent can send to a client.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`AgentNotification::from_method_and_params`].", "anyOf": [ { "title": "UpdateSessionNotification", @@ -6192,7 +6192,7 @@ "description": "Method-specific request parameters.", "anyOf": [ { - "description": "All possible requests that a client can send to an agent.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from client to agent.", + "description": "All possible requests that a client can send to an agent.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from client to agent.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ClientRequest::from_method_and_params`].", "anyOf": [ { "title": "InitializeRequest", @@ -7521,7 +7521,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "All possible notifications that a client can send to an agent.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.", + "description": "All possible notifications that a client can send to an agent.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ClientNotification::from_method_and_params`].", "anyOf": [ { "title": "CancelSessionNotification", diff --git a/schema/v2/schema.unstable.json b/schema/v2/schema.unstable.json index fb1df7475..49822fe58 100644 --- a/schema/v2/schema.unstable.json +++ b/schema/v2/schema.unstable.json @@ -95,7 +95,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "General protocol-level notifications that all sides are expected to\nimplement.\n\nNotifications whose methods start with '$/' are messages which\nare protocol implementation dependent and might not be implementable in all\nclients or agents. For example if the implementation uses a single threaded\nsynchronous programming language then there is little it can do to react to\na `$/cancel_request` notification. If an agent or client receives\nnotifications starting with '$/' it is free to ignore the notification.\n\nNotifications do not expect a response.", + "description": "General protocol-level notifications that all sides are expected to\nimplement.\n\nNotifications whose methods start with '$/' are messages which\nare protocol implementation dependent and might not be implementable in all\nclients or agents. For example if the implementation uses a single threaded\nsynchronous programming language then there is little it can do to react to\na `$/cancel_request` notification. If an agent or client receives\nnotifications starting with '$/' it is free to ignore the notification.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ProtocolLevelNotification::from_method_and_params`].", "anyOf": [ { "title": "CancelRequestNotification", @@ -177,7 +177,7 @@ "description": "Method-specific request parameters.", "anyOf": [ { - "description": "All possible requests that an agent can send to a client.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from agent to client.", + "description": "All possible requests that an agent can send to a client.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from agent to client.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`AgentRequest::from_method_and_params`].", "anyOf": [ { "title": "RequestPermissionRequest", @@ -5691,7 +5691,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "All possible notifications that an agent can send to a client.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.", + "description": "All possible notifications that an agent can send to a client.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`AgentNotification::from_method_and_params`].", "anyOf": [ { "title": "UpdateSessionNotification", @@ -7657,7 +7657,7 @@ "description": "Method-specific request parameters.", "anyOf": [ { - "description": "All possible requests that a client can send to an agent.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from client to agent.", + "description": "All possible requests that a client can send to an agent.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly.\n\nThis enum encompasses all method calls from client to agent.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ClientRequest::from_method_and_params`].", "anyOf": [ { "title": "InitializeRequest", @@ -9547,7 +9547,7 @@ "description": "Method-specific notification parameters.", "anyOf": [ { - "description": "All possible notifications that a client can send to an agent.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.", + "description": "All possible notifications that a client can send to an agent.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly.\n\nNotifications do not expect a response.\n\n# Deserialization\n\nDo not parse incoming messages with the derived untagged `Deserialize`\n(it exists for schema generation): variants are tried in order, so\nall-optional variants silently swallow any payload. Use\n[`ClientNotification::from_method_and_params`].", "anyOf": [ { "title": "CancelSessionNotification",