Skip to main content

systemprompt_agent/models/web/
create_agent.rs

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