#![allow(missing_docs)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use std::io::Write;
use tokio::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub enum PermissionMode {
Default,
AcceptEdits,
Plan,
BypassPermissions,
DontAsk,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Effort {
Low,
Medium,
High,
Max,
}
impl std::fmt::Display for Effort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Effort::Low => write!(f, "low"),
Effort::Medium => write!(f, "medium"),
Effort::High => write!(f, "high"),
Effort::Max => write!(f, "max"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RateLimitStatus {
Allowed,
AllowedWarning,
Rejected,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RateLimitType {
#[serde(rename = "five_hour")]
FiveHour,
#[serde(rename = "seven_day")]
SevenDay,
#[serde(rename = "seven_day_opus")]
SevenDayOpus,
#[serde(rename = "seven_day_sonnet")]
SevenDaySonnet,
#[serde(rename = "overage")]
Overage,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RateLimitInfo {
pub status: RateLimitStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resets_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rate_limit_type: Option<RateLimitType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub utilization: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overage_status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overage_resets_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overage_disabled_reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AssistantMessageError {
AuthenticationFailed,
BillingError,
RateLimit,
InvalidRequest,
ServerError,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SdkBeta {
#[serde(rename = "context-1m-2025-08-07")]
Context1M,
}
impl std::fmt::Display for SdkBeta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SdkBeta::Context1M => write!(f, "context-1m-2025-08-07"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolsConfig {
List(Vec<String>),
Preset(ToolsPreset),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolsPreset {
#[serde(rename = "type")]
pub preset_type: String,
pub preset: String,
}
impl ToolsConfig {
pub fn list(tools: Vec<String>) -> Self {
ToolsConfig::List(tools)
}
pub fn none() -> Self {
ToolsConfig::List(vec![])
}
pub fn claude_code_preset() -> Self {
ToolsConfig::Preset(ToolsPreset {
preset_type: "preset".to_string(),
preset: "claude_code".to_string(),
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxNetworkConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_unix_sockets: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_all_unix_sockets: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_local_binding: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub http_proxy_port: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub socks_proxy_port: Option<u16>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxIgnoreViolations {
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_allow_bash_if_sandboxed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub excluded_commands: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_unsandboxed_commands: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<SandboxNetworkConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_violations: Option<SandboxIgnoreViolations>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_weaker_nested_sandbox: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum SdkPluginConfig {
Local {
path: String,
},
}
impl Default for PermissionMode {
fn default() -> Self {
Self::Default
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlProtocolFormat {
Legacy,
Control,
Auto,
}
impl Default for ControlProtocolFormat {
fn default() -> Self {
Self::Legacy
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum McpConnectionStatus {
Connected,
Failed,
NeedsAuth,
Pending,
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolAnnotations {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub read_only: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub destructive: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub open_world: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolInfo {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub annotations: Option<McpToolAnnotations>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerInfo {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerStatus {
pub name: String,
pub status: McpConnectionStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub server_info: Option<McpServerInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<McpToolInfo>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ThinkingConfig {
Adaptive,
Enabled {
budget_tokens: i32,
},
Disabled,
}
#[derive(Clone)]
pub enum McpServerConfig {
Stdio {
command: String,
args: Option<Vec<String>>,
env: Option<HashMap<String, String>>,
},
Sse {
url: String,
headers: Option<HashMap<String, String>>,
},
Http {
url: String,
headers: Option<HashMap<String, String>>,
},
Sdk {
name: String,
instance: Arc<dyn std::any::Any + Send + Sync>,
},
}
impl std::fmt::Debug for McpServerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Stdio { command, args, env } => f
.debug_struct("Stdio")
.field("command", command)
.field("args", args)
.field("env", env)
.finish(),
Self::Sse { url, headers } => f
.debug_struct("Sse")
.field("url", url)
.field("headers", headers)
.finish(),
Self::Http { url, headers } => f
.debug_struct("Http")
.field("url", url)
.field("headers", headers)
.finish(),
Self::Sdk { name, .. } => f
.debug_struct("Sdk")
.field("name", name)
.field("instance", &"<Arc<dyn Any>>")
.finish(),
}
}
}
impl Serialize for McpServerConfig {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(None)?;
match self {
Self::Stdio { command, args, env } => {
map.serialize_entry("type", "stdio")?;
map.serialize_entry("command", command)?;
if let Some(args) = args {
map.serialize_entry("args", args)?;
}
if let Some(env) = env {
map.serialize_entry("env", env)?;
}
}
Self::Sse { url, headers } => {
map.serialize_entry("type", "sse")?;
map.serialize_entry("url", url)?;
if let Some(headers) = headers {
map.serialize_entry("headers", headers)?;
}
}
Self::Http { url, headers } => {
map.serialize_entry("type", "http")?;
map.serialize_entry("url", url)?;
if let Some(headers) = headers {
map.serialize_entry("headers", headers)?;
}
}
Self::Sdk { name, .. } => {
map.serialize_entry("type", "sdk")?;
map.serialize_entry("name", name)?;
}
}
map.end()
}
}
impl<'de> Deserialize<'de> for McpServerConfig {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
enum McpServerConfigHelper {
Stdio {
command: String,
#[serde(skip_serializing_if = "Option::is_none")]
args: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
env: Option<HashMap<String, String>>,
},
Sse {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<HashMap<String, String>>,
},
Http {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<HashMap<String, String>>,
},
}
let helper = McpServerConfigHelper::deserialize(deserializer)?;
Ok(match helper {
McpServerConfigHelper::Stdio { command, args, env } => {
McpServerConfig::Stdio { command, args, env }
}
McpServerConfigHelper::Sse { url, headers } => {
McpServerConfig::Sse { url, headers }
}
McpServerConfigHelper::Http { url, headers } => {
McpServerConfig::Http { url, headers }
}
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PermissionUpdateDestination {
UserSettings,
ProjectSettings,
LocalSettings,
Session,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PermissionBehavior {
Allow,
Deny,
Ask,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRuleValue {
pub tool_name: String,
pub rule_content: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PermissionUpdateType {
AddRules,
ReplaceRules,
RemoveRules,
SetMode,
AddDirectories,
RemoveDirectories,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionUpdate {
#[serde(rename = "type")]
pub update_type: PermissionUpdateType,
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<Vec<PermissionRuleValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub behavior: Option<PermissionBehavior>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<PermissionMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub directories: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<PermissionUpdateDestination>,
}
#[derive(Debug, Clone)]
pub struct ToolPermissionContext {
pub signal: Option<Arc<dyn std::any::Any + Send + Sync>>,
pub suggestions: Vec<PermissionUpdate>,
}
#[derive(Debug, Clone)]
pub struct PermissionResultAllow {
pub updated_input: Option<serde_json::Value>,
pub updated_permissions: Option<Vec<PermissionUpdate>>,
}
#[derive(Debug, Clone)]
pub struct PermissionResultDeny {
pub message: String,
pub interrupt: bool,
}
#[derive(Debug, Clone)]
pub enum PermissionResult {
Allow(PermissionResultAllow),
Deny(PermissionResultDeny),
}
#[async_trait]
pub trait CanUseTool: Send + Sync {
async fn can_use_tool(
&self,
tool_name: &str,
input: &serde_json::Value,
context: &ToolPermissionContext,
) -> PermissionResult;
}
#[derive(Debug, Clone)]
pub struct HookContext {
pub signal: Option<Arc<dyn std::any::Any + Send + Sync>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaseHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreToolUseHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub tool_name: String,
pub tool_input: serde_json::Value,
pub tool_use_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostToolUseHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub tool_name: String,
pub tool_input: serde_json::Value,
pub tool_response: serde_json::Value,
pub tool_use_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserPromptSubmitHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub prompt: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StopHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub stop_hook_active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubagentStopHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub stop_hook_active: bool,
pub agent_id: String,
pub agent_transcript_path: String,
pub agent_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreCompactHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub trigger: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_instructions: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostToolUseFailureHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub tool_name: String,
pub tool_input: serde_json::Value,
pub tool_use_id: String,
pub error: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_interrupt: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub notification_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubagentStartHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub agent_id: String,
pub agent_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRequestHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_mode: Option<String>,
pub tool_name: String,
pub tool_input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub permission_suggestions: Option<Vec<serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(tag = "hook_event_name")]
pub enum HookInput {
#[serde(rename = "PreToolUse")]
PreToolUse(PreToolUseHookInput),
#[serde(rename = "PostToolUse")]
PostToolUse(PostToolUseHookInput),
#[serde(rename = "PostToolUseFailure")]
PostToolUseFailure(PostToolUseFailureHookInput),
#[serde(rename = "UserPromptSubmit")]
UserPromptSubmit(UserPromptSubmitHookInput),
#[serde(rename = "Stop")]
Stop(StopHookInput),
#[serde(rename = "SubagentStop")]
SubagentStop(SubagentStopHookInput),
#[serde(rename = "PreCompact")]
PreCompact(PreCompactHookInput),
#[serde(rename = "Notification")]
Notification(NotificationHookInput),
#[serde(rename = "SubagentStart")]
SubagentStart(SubagentStartHookInput),
#[serde(rename = "PermissionRequest")]
PermissionRequest(PermissionRequestHookInput),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AsyncHookJSONOutput {
#[serde(rename = "async")]
pub async_: bool,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "asyncTimeout")]
pub async_timeout: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SyncHookJSONOutput {
#[serde(rename = "continue", skip_serializing_if = "Option::is_none")]
pub continue_: Option<bool>,
#[serde(rename = "suppressOutput", skip_serializing_if = "Option::is_none")]
pub suppress_output: Option<bool>,
#[serde(rename = "stopReason", skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub decision: Option<String>, #[serde(rename = "systemMessage", skip_serializing_if = "Option::is_none")]
pub system_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(rename = "hookSpecificOutput", skip_serializing_if = "Option::is_none")]
pub hook_specific_output: Option<HookSpecificOutput>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum HookJSONOutput {
Async(AsyncHookJSONOutput),
Sync(SyncHookJSONOutput),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreToolUseHookSpecificOutput {
#[serde(rename = "permissionDecision", skip_serializing_if = "Option::is_none")]
pub permission_decision: Option<String>,
#[serde(rename = "permissionDecisionReason", skip_serializing_if = "Option::is_none")]
pub permission_decision_reason: Option<String>,
#[serde(rename = "updatedInput", skip_serializing_if = "Option::is_none")]
pub updated_input: Option<serde_json::Value>,
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostToolUseHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
#[serde(rename = "updatedMCPToolOutput", skip_serializing_if = "Option::is_none")]
pub updated_mcp_tool_output: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserPromptSubmitHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionStartHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostToolUseFailureHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubagentStartHookSpecificOutput {
#[serde(rename = "additionalContext", skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRequestHookSpecificOutput {
pub decision: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "hookEventName")]
pub enum HookSpecificOutput {
#[serde(rename = "PreToolUse")]
PreToolUse(PreToolUseHookSpecificOutput),
#[serde(rename = "PostToolUse")]
PostToolUse(PostToolUseHookSpecificOutput),
#[serde(rename = "PostToolUseFailure")]
PostToolUseFailure(PostToolUseFailureHookSpecificOutput),
#[serde(rename = "UserPromptSubmit")]
UserPromptSubmit(UserPromptSubmitHookSpecificOutput),
#[serde(rename = "SessionStart")]
SessionStart(SessionStartHookSpecificOutput),
#[serde(rename = "Notification")]
Notification(NotificationHookSpecificOutput),
#[serde(rename = "SubagentStart")]
SubagentStart(SubagentStartHookSpecificOutput),
#[serde(rename = "PermissionRequest")]
PermissionRequest(PermissionRequestHookSpecificOutput),
}
#[async_trait]
pub trait HookCallback: Send + Sync {
async fn execute(
&self,
input: &HookInput,
tool_use_id: Option<&str>,
context: &HookContext,
) -> Result<HookJSONOutput, crate::errors::SdkError>;
}
#[deprecated(
since = "0.3.0",
note = "Use the new HookCallback trait with HookInput/HookJSONOutput instead"
)]
#[allow(dead_code)]
#[async_trait]
pub trait HookCallbackLegacy: Send + Sync {
async fn execute_legacy(
&self,
input: &serde_json::Value,
tool_use_id: Option<&str>,
context: &HookContext,
) -> serde_json::Value;
}
#[derive(Clone)]
pub struct HookMatcher {
pub matcher: Option<serde_json::Value>,
pub hooks: Vec<Arc<dyn HookCallback>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SettingSource {
User,
Project,
Local,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDefinition {
pub description: String,
pub prompt: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "disallowedTools")]
pub disallowed_tools: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skills: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "mcpServers")]
pub mcp_servers: Option<Vec<serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "initialPrompt")]
pub initial_prompt: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "maxTurns")]
pub max_turns: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub background: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effort: Option<Effort>,
#[serde(default, skip_serializing_if = "Option::is_none", rename = "permissionMode")]
pub permission_mode: Option<PermissionMode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SystemPrompt {
String(String),
Preset {
#[serde(rename = "type")]
preset_type: String, preset: String, #[serde(skip_serializing_if = "Option::is_none")]
append: Option<String>,
},
}
#[derive(Clone, Default)]
pub struct ClaudeCodeOptions {
pub system_prompt_v2: Option<SystemPrompt>,
#[deprecated(since = "0.1.12", note = "Use system_prompt_v2 instead")]
pub system_prompt: Option<String>,
#[deprecated(since = "0.1.12", note = "Use system_prompt_v2 instead")]
pub append_system_prompt: Option<String>,
pub allowed_tools: Vec<String>,
pub disallowed_tools: Vec<String>,
pub permission_mode: PermissionMode,
pub mcp_servers: HashMap<String, McpServerConfig>,
pub mcp_tools: Vec<String>,
pub max_turns: Option<i32>,
pub max_thinking_tokens: Option<i32>,
pub max_output_tokens: Option<u32>,
pub model: Option<String>,
pub cwd: Option<PathBuf>,
pub continue_conversation: bool,
pub resume: Option<String>,
pub permission_prompt_tool_name: Option<String>,
pub settings: Option<String>,
pub add_dirs: Vec<PathBuf>,
pub extra_args: HashMap<String, Option<String>>,
pub env: HashMap<String, String>,
pub debug_stderr: Option<Arc<Mutex<dyn Write + Send + Sync>>>,
pub include_partial_messages: bool,
pub can_use_tool: Option<Arc<dyn CanUseTool>>,
pub hooks: Option<HashMap<String, Vec<HookMatcher>>>,
pub control_protocol_format: ControlProtocolFormat,
pub setting_sources: Option<Vec<SettingSource>>,
pub fork_session: bool,
pub agents: Option<HashMap<String, AgentDefinition>>,
pub cli_channel_buffer_size: Option<usize>,
pub tools: Option<ToolsConfig>,
pub betas: Vec<SdkBeta>,
pub max_budget_usd: Option<f64>,
pub fallback_model: Option<String>,
pub output_format: Option<serde_json::Value>,
pub enable_file_checkpointing: bool,
pub sandbox: Option<SandboxSettings>,
pub plugins: Vec<SdkPluginConfig>,
pub user: Option<String>,
pub stderr_callback: Option<Arc<dyn Fn(&str) + Send + Sync>>,
pub auto_download_cli: bool,
pub effort: Option<Effort>,
pub thinking: Option<ThinkingConfig>,
pub session_id: Option<String>,
}
impl std::fmt::Debug for ClaudeCodeOptions {
#[allow(deprecated)]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClaudeCodeOptions")
.field("system_prompt", &self.system_prompt)
.field("append_system_prompt", &self.append_system_prompt)
.field("allowed_tools", &self.allowed_tools)
.field("disallowed_tools", &self.disallowed_tools)
.field("permission_mode", &self.permission_mode)
.field("mcp_servers", &self.mcp_servers)
.field("mcp_tools", &self.mcp_tools)
.field("max_turns", &self.max_turns)
.field("max_thinking_tokens", &self.max_thinking_tokens)
.field("max_output_tokens", &self.max_output_tokens)
.field("model", &self.model)
.field("cwd", &self.cwd)
.field("continue_conversation", &self.continue_conversation)
.field("resume", &self.resume)
.field("permission_prompt_tool_name", &self.permission_prompt_tool_name)
.field("settings", &self.settings)
.field("add_dirs", &self.add_dirs)
.field("extra_args", &self.extra_args)
.field("env", &self.env)
.field("debug_stderr", &self.debug_stderr.is_some())
.field("include_partial_messages", &self.include_partial_messages)
.field("can_use_tool", &self.can_use_tool.is_some())
.field("hooks", &self.hooks.is_some())
.field("control_protocol_format", &self.control_protocol_format)
.field("effort", &self.effort)
.field("thinking", &self.thinking)
.field("session_id", &self.session_id)
.finish()
}
}
impl ClaudeCodeOptions {
pub fn builder() -> ClaudeCodeOptionsBuilder {
ClaudeCodeOptionsBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct ClaudeCodeOptionsBuilder {
options: ClaudeCodeOptions,
}
impl ClaudeCodeOptionsBuilder {
#[allow(deprecated)]
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.options.system_prompt = Some(prompt.into());
self
}
#[allow(deprecated)]
pub fn append_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.options.append_system_prompt = Some(prompt.into());
self
}
pub fn allowed_tools(mut self, tools: Vec<String>) -> Self {
self.options.allowed_tools = tools;
self
}
pub fn allow_tool(mut self, tool: impl Into<String>) -> Self {
self.options.allowed_tools.push(tool.into());
self
}
pub fn disallowed_tools(mut self, tools: Vec<String>) -> Self {
self.options.disallowed_tools = tools;
self
}
pub fn disallow_tool(mut self, tool: impl Into<String>) -> Self {
self.options.disallowed_tools.push(tool.into());
self
}
pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
self.options.permission_mode = mode;
self
}
pub fn add_mcp_server(mut self, name: impl Into<String>, config: McpServerConfig) -> Self {
self.options.mcp_servers.insert(name.into(), config);
self
}
pub fn mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
self.options.mcp_servers = servers;
self
}
pub fn mcp_tools(mut self, tools: Vec<String>) -> Self {
self.options.mcp_tools = tools;
self
}
pub fn max_turns(mut self, turns: i32) -> Self {
self.options.max_turns = Some(turns);
self
}
pub fn max_thinking_tokens(mut self, tokens: i32) -> Self {
self.options.max_thinking_tokens = Some(tokens);
self
}
pub fn max_output_tokens(mut self, tokens: u32) -> Self {
self.options.max_output_tokens = Some(tokens.clamp(1, 32000));
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.options.model = Some(model.into());
self
}
pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
self.options.cwd = Some(path.into());
self
}
pub fn continue_conversation(mut self, enable: bool) -> Self {
self.options.continue_conversation = enable;
self
}
pub fn resume(mut self, id: impl Into<String>) -> Self {
self.options.resume = Some(id.into());
self
}
pub fn session_id(mut self, id: impl Into<String>) -> Self {
self.options.session_id = Some(id.into());
self
}
pub fn permission_prompt_tool_name(mut self, name: impl Into<String>) -> Self {
self.options.permission_prompt_tool_name = Some(name.into());
self
}
pub fn settings(mut self, settings: impl Into<String>) -> Self {
self.options.settings = Some(settings.into());
self
}
pub fn add_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
self.options.add_dirs = dirs;
self
}
pub fn add_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.options.add_dirs.push(dir.into());
self
}
pub fn extra_args(mut self, args: HashMap<String, Option<String>>) -> Self {
self.options.extra_args = args;
self
}
pub fn add_extra_arg(mut self, key: impl Into<String>, value: Option<String>) -> Self {
self.options.extra_args.insert(key.into(), value);
self
}
pub fn control_protocol_format(mut self, format: ControlProtocolFormat) -> Self {
self.options.control_protocol_format = format;
self
}
pub fn include_partial_messages(mut self, include: bool) -> Self {
self.options.include_partial_messages = include;
self
}
pub fn fork_session(mut self, fork: bool) -> Self {
self.options.fork_session = fork;
self
}
pub fn setting_sources(mut self, sources: Vec<SettingSource>) -> Self {
self.options.setting_sources = Some(sources);
self
}
pub fn agents(mut self, agents: HashMap<String, AgentDefinition>) -> Self {
self.options.agents = Some(agents);
self
}
pub fn cli_channel_buffer_size(mut self, size: usize) -> Self {
self.options.cli_channel_buffer_size = Some(size);
self
}
pub fn tools(mut self, config: ToolsConfig) -> Self {
self.options.tools = Some(config);
self
}
pub fn betas(mut self, betas: Vec<SdkBeta>) -> Self {
self.options.betas = betas;
self
}
pub fn add_beta(mut self, beta: SdkBeta) -> Self {
self.options.betas.push(beta);
self
}
pub fn max_budget_usd(mut self, budget: f64) -> Self {
self.options.max_budget_usd = Some(budget);
self
}
pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
self.options.fallback_model = Some(model.into());
self
}
pub fn output_format(mut self, format: serde_json::Value) -> Self {
self.options.output_format = Some(format);
self
}
pub fn enable_file_checkpointing(mut self, enable: bool) -> Self {
self.options.enable_file_checkpointing = enable;
self
}
pub fn sandbox(mut self, settings: SandboxSettings) -> Self {
self.options.sandbox = Some(settings);
self
}
pub fn plugins(mut self, plugins: Vec<SdkPluginConfig>) -> Self {
self.options.plugins = plugins;
self
}
pub fn add_plugin(mut self, plugin: SdkPluginConfig) -> Self {
self.options.plugins.push(plugin);
self
}
pub fn user(mut self, user: impl Into<String>) -> Self {
self.options.user = Some(user.into());
self
}
pub fn stderr_callback(mut self, callback: Arc<dyn Fn(&str) + Send + Sync>) -> Self {
self.options.stderr_callback = Some(callback);
self
}
pub fn auto_download_cli(mut self, enable: bool) -> Self {
self.options.auto_download_cli = enable;
self
}
pub fn effort(mut self, effort: Effort) -> Self {
self.options.effort = Some(effort);
self
}
pub fn thinking(mut self, config: ThinkingConfig) -> Self {
self.options.thinking = Some(config);
self
}
pub fn build(self) -> ClaudeCodeOptions {
self.options
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct TaskUsage {
#[serde(default)]
pub total_tokens: u64,
#[serde(default)]
pub tool_uses: u64,
#[serde(default)]
pub duration_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TaskStatus {
Completed,
Failed,
Stopped,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskStartedMessage {
pub task_id: String,
pub description: String,
pub uuid: String,
pub session_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_use_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskProgressMessage {
pub task_id: String,
pub description: String,
#[serde(default)]
pub usage: TaskUsage,
pub uuid: String,
pub session_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_use_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_tool_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TaskNotificationMessage {
pub task_id: String,
pub status: TaskStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
pub uuid: String,
pub session_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_use_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<TaskUsage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[non_exhaustive]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Message {
User {
message: UserMessage,
},
Assistant {
message: AssistantMessage,
},
System {
subtype: String,
data: serde_json::Value,
},
Result {
subtype: String,
duration_ms: i64,
duration_api_ms: i64,
is_error: bool,
num_turns: i32,
session_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
total_cost_usd: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
usage: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", alias = "structuredOutput")]
structured_output: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
stop_reason: Option<String>,
},
#[serde(rename = "stream_event")]
StreamEvent {
uuid: String,
session_id: String,
event: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
parent_tool_use_id: Option<String>,
},
#[serde(rename = "rate_limit")]
RateLimit {
rate_limit_info: RateLimitInfo,
uuid: String,
session_id: String,
},
#[serde(skip)]
Unknown {
msg_type: String,
raw: serde_json::Value,
},
}
impl Message {
pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
if let Message::System { subtype, data } = self {
if subtype == "task_started" {
return serde_json::from_value(data.clone()).ok();
}
}
None
}
pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
if let Message::System { subtype, data } = self {
if subtype == "task_progress" {
return serde_json::from_value(data.clone()).ok();
}
}
None
}
pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
if let Message::System { subtype, data } = self {
if subtype == "task_notification" {
return serde_json::from_value(data.clone()).ok();
}
}
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UserMessage {
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AssistantMessage {
pub content: Vec<ContentBlock>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<AssistantMessageError>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_tool_use_id: Option<String>,
}
pub use Message::Result as ResultMessage;
pub use Message::System as SystemMessage;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ContentBlock {
Text(TextContent),
Thinking(ThinkingContent),
ToolUse(ToolUseContent),
ToolResult(ToolResultContent),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TextContent {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ThinkingContent {
pub thinking: String,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolUseContent {
pub id: String,
pub name: String,
pub input: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolResultContent {
pub tool_use_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<ContentValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ContentValue {
Text(String),
Structured(Vec<serde_json::Value>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserContent {
pub role: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantContent {
pub role: String,
pub content: Vec<ContentBlock>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlInterruptRequest {
pub subtype: String, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlPermissionRequest {
pub subtype: String, #[serde(alias = "toolName")]
pub tool_name: String,
pub input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none", alias = "permissionSuggestions")]
pub permission_suggestions: Option<Vec<PermissionUpdate>>,
#[serde(skip_serializing_if = "Option::is_none", alias = "blockedPath")]
pub blocked_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlInitializeRequest {
pub subtype: String, #[serde(skip_serializing_if = "Option::is_none")]
pub hooks: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SDKControlSetPermissionModeRequest {
pub subtype: String, pub mode: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SDKControlSetModelRequest {
pub subtype: String, #[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKHookCallbackRequest {
pub subtype: String, #[serde(alias = "callbackId")]
pub callback_id: String,
pub input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none", alias = "toolUseId")]
pub tool_use_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlMcpMessageRequest {
pub subtype: String, #[serde(rename = "server_name", alias = "mcpServerName", alias = "mcp_server_name")]
pub mcp_server_name: String,
pub message: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlRewindFilesRequest {
pub subtype: String,
#[serde(alias = "userMessageId")]
pub user_message_id: String,
}
impl SDKControlRewindFilesRequest {
pub fn new(user_message_id: impl Into<String>) -> Self {
Self {
subtype: "rewind_files".to_string(),
user_message_id: user_message_id.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlGetContextUsageRequest {
pub subtype: String,
}
impl SDKControlGetContextUsageRequest {
pub fn new() -> Self {
Self {
subtype: "get_context_usage".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlStopTaskRequest {
pub subtype: String,
#[serde(alias = "taskId")]
pub task_id: String,
}
impl SDKControlStopTaskRequest {
pub fn new(task_id: impl Into<String>) -> Self {
Self {
subtype: "stop_task".to_string(),
task_id: task_id.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlMcpStatusRequest {
pub subtype: String,
}
impl SDKControlMcpStatusRequest {
pub fn new() -> Self {
Self {
subtype: "mcp_status".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlMcpReconnectRequest {
pub subtype: String,
#[serde(alias = "serverName")]
pub server_name: String,
}
impl SDKControlMcpReconnectRequest {
pub fn new(server_name: impl Into<String>) -> Self {
Self {
subtype: "mcp_reconnect".to_string(),
server_name: server_name.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SDKControlMcpToggleRequest {
pub subtype: String,
#[serde(alias = "serverName")]
pub server_name: String,
pub enabled: bool,
}
impl SDKControlMcpToggleRequest {
pub fn new(server_name: impl Into<String>, enabled: bool) -> Self {
Self {
subtype: "mcp_toggle".to_string(),
server_name: server_name.into(),
enabled,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContextUsageCategory {
pub name: String,
pub token_count: u64,
#[serde(default)]
pub percentage: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiUsage {
#[serde(default)]
pub input_tokens: u64,
#[serde(default)]
pub output_tokens: u64,
#[serde(default)]
pub cache_read_input_tokens: u64,
#[serde(default)]
pub cache_creation_input_tokens: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContextUsageResponse {
#[serde(default)]
pub categories: Vec<ContextUsageCategory>,
#[serde(default)]
pub total_tokens: u64,
#[serde(default)]
pub max_tokens: u64,
#[serde(default)]
pub percentage: f64,
#[serde(default)]
pub model: String,
#[serde(default)]
pub is_auto_compact_enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_compact_threshold: Option<u64>,
#[serde(default)]
pub memory_files: Vec<serde_json::Value>,
#[serde(default)]
pub mcp_tools: Vec<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message_breakdown: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_usage: Option<ApiUsage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskBudget {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_cost_usd: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_turns: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ForkSessionResult {
pub session_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SDKControlRequest {
#[serde(rename = "interrupt")]
Interrupt(SDKControlInterruptRequest),
#[serde(rename = "can_use_tool")]
CanUseTool(SDKControlPermissionRequest),
#[serde(rename = "initialize")]
Initialize(SDKControlInitializeRequest),
#[serde(rename = "set_permission_mode")]
SetPermissionMode(SDKControlSetPermissionModeRequest),
#[serde(rename = "set_model")]
SetModel(SDKControlSetModelRequest),
#[serde(rename = "hook_callback")]
HookCallback(SDKHookCallbackRequest),
#[serde(rename = "mcp_message")]
McpMessage(SDKControlMcpMessageRequest),
#[serde(rename = "rewind_files")]
RewindFiles(SDKControlRewindFilesRequest),
#[serde(rename = "get_context_usage")]
GetContextUsage(SDKControlGetContextUsageRequest),
#[serde(rename = "stop_task")]
StopTask(SDKControlStopTaskRequest),
#[serde(rename = "mcp_status")]
McpStatus(SDKControlMcpStatusRequest),
#[serde(rename = "mcp_reconnect")]
McpReconnect(SDKControlMcpReconnectRequest),
#[serde(rename = "mcp_toggle")]
McpToggle(SDKControlMcpToggleRequest),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ControlRequest {
Interrupt {
request_id: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ControlResponse {
InterruptAck {
request_id: String,
success: bool,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_mode_serialization() {
let mode = PermissionMode::AcceptEdits;
let json = serde_json::to_string(&mode).unwrap();
assert_eq!(json, r#""acceptEdits""#);
let deserialized: PermissionMode = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, mode);
let plan_mode = PermissionMode::Plan;
let plan_json = serde_json::to_string(&plan_mode).unwrap();
assert_eq!(plan_json, r#""plan""#);
let plan_deserialized: PermissionMode = serde_json::from_str(&plan_json).unwrap();
assert_eq!(plan_deserialized, plan_mode);
}
#[test]
fn test_message_serialization() {
let msg = Message::User {
message: UserMessage {
content: "Hello".to_string(),
},
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains(r#""type":"user""#));
assert!(json.contains(r#""content":"Hello""#));
let deserialized: Message = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, msg);
}
#[test]
#[allow(deprecated)]
fn test_options_builder() {
let options = ClaudeCodeOptions::builder()
.system_prompt("Test prompt")
.model("claude-3-opus")
.permission_mode(PermissionMode::AcceptEdits)
.allow_tool("read")
.allow_tool("write")
.max_turns(10)
.build();
assert_eq!(options.system_prompt, Some("Test prompt".to_string()));
assert_eq!(options.model, Some("claude-3-opus".to_string()));
assert_eq!(options.permission_mode, PermissionMode::AcceptEdits);
assert_eq!(options.allowed_tools, vec!["read", "write"]);
assert_eq!(options.max_turns, Some(10));
}
#[test]
fn test_extra_args() {
let mut extra_args = HashMap::new();
extra_args.insert("custom-flag".to_string(), Some("value".to_string()));
extra_args.insert("boolean-flag".to_string(), None);
let options = ClaudeCodeOptions::builder()
.extra_args(extra_args.clone())
.add_extra_arg("another-flag", Some("another-value".to_string()))
.build();
assert_eq!(options.extra_args.len(), 3);
assert_eq!(options.extra_args.get("custom-flag"), Some(&Some("value".to_string())));
assert_eq!(options.extra_args.get("boolean-flag"), Some(&None));
assert_eq!(options.extra_args.get("another-flag"), Some(&Some("another-value".to_string())));
}
#[test]
fn test_thinking_content_serialization() {
let thinking = ThinkingContent {
thinking: "Let me think about this...".to_string(),
signature: "sig123".to_string(),
};
let json = serde_json::to_string(&thinking).unwrap();
assert!(json.contains(r#""thinking":"Let me think about this...""#));
assert!(json.contains(r#""signature":"sig123""#));
let deserialized: ThinkingContent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.thinking, thinking.thinking);
assert_eq!(deserialized.signature, thinking.signature);
}
#[test]
fn test_tools_config_list_serialization() {
let tools = ToolsConfig::List(vec!["Read".to_string(), "Write".to_string(), "Bash".to_string()]);
let json = serde_json::to_string(&tools).unwrap();
assert!(json.contains("Read"));
assert!(json.contains("Write"));
assert!(json.contains("Bash"));
let deserialized: ToolsConfig = serde_json::from_str(&json).unwrap();
match deserialized {
ToolsConfig::List(list) => {
assert_eq!(list.len(), 3);
assert!(list.contains(&"Read".to_string()));
}
_ => panic!("Expected List variant"),
}
}
#[test]
fn test_tools_config_preset_serialization() {
let preset = ToolsConfig::claude_code_preset();
let json = serde_json::to_string(&preset).unwrap();
assert!(json.contains("preset"));
assert!(json.contains("claude_code"));
let custom_preset = ToolsConfig::Preset(ToolsPreset {
preset_type: "preset".to_string(),
preset: "custom".to_string(),
});
let json = serde_json::to_string(&custom_preset).unwrap();
assert!(json.contains("custom"));
let deserialized: ToolsConfig = serde_json::from_str(&json).unwrap();
match deserialized {
ToolsConfig::Preset(p) => assert_eq!(p.preset, "custom"),
_ => panic!("Expected Preset variant"),
}
}
#[test]
fn test_tools_config_helper_methods() {
let tools = ToolsConfig::list(vec!["Read".to_string(), "Write".to_string()]);
match tools {
ToolsConfig::List(list) => assert_eq!(list.len(), 2),
_ => panic!("Expected List variant"),
}
let empty = ToolsConfig::none();
match empty {
ToolsConfig::List(list) => assert!(list.is_empty()),
_ => panic!("Expected empty List variant"),
}
let preset = ToolsConfig::claude_code_preset();
match preset {
ToolsConfig::Preset(p) => {
assert_eq!(p.preset_type, "preset");
assert_eq!(p.preset, "claude_code");
}
_ => panic!("Expected Preset variant"),
}
}
#[test]
fn test_sdk_beta_serialization() {
let beta = SdkBeta::Context1M;
let json = serde_json::to_string(&beta).unwrap();
assert_eq!(json, r#""context-1m-2025-08-07""#);
let display = format!("{}", beta);
assert_eq!(display, "context-1m-2025-08-07");
let deserialized: SdkBeta = serde_json::from_str(r#""context-1m-2025-08-07""#).unwrap();
assert!(matches!(deserialized, SdkBeta::Context1M));
}
#[test]
fn test_sandbox_settings_serialization() {
let sandbox = SandboxSettings {
enabled: Some(true),
auto_allow_bash_if_sandboxed: Some(true),
excluded_commands: Some(vec!["git".to_string(), "docker".to_string()]),
allow_unsandboxed_commands: Some(false),
network: Some(SandboxNetworkConfig {
allow_unix_sockets: Some(vec!["/tmp/ssh-agent.sock".to_string()]),
allow_all_unix_sockets: Some(false),
allow_local_binding: Some(true),
http_proxy_port: Some(8080),
socks_proxy_port: Some(1080),
}),
ignore_violations: Some(SandboxIgnoreViolations {
file: Some(vec!["/tmp".to_string(), "/var/log".to_string()]),
network: Some(vec!["localhost".to_string()]),
}),
enable_weaker_nested_sandbox: Some(false),
};
let json = serde_json::to_string(&sandbox).unwrap();
assert!(json.contains("enabled"));
assert!(json.contains("autoAllowBashIfSandboxed")); assert!(json.contains("excludedCommands"));
assert!(json.contains("httpProxyPort"));
assert!(json.contains("8080"));
let deserialized: SandboxSettings = serde_json::from_str(&json).unwrap();
assert!(deserialized.enabled.unwrap());
assert!(deserialized.network.is_some());
assert_eq!(deserialized.network.as_ref().unwrap().http_proxy_port, Some(8080));
}
#[test]
fn test_sandbox_network_config() {
let config = SandboxNetworkConfig {
allow_unix_sockets: Some(vec!["/run/user/1000/keyring/ssh".to_string()]),
allow_all_unix_sockets: Some(false),
allow_local_binding: Some(true),
http_proxy_port: Some(3128),
socks_proxy_port: Some(1080),
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: SandboxNetworkConfig = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.http_proxy_port, Some(3128));
assert_eq!(deserialized.socks_proxy_port, Some(1080));
assert_eq!(deserialized.allow_local_binding, Some(true));
}
#[test]
fn test_sandbox_ignore_violations() {
let violations = SandboxIgnoreViolations {
file: Some(vec!["/tmp".to_string(), "/var/cache".to_string()]),
network: Some(vec!["127.0.0.1".to_string(), "localhost".to_string()]),
};
let json = serde_json::to_string(&violations).unwrap();
assert!(json.contains("file"));
assert!(json.contains("/tmp"));
let deserialized: SandboxIgnoreViolations = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.file.as_ref().unwrap().len(), 2);
assert_eq!(deserialized.network.as_ref().unwrap().len(), 2);
}
#[test]
fn test_sandbox_settings_default() {
let sandbox = SandboxSettings::default();
assert!(sandbox.enabled.is_none());
assert!(sandbox.network.is_none());
assert!(sandbox.ignore_violations.is_none());
}
#[test]
fn test_sdk_plugin_config_serialization() {
let plugin = SdkPluginConfig::Local {
path: "/path/to/plugin".to_string()
};
let json = serde_json::to_string(&plugin).unwrap();
assert!(json.contains("local")); assert!(json.contains("/path/to/plugin"));
let deserialized: SdkPluginConfig = serde_json::from_str(&json).unwrap();
match deserialized {
SdkPluginConfig::Local { path } => {
assert_eq!(path, "/path/to/plugin");
}
}
}
#[test]
fn test_sdk_control_rewind_files_request() {
let request = SDKControlRewindFilesRequest {
subtype: "rewind_files".to_string(),
user_message_id: "msg_12345".to_string(),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("user_message_id"));
assert!(json.contains("msg_12345"));
assert!(json.contains("subtype"));
assert!(json.contains("rewind_files"));
let deserialized: SDKControlRewindFilesRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.user_message_id, "msg_12345");
assert_eq!(deserialized.subtype, "rewind_files");
}
#[test]
fn test_options_builder_with_new_fields() {
let options = ClaudeCodeOptions::builder()
.tools(ToolsConfig::claude_code_preset())
.add_beta(SdkBeta::Context1M)
.max_budget_usd(10.0)
.fallback_model("claude-3-haiku")
.output_format(serde_json::json!({"type": "object"}))
.enable_file_checkpointing(true)
.sandbox(SandboxSettings::default())
.add_plugin(SdkPluginConfig::Local { path: "/plugin".to_string() })
.auto_download_cli(true)
.build();
assert!(options.tools.is_some());
match options.tools.as_ref().unwrap() {
ToolsConfig::Preset(preset) => assert_eq!(preset.preset, "claude_code"),
_ => panic!("Expected Preset variant"),
}
assert_eq!(options.betas.len(), 1);
assert!(matches!(options.betas[0], SdkBeta::Context1M));
assert_eq!(options.max_budget_usd, Some(10.0));
assert_eq!(options.fallback_model, Some("claude-3-haiku".to_string()));
assert!(options.output_format.is_some());
assert!(options.enable_file_checkpointing);
assert!(options.sandbox.is_some());
assert_eq!(options.plugins.len(), 1);
assert!(options.auto_download_cli);
}
#[test]
fn test_options_builder_with_tools_list() {
let options = ClaudeCodeOptions::builder()
.tools(ToolsConfig::List(vec!["Read".to_string(), "Bash".to_string()]))
.build();
match options.tools.as_ref().unwrap() {
ToolsConfig::List(list) => {
assert_eq!(list.len(), 2);
assert!(list.contains(&"Read".to_string()));
assert!(list.contains(&"Bash".to_string()));
}
_ => panic!("Expected List variant"),
}
}
#[test]
fn test_options_builder_multiple_betas() {
let options = ClaudeCodeOptions::builder()
.add_beta(SdkBeta::Context1M)
.betas(vec![SdkBeta::Context1M])
.build();
assert_eq!(options.betas.len(), 1);
}
#[test]
fn test_options_builder_add_beta_accumulates() {
let options = ClaudeCodeOptions::builder()
.add_beta(SdkBeta::Context1M)
.add_beta(SdkBeta::Context1M)
.build();
assert_eq!(options.betas.len(), 2);
}
#[test]
fn test_options_builder_multiple_plugins() {
let options = ClaudeCodeOptions::builder()
.add_plugin(SdkPluginConfig::Local { path: "/plugin1".to_string() })
.add_plugin(SdkPluginConfig::Local { path: "/plugin2".to_string() })
.plugins(vec![SdkPluginConfig::Local { path: "/plugin3".to_string() }])
.build();
assert_eq!(options.plugins.len(), 1);
}
#[test]
fn test_options_builder_add_plugin_accumulates() {
let options = ClaudeCodeOptions::builder()
.add_plugin(SdkPluginConfig::Local { path: "/plugin1".to_string() })
.add_plugin(SdkPluginConfig::Local { path: "/plugin2".to_string() })
.add_plugin(SdkPluginConfig::Local { path: "/plugin3".to_string() })
.build();
assert_eq!(options.plugins.len(), 3);
}
#[test]
fn test_message_result_with_structured_output() {
let json = r#"{
"type": "result",
"subtype": "success",
"cost_usd": 0.05,
"duration_ms": 1500,
"duration_api_ms": 1200,
"is_error": false,
"num_turns": 3,
"session_id": "session_123",
"structured_output": {"answer": 42}
}"#;
let msg: Message = serde_json::from_str(json).unwrap();
match msg {
Message::Result {
structured_output,
..
} => {
assert!(structured_output.is_some());
let output = structured_output.unwrap();
assert_eq!(output["answer"], 42);
}
_ => panic!("Expected Result message"),
}
}
#[test]
fn test_message_result_with_structured_output_camel_case() {
let json = r#"{
"type": "result",
"subtype": "success",
"cost_usd": 0.05,
"duration_ms": 1500,
"duration_api_ms": 1200,
"is_error": false,
"num_turns": 3,
"session_id": "session_123",
"structuredOutput": {"name": "test", "value": true}
}"#;
let msg: Message = serde_json::from_str(json).unwrap();
match msg {
Message::Result {
structured_output,
..
} => {
assert!(structured_output.is_some());
let output = structured_output.unwrap();
assert_eq!(output["name"], "test");
assert_eq!(output["value"], true);
}
_ => panic!("Expected Result message"),
}
}
#[test]
fn test_default_options_new_fields() {
let options = ClaudeCodeOptions::default();
assert!(options.tools.is_none());
assert!(options.betas.is_empty());
assert!(options.max_budget_usd.is_none());
assert!(options.fallback_model.is_none());
assert!(options.output_format.is_none());
assert!(!options.enable_file_checkpointing);
assert!(options.sandbox.is_none());
assert!(options.plugins.is_empty());
assert!(options.user.is_none());
assert!(!options.auto_download_cli);
}
#[test]
fn test_permission_mode_dont_ask_serialization() {
let mode = PermissionMode::DontAsk;
let json = serde_json::to_string(&mode).unwrap();
assert_eq!(json, r#""dontAsk""#);
let deserialized: PermissionMode = serde_json::from_str(r#""dontAsk""#).unwrap();
assert_eq!(deserialized, PermissionMode::DontAsk);
}
#[test]
fn test_permission_mode_all_variants_roundtrip() {
let variants = vec![
(PermissionMode::Default, r#""default""#),
(PermissionMode::AcceptEdits, r#""acceptEdits""#),
(PermissionMode::Plan, r#""plan""#),
(PermissionMode::BypassPermissions, r#""bypassPermissions""#),
(PermissionMode::DontAsk, r#""dontAsk""#),
];
for (mode, expected_json) in variants {
let json = serde_json::to_string(&mode).unwrap();
assert_eq!(json, expected_json, "serialization failed for {:?}", mode);
let back: PermissionMode = serde_json::from_str(&json).unwrap();
assert_eq!(back, mode, "roundtrip failed for {:?}", mode);
}
}
#[test]
fn test_agent_definition_all_new_fields() {
let agent = AgentDefinition {
description: "Coder".to_string(),
prompt: "You write code".to_string(),
tools: Some(vec!["Bash".to_string()]),
disallowed_tools: Some(vec!["Write".to_string()]),
model: Some("claude-sonnet-4-20250514".to_string()),
skills: Some(vec!["tdd".to_string()]),
memory: Some("project".to_string()),
mcp_servers: None,
initial_prompt: Some("Start coding".to_string()),
max_turns: Some(50),
background: Some(true),
effort: Some(Effort::High),
permission_mode: Some(PermissionMode::DontAsk),
};
let json = serde_json::to_value(&agent).unwrap();
assert_eq!(json["disallowedTools"], serde_json::json!(["Write"]));
assert_eq!(json["initialPrompt"], "Start coding");
assert_eq!(json["maxTurns"], 50);
assert_eq!(json["background"], true);
assert_eq!(json["effort"], "high");
assert_eq!(json["permissionMode"], "dontAsk");
let back: AgentDefinition = serde_json::from_value(json).unwrap();
assert_eq!(back.disallowed_tools, Some(vec!["Write".to_string()]));
assert_eq!(back.initial_prompt, Some("Start coding".to_string()));
assert_eq!(back.max_turns, Some(50));
assert_eq!(back.background, Some(true));
assert_eq!(back.effort, Some(Effort::High));
assert_eq!(back.permission_mode, Some(PermissionMode::DontAsk));
}
#[test]
fn test_agent_definition_backward_compat_minimal() {
let json = serde_json::json!({
"description": "Agent",
"prompt": "Do stuff"
});
let agent: AgentDefinition = serde_json::from_value(json).unwrap();
assert!(agent.disallowed_tools.is_none());
assert!(agent.initial_prompt.is_none());
assert!(agent.max_turns.is_none());
assert!(agent.background.is_none());
assert!(agent.effort.is_none());
assert!(agent.permission_mode.is_none());
}
#[test]
fn test_sdk_control_get_context_usage_request() {
let req = SDKControlGetContextUsageRequest::new();
assert_eq!(req.subtype, "get_context_usage");
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["subtype"], "get_context_usage");
}
#[test]
fn test_sdk_control_stop_task_request() {
let req = SDKControlStopTaskRequest::new("task-abc-123");
assert_eq!(req.subtype, "stop_task");
assert_eq!(req.task_id, "task-abc-123");
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["task_id"], "task-abc-123");
}
#[test]
fn test_sdk_control_mcp_status_request() {
let req = SDKControlMcpStatusRequest::new();
assert_eq!(req.subtype, "mcp_status");
}
#[test]
fn test_sdk_control_mcp_reconnect_request() {
let req = SDKControlMcpReconnectRequest::new("my-server");
assert_eq!(req.subtype, "mcp_reconnect");
assert_eq!(req.server_name, "my-server");
}
#[test]
fn test_sdk_control_mcp_toggle_request() {
let req = SDKControlMcpToggleRequest::new("my-server", false);
assert_eq!(req.subtype, "mcp_toggle");
assert_eq!(req.server_name, "my-server");
assert!(!req.enabled);
}
#[test]
fn test_context_usage_response_deserialize() {
let json = serde_json::json!({
"categories": [
{"name": "system", "tokenCount": 500, "percentage": 10.0},
{"name": "conversation", "tokenCount": 3000, "percentage": 60.0}
],
"totalTokens": 5000,
"maxTokens": 200000,
"percentage": 2.5,
"model": "claude-sonnet-4-20250514",
"isAutoCompactEnabled": true,
"autoCompactThreshold": 180000,
"memoryFiles": [],
"mcpTools": [],
"apiUsage": {
"inputTokens": 4000,
"outputTokens": 1000,
"cacheReadInputTokens": 2000,
"cacheCreationInputTokens": 500
}
});
let resp: ContextUsageResponse = serde_json::from_value(json).unwrap();
assert_eq!(resp.categories.len(), 2);
assert_eq!(resp.categories[0].name, "system");
assert_eq!(resp.categories[0].token_count, 500);
assert_eq!(resp.total_tokens, 5000);
assert_eq!(resp.max_tokens, 200000);
assert_eq!(resp.model, "claude-sonnet-4-20250514");
assert!(resp.is_auto_compact_enabled);
assert_eq!(resp.auto_compact_threshold, Some(180000));
let api = resp.api_usage.unwrap();
assert_eq!(api.input_tokens, 4000);
assert_eq!(api.output_tokens, 1000);
assert_eq!(api.cache_read_input_tokens, 2000);
assert_eq!(api.cache_creation_input_tokens, 500);
}
#[test]
fn test_context_usage_response_minimal() {
let json = serde_json::json!({});
let resp: ContextUsageResponse = serde_json::from_value(json).unwrap();
assert_eq!(resp.total_tokens, 0);
assert!(resp.categories.is_empty());
assert!(resp.api_usage.is_none());
}
#[test]
fn test_task_budget_serialization() {
let budget = TaskBudget {
max_cost_usd: Some(5.0),
max_tokens: Some(100_000),
max_turns: Some(20),
};
let json = serde_json::to_value(&budget).unwrap();
assert_eq!(json["maxCostUsd"], 5.0);
assert_eq!(json["maxTokens"], 100_000);
assert_eq!(json["maxTurns"], 20);
let back: TaskBudget = serde_json::from_value(json).unwrap();
assert_eq!(back.max_cost_usd, Some(5.0));
assert_eq!(back.max_tokens, Some(100_000));
assert_eq!(back.max_turns, Some(20));
}
#[test]
fn test_fork_session_result_deserialize() {
let json = serde_json::json!({"sessionId": "sess-forked-abc"});
let result: ForkSessionResult = serde_json::from_value(json).unwrap();
assert_eq!(result.session_id, "sess-forked-abc");
}
#[test]
fn test_sdk_control_request_new_variants_serialize() {
let req = SDKControlRequest::GetContextUsage(SDKControlGetContextUsageRequest::new());
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["type"], "get_context_usage");
let req = SDKControlRequest::StopTask(SDKControlStopTaskRequest::new("t1"));
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["type"], "stop_task");
assert_eq!(json["task_id"], "t1");
let req = SDKControlRequest::McpStatus(SDKControlMcpStatusRequest::new());
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["type"], "mcp_status");
let req = SDKControlRequest::McpReconnect(SDKControlMcpReconnectRequest::new("srv"));
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["type"], "mcp_reconnect");
let req = SDKControlRequest::McpToggle(SDKControlMcpToggleRequest::new("srv", true));
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["type"], "mcp_toggle");
assert_eq!(json["enabled"], true);
}
}