systemprompt_models/a2a/agent_card/
mod.rs1mod 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}