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,
179 pub name: String,
180 pub description: String,
181 pub tags: Vec<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub examples: Option<Vec<String>>,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub input_modes: Option<Vec<String>>,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub output_modes: Option<Vec<String>>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub security: Option<Vec<HashMap<String, Vec<String>>>>,
190}
191
192impl AgentSkill {
193 pub const fn from_mcp_server(
194 server_name: String,
195 display_name: String,
196 description: String,
197 tags: Vec<String>,
198 ) -> Self {
199 Self {
200 id: server_name,
201 name: display_name,
202 description,
203 tags,
204 examples: None,
205 input_modes: None,
206 output_modes: None,
207 security: None,
208 }
209 }
210
211 pub fn mcp_server_name(&self) -> &str {
212 &self.id
213 }
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
217pub struct AgentCardSignature {
218 pub protected: String,
219 pub signature: String,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub header: Option<serde_json::Value>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
225#[serde(rename_all = "camelCase")]
226pub struct AgentCard {
227 pub name: String,
228 pub description: String,
229 pub supported_interfaces: Vec<AgentInterface>,
230 pub version: String,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub icon_url: Option<String>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub provider: Option<AgentProvider>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub documentation_url: Option<String>,
237 pub capabilities: AgentCapabilities,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub security_schemes: Option<HashMap<String, SecurityScheme>>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub security: Option<Vec<HashMap<String, Vec<String>>>>,
242 pub default_input_modes: Vec<String>,
243 pub default_output_modes: Vec<String>,
244 #[serde(default)]
245 pub skills: Vec<AgentSkill>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub supports_authenticated_extended_card: Option<bool>,
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub signatures: Option<Vec<AgentCardSignature>>,
250}
251
252fn default_protocol_version() -> String {
253 "1.0.0".to_string()
254}
255
256impl AgentCard {
257 pub fn builder(
258 name: String,
259 description: String,
260 url: String,
261 version: String,
262 ) -> AgentCardBuilder {
263 AgentCardBuilder::new(name, description, url, version)
264 }
265
266 pub fn url(&self) -> Option<&str> {
267 self.supported_interfaces.first().map(|i| i.url.as_str())
268 }
269
270 pub fn has_mcp_extension(&self) -> bool {
271 self.capabilities
272 .extensions
273 .as_ref()
274 .is_some_and(|exts| exts.iter().any(|ext| ext.uri == "systemprompt:mcp-tools"))
275 }
276
277 pub fn ensure_mcp_extension(&mut self) {
278 if self.has_mcp_extension() {
279 return;
280 }
281
282 self.capabilities
283 .extensions
284 .get_or_insert_with(Vec::new)
285 .push(AgentExtension::mcp_tools_extension());
286 }
287}
288
289#[derive(Debug)]
290pub struct AgentCardBuilder {
291 agent_card: AgentCard,
292}
293
294impl AgentCardBuilder {
295 pub fn new(name: String, description: String, url: String, version: String) -> Self {
296 Self {
297 agent_card: AgentCard {
298 name,
299 description,
300 supported_interfaces: vec![AgentInterface {
301 url,
302 protocol_binding: ProtocolBinding::JsonRpc,
303 protocol_version: "1.0.0".to_string(),
304 }],
305 version,
306 icon_url: None,
307 provider: None,
308 documentation_url: None,
309 capabilities: AgentCapabilities::default(),
310 security_schemes: None,
311 security: None,
312 default_input_modes: vec!["text/plain".to_string()],
313 default_output_modes: vec!["text/plain".to_string()],
314 skills: Vec::new(),
315 supports_authenticated_extended_card: Some(false),
316 signatures: None,
317 },
318 }
319 }
320
321 pub fn with_mcp_skills(
322 mut self,
323 mcp_servers: Vec<(String, String, String, Vec<String>)>,
324 ) -> Self {
325 for (server_name, display_name, description, tags) in mcp_servers {
326 let skill = AgentSkill::from_mcp_server(server_name, display_name, description, tags);
327 self.agent_card.skills.push(skill);
328 }
329
330 let mcp_extension = AgentExtension::mcp_tools_extension();
331 let opencode_extension = AgentExtension::opencode_integration_extension();
332 let artifact_rendering = AgentExtension::artifact_rendering_extension();
333
334 self.agent_card.capabilities.extensions =
335 Some(vec![mcp_extension, opencode_extension, artifact_rendering]);
336
337 self
338 }
339
340 pub const fn with_streaming(mut self) -> Self {
341 self.agent_card.capabilities.streaming = Some(true);
342 self
343 }
344
345 pub const fn with_push_notifications(mut self) -> Self {
346 self.agent_card.capabilities.push_notifications = Some(true);
347 self
348 }
349
350 pub fn with_provider(mut self, organization: String, url: String) -> Self {
351 self.agent_card.provider = Some(AgentProvider { organization, url });
352 self
353 }
354
355 pub fn with_oauth2_security(
356 mut self,
357 authorization_url: String,
358 token_url: String,
359 scopes: HashMap<String, String>,
360 ) -> Self {
361 let oauth2_flows = OAuth2Flows {
362 authorization_code: Some(OAuth2Flow {
363 authorization_url: Some(authorization_url),
364 token_url: Some(token_url),
365 refresh_url: None,
366 scopes,
367 }),
368 implicit: None,
369 password: None,
370 client_credentials: None,
371 };
372
373 let oauth2_scheme = SecurityScheme::OAuth2 {
374 flows: Box::new(oauth2_flows),
375 description: Some("OAuth 2.0 authorization code flow for secure access".to_string()),
376 };
377
378 self.agent_card
379 .security_schemes
380 .get_or_insert_with(HashMap::new)
381 .insert("oauth2".to_string(), oauth2_scheme);
382
383 let mut authentication_requirement = HashMap::new();
384 authentication_requirement.insert(
385 "oauth2".to_string(),
386 vec!["admin".to_string(), "user".to_string()],
387 );
388
389 self.agent_card
390 .security
391 .get_or_insert_with(Vec::new)
392 .push(authentication_requirement);
393
394 self
395 }
396
397 pub fn build(self) -> AgentCard {
398 self.agent_card
399 }
400}