Skip to main content

systemprompt_models/a2a/
agent_card.rs

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