Skip to main content

systemprompt_models/a2a/
agent_card.rs

1use super::security::{OAuth2Flow, OAuth2Flows, SecurityScheme};
2use super::transport::TransportProtocol;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub struct AgentInterface {
8    pub url: String,
9    pub transport: TransportProtocol,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13pub struct AgentProvider {
14    pub organization: String,
15    pub url: String,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19#[serde(rename_all = "camelCase")]
20pub struct AgentCapabilities {
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub streaming: Option<bool>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub push_notifications: Option<bool>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub state_transition_history: Option<bool>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub extensions: Option<Vec<AgentExtension>>,
29}
30
31impl Default for AgentCapabilities {
32    fn default() -> Self {
33        Self {
34            streaming: Some(true),
35            push_notifications: Some(true),
36            state_transition_history: Some(true),
37            extensions: None,
38        }
39    }
40}
41
42impl AgentCapabilities {
43    pub const fn normalize(mut self) -> Self {
44        if self.streaming.is_none() {
45            self.streaming = Some(true);
46        }
47        if self.push_notifications.is_none() {
48            self.push_notifications = Some(false);
49        }
50        if self.state_transition_history.is_none() {
51            self.state_transition_history = Some(true);
52        }
53        self
54    }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58pub struct AgentExtension {
59    pub uri: String,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub description: Option<String>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub required: Option<bool>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub params: Option<serde_json::Value>,
66}
67
68impl AgentExtension {
69    pub fn mcp_tools_extension() -> Self {
70        Self {
71            uri: "systemprompt:mcp-tools".to_string(),
72            description: Some("MCP tool execution capabilities".to_string()),
73            required: Some(false),
74            params: Some(serde_json::json!({
75                "supported_protocols": ["mcp-1.0"]
76            })),
77        }
78    }
79
80    pub fn mcp_tools_extension_with_servers(servers: &[serde_json::Value]) -> Self {
81        Self {
82            uri: "systemprompt:mcp-tools".to_string(),
83            description: Some("MCP tool execution capabilities with server endpoints".to_string()),
84            required: Some(false),
85            params: Some(serde_json::json!({
86                "supported_protocols": ["mcp-1.0"],
87                "servers": servers
88            })),
89        }
90    }
91
92    pub fn opencode_integration_extension() -> Self {
93        Self {
94            uri: "systemprompt:opencode-integration".to_string(),
95            description: Some("OpenCode AI reasoning integration".to_string()),
96            required: Some(false),
97            params: Some(serde_json::json!({
98                "reasoning_model": "claude-3-5-sonnet",
99                "execution_mode": "structured_planning"
100            })),
101        }
102    }
103
104    pub fn artifact_rendering_extension() -> Self {
105        Self {
106            uri: "https://systemprompt.io/extensions/artifact-rendering/v1".to_string(),
107            description: Some(
108                "MCP tool results rendered as typed artifacts with UI hints".to_string(),
109            ),
110            required: Some(false),
111            params: Some(serde_json::json!({
112                "supported_types": ["table", "form", "chart", "tree", "code", "json", "markdown"],
113                "version": "1.0.0"
114            })),
115        }
116    }
117
118    pub fn agent_identity(agent_name: &str) -> Self {
119        Self {
120            uri: "systemprompt:agent-identity".to_string(),
121            description: Some("systemprompt.io platform agent name".to_string()),
122            required: Some(true),
123            params: Some(serde_json::json!({
124                "name": agent_name
125            })),
126        }
127    }
128
129    pub fn system_instructions(system_prompt: &str) -> Self {
130        Self {
131            uri: "systemprompt:system-instructions".to_string(),
132            description: Some("Agent system prompt and behavioral guidelines".to_string()),
133            required: Some(true),
134            params: Some(serde_json::json!({
135                "systemPrompt": system_prompt,
136                "format": "text/plain"
137            })),
138        }
139    }
140
141    pub fn system_instructions_opt(system_prompt: Option<&str>) -> Option<Self> {
142        system_prompt.map(Self::system_instructions)
143    }
144
145    pub fn service_status(
146        status: &str,
147        port: Option<u16>,
148        pid: Option<u32>,
149        default: bool,
150    ) -> Self {
151        let mut params = serde_json::json!({
152            "status": status,
153            "default": default
154        });
155
156        if let Some(p) = port {
157            params["port"] = serde_json::json!(p);
158        }
159        if let Some(p) = pid {
160            params["pid"] = serde_json::json!(p);
161        }
162
163        Self {
164            uri: "systemprompt:service-status".to_string(),
165            description: Some("Runtime service status from orchestrator".to_string()),
166            required: Some(true),
167            params: Some(params),
168        }
169    }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
173#[serde(rename_all = "camelCase")]
174pub struct AgentSkill {
175    pub id: String,
176    pub name: String,
177    pub description: String,
178    pub tags: Vec<String>,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub examples: Option<Vec<String>>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub input_modes: Option<Vec<String>>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub output_modes: Option<Vec<String>>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub security: Option<Vec<HashMap<String, Vec<String>>>>,
187}
188
189impl AgentSkill {
190    pub const fn from_mcp_server(
191        server_name: String,
192        display_name: String,
193        description: String,
194        tags: Vec<String>,
195    ) -> Self {
196        Self {
197            id: server_name,
198            name: display_name,
199            description,
200            tags,
201            examples: None,
202            input_modes: None,
203            output_modes: None,
204            security: None,
205        }
206    }
207
208    pub fn mcp_server_name(&self) -> &str {
209        &self.id
210    }
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
214pub struct AgentCardSignature {
215    pub protected: String,
216    pub signature: String,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub header: Option<serde_json::Value>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
222#[serde(rename_all = "camelCase")]
223pub struct AgentCard {
224    #[serde(default = "default_protocol_version")]
225    pub protocol_version: String,
226    pub name: String,
227    pub description: String,
228    pub url: String,
229    pub version: String,
230    #[serde(default, skip_serializing_if = "Option::is_none")]
231    pub preferred_transport: Option<TransportProtocol>,
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub additional_interfaces: Option<Vec<AgentInterface>>,
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub icon_url: Option<String>,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub provider: Option<AgentProvider>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub documentation_url: Option<String>,
240    pub capabilities: AgentCapabilities,
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub security_schemes: Option<HashMap<String, SecurityScheme>>,
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub security: Option<Vec<HashMap<String, Vec<String>>>>,
245    pub default_input_modes: Vec<String>,
246    pub default_output_modes: Vec<String>,
247    #[serde(default)]
248    pub skills: Vec<AgentSkill>,
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub supports_authenticated_extended_card: Option<bool>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub signatures: Option<Vec<AgentCardSignature>>,
253}
254
255fn default_protocol_version() -> String {
256    "0.3.0".to_string()
257}
258
259impl AgentCard {
260    pub fn builder(
261        name: String,
262        description: String,
263        url: String,
264        version: String,
265    ) -> AgentCardBuilder {
266        AgentCardBuilder::new(name, description, url, version)
267    }
268
269    pub fn has_mcp_extension(&self) -> bool {
270        self.capabilities
271            .extensions
272            .as_ref()
273            .is_some_and(|exts| exts.iter().any(|ext| ext.uri == "systemprompt:mcp-tools"))
274    }
275
276    pub fn ensure_mcp_extension(&mut self) {
277        if self.has_mcp_extension() {
278            return;
279        }
280
281        self.capabilities
282            .extensions
283            .get_or_insert_with(Vec::new)
284            .push(AgentExtension::mcp_tools_extension());
285    }
286}
287
288#[derive(Debug)]
289pub struct AgentCardBuilder {
290    agent_card: AgentCard,
291}
292
293impl AgentCardBuilder {
294    pub fn new(name: String, description: String, url: String, version: String) -> Self {
295        Self {
296            agent_card: AgentCard {
297                protocol_version: "0.3.0".to_string(),
298                name,
299                description,
300                url,
301                version,
302                preferred_transport: Some(TransportProtocol::JsonRpc),
303                additional_interfaces: None,
304                icon_url: None,
305                provider: None,
306                documentation_url: None,
307                capabilities: AgentCapabilities::default(),
308                security_schemes: None,
309                security: None,
310                default_input_modes: vec!["text/plain".to_string()],
311                default_output_modes: vec!["text/plain".to_string()],
312                skills: Vec::new(),
313                supports_authenticated_extended_card: Some(false),
314                signatures: None,
315            },
316        }
317    }
318
319    pub fn with_mcp_skills(
320        mut self,
321        mcp_servers: Vec<(String, String, String, Vec<String>)>,
322    ) -> Self {
323        for (server_name, display_name, description, tags) in mcp_servers {
324            let skill = AgentSkill::from_mcp_server(server_name, display_name, description, tags);
325            self.agent_card.skills.push(skill);
326        }
327
328        let mcp_extension = AgentExtension::mcp_tools_extension();
329        let opencode_extension = AgentExtension::opencode_integration_extension();
330        let artifact_rendering = AgentExtension::artifact_rendering_extension();
331
332        self.agent_card.capabilities.extensions =
333            Some(vec![mcp_extension, opencode_extension, artifact_rendering]);
334
335        self
336    }
337
338    pub const fn with_streaming(mut self) -> Self {
339        self.agent_card.capabilities.streaming = Some(true);
340        self
341    }
342
343    pub const fn with_push_notifications(mut self) -> Self {
344        self.agent_card.capabilities.push_notifications = Some(true);
345        self
346    }
347
348    pub fn with_provider(mut self, organization: String, url: String) -> Self {
349        self.agent_card.provider = Some(AgentProvider { organization, url });
350        self
351    }
352
353    pub fn with_oauth2_security(
354        mut self,
355        authorization_url: String,
356        token_url: String,
357        scopes: HashMap<String, String>,
358    ) -> Self {
359        let oauth2_flows = OAuth2Flows {
360            authorization_code: Some(OAuth2Flow {
361                authorization_url: Some(authorization_url),
362                token_url: Some(token_url),
363                refresh_url: None,
364                scopes,
365            }),
366            implicit: None,
367            password: None,
368            client_credentials: None,
369        };
370
371        let oauth2_scheme = SecurityScheme::OAuth2 {
372            flows: Box::new(oauth2_flows),
373            description: Some("OAuth 2.0 authorization code flow for secure access".to_string()),
374        };
375
376        self.agent_card
377            .security_schemes
378            .get_or_insert_with(HashMap::new)
379            .insert("oauth2".to_string(), oauth2_scheme);
380
381        let mut authentication_requirement = HashMap::new();
382        authentication_requirement.insert(
383            "oauth2".to_string(),
384            vec!["admin".to_string(), "user".to_string()],
385        );
386
387        self.agent_card
388            .security
389            .get_or_insert_with(Vec::new)
390            .push(authentication_requirement);
391
392        self
393    }
394
395    pub fn build(self) -> AgentCard {
396        self.agent_card
397    }
398}