Skip to main content

systemprompt_agent/models/web/
update_agent.rs

1//! The update-agent request type and its card normalization.
2//!
3//! [`UpdateAgentRequestRaw`] is the wire shape; [`UpdateAgentRequest`] derives
4//! a fully-populated [`AgentCard`](crate::models::a2a::AgentCard) from it and
5//! validates the result before it is applied to an existing agent.
6
7use crate::models::a2a::{AgentCard, AgentInterface, TransportProtocol};
8use serde::{Deserialize, Serialize};
9
10use super::card_input::AgentCardInput;
11use super::validation::{extract_port_from_url, is_valid_version, list_available_mcp_servers};
12
13#[derive(Debug, Clone, Deserialize)]
14pub struct UpdateAgentRequestRaw {
15    pub card: AgentCardInput,
16    pub is_active: Option<bool>,
17    pub system_prompt: Option<String>,
18    pub mcp_servers: Option<Vec<String>>,
19}
20
21#[derive(Debug, Clone, Serialize)]
22pub struct UpdateAgentRequest {
23    pub card: AgentCard,
24    pub is_active: Option<bool>,
25    pub system_prompt: Option<String>,
26    pub mcp_servers: Option<Vec<String>>,
27}
28
29impl<'de> Deserialize<'de> for UpdateAgentRequest {
30    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
31    where
32        D: serde::Deserializer<'de>,
33    {
34        let raw = UpdateAgentRequestRaw::deserialize(deserializer)?;
35
36        let url = raw
37            .card
38            .url
39            .unwrap_or_else(|| format!("http://placeholder/api/v1/agents/{}", raw.card.name));
40
41        let card = AgentCard {
42            name: raw.card.name,
43            description: raw.card.description,
44            supported_interfaces: vec![AgentInterface {
45                url,
46                protocol_binding: raw
47                    .card
48                    .preferred_transport
49                    .unwrap_or(TransportProtocol::JsonRpc),
50                protocol_version: raw.card.protocol_version,
51            }],
52            version: raw.card.version,
53            icon_url: None,
54            provider: None,
55            documentation_url: None,
56            capabilities: raw.card.capabilities.normalize(),
57            security_schemes: raw.card.security_schemes,
58            security: raw.card.security,
59            default_input_modes: if raw.card.default_input_modes.is_empty() {
60                vec!["text/plain".to_owned()]
61            } else {
62                raw.card.default_input_modes
63            },
64            default_output_modes: if raw.card.default_output_modes.is_empty() {
65                vec!["text/plain".to_owned()]
66            } else {
67                raw.card.default_output_modes
68            },
69            skills: raw.card.skills,
70            supports_authenticated_extended_card: None,
71            signatures: None,
72        };
73
74        Ok(Self {
75            card,
76            is_active: raw.is_active,
77            system_prompt: raw.system_prompt,
78            mcp_servers: raw.mcp_servers,
79        })
80    }
81}
82
83impl UpdateAgentRequest {
84    pub fn from_raw(raw: UpdateAgentRequestRaw, api_server_url: &str) -> Self {
85        let url = raw
86            .card
87            .url
88            .unwrap_or_else(|| format!("{}/api/v1/agents/{}", api_server_url, raw.card.name));
89
90        let card = AgentCard {
91            name: raw.card.name,
92            description: raw.card.description,
93            supported_interfaces: vec![AgentInterface {
94                url,
95                protocol_binding: raw
96                    .card
97                    .preferred_transport
98                    .unwrap_or(TransportProtocol::JsonRpc),
99                protocol_version: raw.card.protocol_version,
100            }],
101            version: raw.card.version,
102            icon_url: None,
103            provider: None,
104            documentation_url: None,
105            capabilities: raw.card.capabilities.normalize(),
106            security_schemes: raw.card.security_schemes,
107            security: raw.card.security,
108            default_input_modes: if raw.card.default_input_modes.is_empty() {
109                vec!["text/plain".to_owned()]
110            } else {
111                raw.card.default_input_modes
112            },
113            default_output_modes: if raw.card.default_output_modes.is_empty() {
114                vec!["text/plain".to_owned()]
115            } else {
116                raw.card.default_output_modes
117            },
118            skills: raw.card.skills,
119            supports_authenticated_extended_card: None,
120            signatures: None,
121        };
122
123        Self {
124            card,
125            is_active: raw.is_active,
126            system_prompt: raw.system_prompt,
127            mcp_servers: raw.mcp_servers,
128        }
129    }
130
131    pub async fn validate(&self) -> Result<(), String> {
132        if self.card.name.trim().is_empty() {
133            return Err("Name is required".to_owned());
134        }
135
136        let card_url = self.card.url().unwrap_or("");
137        if card_url.trim().is_empty() {
138            return Err("Endpoint is required".to_owned());
139        }
140
141        if !card_url.starts_with("http://") && !card_url.starts_with("https://") {
142            return Err("Endpoint must be a valid HTTP or HTTPS URL".to_owned());
143        }
144
145        if !is_valid_version(&self.card.version) {
146            return Err("Version must be in semantic version format (e.g., 1.0.0)".to_owned());
147        }
148
149        if let Some(ref mcp_servers) = self.mcp_servers {
150            if !mcp_servers.is_empty() {
151                let available_servers = list_available_mcp_servers().await?;
152                let mut invalid_servers = Vec::new();
153
154                for server in mcp_servers {
155                    if !available_servers.contains(server) {
156                        invalid_servers.push(server.clone());
157                    }
158                }
159
160                if !invalid_servers.is_empty() {
161                    return Err(format!(
162                        "Invalid MCP server(s): {}. Available servers: {}",
163                        invalid_servers.join(", "),
164                        if available_servers.is_empty() {
165                            "(none)".to_owned()
166                        } else {
167                            available_servers.join(", ")
168                        }
169                    ));
170                }
171            }
172        }
173
174        Ok(())
175    }
176
177    pub fn is_active(&self) -> bool {
178        self.is_active.unwrap_or(true)
179    }
180
181    pub fn extract_port(&self) -> u16 {
182        self.card
183            .url()
184            .and_then(extract_port_from_url)
185            .unwrap_or(80)
186    }
187}