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 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}