Skip to main content

systemprompt_models/a2a/agent_card/
mod.rs

1//! A2A protocol agent card — the JSON document an agent publishes to
2//! describe its identity, capabilities, transports, and skills.
3
4mod extension;
5mod skill;
6
7pub use extension::{ARTIFACT_RENDERING_URI, AgentCapabilities, AgentExtension};
8pub use skill::{AgentCardSignature, AgentInterface, AgentProvider, AgentSkill};
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13use super::security::{OAuth2Flow, OAuth2Flows, SecurityScheme};
14use super::transport::ProtocolBinding;
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
17#[serde(rename_all = "camelCase")]
18pub struct AgentCard {
19    pub name: String,
20    pub description: String,
21    pub supported_interfaces: Vec<AgentInterface>,
22    pub version: String,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub icon_url: Option<String>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub provider: Option<AgentProvider>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub documentation_url: Option<String>,
29    pub capabilities: AgentCapabilities,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub security_schemes: Option<HashMap<String, SecurityScheme>>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub security: Option<Vec<HashMap<String, Vec<String>>>>,
34    pub default_input_modes: Vec<String>,
35    pub default_output_modes: Vec<String>,
36    #[serde(default)]
37    pub skills: Vec<AgentSkill>,
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub supports_authenticated_extended_card: Option<bool>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub signatures: Option<Vec<AgentCardSignature>>,
42}
43
44impl AgentCard {
45    #[must_use]
46    pub fn builder(
47        name: String,
48        description: String,
49        url: String,
50        version: String,
51    ) -> AgentCardBuilder {
52        AgentCardBuilder::new(name, description, url, version)
53    }
54
55    #[must_use]
56    pub fn url(&self) -> Option<&str> {
57        self.supported_interfaces.first().map(|i| i.url.as_str())
58    }
59
60    #[must_use]
61    pub fn has_mcp_extension(&self) -> bool {
62        self.capabilities
63            .extensions
64            .as_ref()
65            .is_some_and(|exts| exts.iter().any(|ext| ext.uri == "systemprompt:mcp-tools"))
66    }
67
68    pub fn ensure_mcp_extension(&mut self) {
69        if self.has_mcp_extension() {
70            return;
71        }
72
73        self.capabilities
74            .extensions
75            .get_or_insert_with(Vec::new)
76            .push(AgentExtension::mcp_tools_extension());
77    }
78}
79
80#[derive(Debug)]
81pub struct AgentCardBuilder {
82    agent_card: AgentCard,
83}
84
85impl AgentCardBuilder {
86    #[must_use]
87    pub fn new(name: String, description: String, url: String, version: String) -> Self {
88        Self {
89            agent_card: AgentCard {
90                name,
91                description,
92                supported_interfaces: vec![AgentInterface {
93                    url,
94                    protocol_binding: ProtocolBinding::JsonRpc,
95                    protocol_version: "1.0.0".to_string(),
96                }],
97                version,
98                icon_url: None,
99                provider: None,
100                documentation_url: None,
101                capabilities: AgentCapabilities::default(),
102                security_schemes: None,
103                security: None,
104                default_input_modes: vec!["text/plain".to_string()],
105                default_output_modes: vec!["text/plain".to_string()],
106                skills: Vec::new(),
107                supports_authenticated_extended_card: Some(false),
108                signatures: None,
109            },
110        }
111    }
112
113    #[must_use]
114    pub fn with_mcp_skills(
115        mut self,
116        mcp_servers: Vec<(String, String, String, Vec<String>)>,
117    ) -> Self {
118        for (server_name, display_name, description, tags) in mcp_servers {
119            let skill = AgentSkill::from_mcp_server(server_name, display_name, description, tags);
120            self.agent_card.skills.push(skill);
121        }
122
123        let mcp_extension = AgentExtension::mcp_tools_extension();
124        let opencode_extension = AgentExtension::opencode_integration_extension();
125        let artifact_rendering = AgentExtension::artifact_rendering_extension();
126
127        self.agent_card.capabilities.extensions =
128            Some(vec![mcp_extension, opencode_extension, artifact_rendering]);
129
130        self
131    }
132
133    #[must_use]
134    pub const fn with_streaming(mut self) -> Self {
135        self.agent_card.capabilities.streaming = Some(true);
136        self
137    }
138
139    #[must_use]
140    pub const fn with_push_notifications(mut self) -> Self {
141        self.agent_card.capabilities.push_notifications = Some(true);
142        self
143    }
144
145    #[must_use]
146    pub fn with_provider(mut self, organization: String, url: String) -> Self {
147        self.agent_card.provider = Some(AgentProvider { organization, url });
148        self
149    }
150
151    #[must_use]
152    pub fn with_oauth2_security(
153        mut self,
154        authorization_url: String,
155        token_url: String,
156        scopes: HashMap<String, String>,
157    ) -> Self {
158        let oauth2_flows = OAuth2Flows {
159            authorization_code: Some(OAuth2Flow {
160                authorization_url: Some(authorization_url),
161                token_url: Some(token_url),
162                refresh_url: None,
163                scopes,
164            }),
165            implicit: None,
166            password: None,
167            client_credentials: None,
168        };
169
170        let oauth2_scheme = SecurityScheme::OAuth2 {
171            flows: Box::new(oauth2_flows),
172            description: Some("OAuth 2.0 authorization code flow for secure access".to_string()),
173        };
174
175        self.agent_card
176            .security_schemes
177            .get_or_insert_with(HashMap::new)
178            .insert("oauth2".to_string(), oauth2_scheme);
179
180        let mut authentication_requirement = HashMap::new();
181        authentication_requirement.insert(
182            "oauth2".to_string(),
183            vec!["admin".to_string(), "user".to_string()],
184        );
185
186        self.agent_card
187            .security
188            .get_or_insert_with(Vec::new)
189            .push(authentication_requirement);
190
191        self
192    }
193
194    #[must_use]
195    pub fn build(self) -> AgentCard {
196        self.agent_card
197    }
198}