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}