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