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