1use super::security::{OAuth2Flow, OAuth2Flows, SecurityScheme};
2use super::transport::ProtocolBinding;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6pub const ARTIFACT_RENDERING_URI: &str = "https://systemprompt.io/extensions/artifact-rendering/v1";
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "camelCase")]
10pub struct AgentInterface {
11 pub url: String,
12 pub protocol_binding: ProtocolBinding,
13 #[serde(default = "default_protocol_version")]
14 pub protocol_version: String,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18pub struct AgentProvider {
19 pub organization: String,
20 pub url: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24#[serde(rename_all = "camelCase")]
25pub struct AgentCapabilities {
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub streaming: Option<bool>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub push_notifications: Option<bool>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub state_transition_history: Option<bool>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub extensions: Option<Vec<AgentExtension>>,
34}
35
36impl Default for AgentCapabilities {
37 fn default() -> Self {
38 Self {
39 streaming: Some(true),
40 push_notifications: Some(true),
41 state_transition_history: Some(true),
42 extensions: None,
43 }
44 }
45}
46
47impl AgentCapabilities {
48 pub const fn normalize(mut self) -> Self {
49 if self.streaming.is_none() {
50 self.streaming = Some(true);
51 }
52 if self.push_notifications.is_none() {
53 self.push_notifications = Some(false);
54 }
55 if self.state_transition_history.is_none() {
56 self.state_transition_history = Some(true);
57 }
58 self
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63pub struct AgentExtension {
64 pub uri: String,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub description: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub required: Option<bool>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub params: Option<serde_json::Value>,
71}
72
73impl AgentExtension {
74 pub fn mcp_tools_extension() -> Self {
75 Self {
76 uri: "systemprompt:mcp-tools".to_string(),
77 description: Some("MCP tool execution capabilities".to_string()),
78 required: Some(false),
79 params: Some(serde_json::json!({
80 "supported_protocols": ["mcp-1.0"]
81 })),
82 }
83 }
84
85 pub fn mcp_tools_extension_with_servers(servers: &[serde_json::Value]) -> Self {
86 Self {
87 uri: "systemprompt:mcp-tools".to_string(),
88 description: Some("MCP tool execution capabilities with server endpoints".to_string()),
89 required: Some(false),
90 params: Some(serde_json::json!({
91 "supported_protocols": ["mcp-1.0"],
92 "servers": servers
93 })),
94 }
95 }
96
97 pub fn opencode_integration_extension() -> Self {
98 Self {
99 uri: "systemprompt:opencode-integration".to_string(),
100 description: Some("OpenCode AI reasoning integration".to_string()),
101 required: Some(false),
102 params: Some(serde_json::json!({
103 "reasoning_model": "claude-3-5-sonnet",
104 "execution_mode": "structured_planning"
105 })),
106 }
107 }
108
109 pub fn artifact_rendering_extension() -> Self {
110 Self {
111 uri: ARTIFACT_RENDERING_URI.to_string(),
112 description: Some(
113 "MCP tool results rendered as typed artifacts with UI hints".to_string(),
114 ),
115 required: Some(false),
116 params: Some(serde_json::json!({
117 "supported_types": ["table", "form", "chart", "tree", "code", "json", "markdown"],
118 "version": "1.0.0"
119 })),
120 }
121 }
122
123 pub fn agent_identity(agent_name: &str) -> Self {
124 Self {
125 uri: "systemprompt:agent-identity".to_string(),
126 description: Some("systemprompt.io platform agent name".to_string()),
127 required: Some(true),
128 params: Some(serde_json::json!({
129 "name": agent_name
130 })),
131 }
132 }
133
134 pub fn system_instructions(system_prompt: &str) -> Self {
135 Self {
136 uri: "systemprompt:system-instructions".to_string(),
137 description: Some("Agent system prompt and behavioral guidelines".to_string()),
138 required: Some(true),
139 params: Some(serde_json::json!({
140 "systemPrompt": system_prompt,
141 "format": "text/plain"
142 })),
143 }
144 }
145
146 pub fn system_instructions_opt(system_prompt: Option<&str>) -> Option<Self> {
147 system_prompt.map(Self::system_instructions)
148 }
149
150 pub fn service_status(
151 status: &str,
152 port: Option<u16>,
153 pid: Option<u32>,
154 default: bool,
155 ) -> Self {
156 let mut params = serde_json::json!({
157 "status": status,
158 "default": default
159 });
160
161 if let Some(p) = port {
162 params["port"] = serde_json::json!(p);
163 }
164 if let Some(p) = pid {
165 params["pid"] = serde_json::json!(p);
166 }
167
168 Self {
169 uri: "systemprompt:service-status".to_string(),
170 description: Some("Runtime service status from orchestrator".to_string()),
171 required: Some(true),
172 params: Some(params),
173 }
174 }
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
178#[serde(rename_all = "camelCase")]
179pub struct AgentSkill {
180 pub id: String,
182 pub name: String,
183 pub description: String,
184 pub tags: Vec<String>,
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub examples: Option<Vec<String>>,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub input_modes: Option<Vec<String>>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub output_modes: Option<Vec<String>>,
191 #[serde(skip_serializing_if = "Option::is_none")]
192 pub security: Option<Vec<HashMap<String, Vec<String>>>>,
193}
194
195impl AgentSkill {
196 pub const fn from_mcp_server(
197 server_name: String,
198 display_name: String,
199 description: String,
200 tags: Vec<String>,
201 ) -> Self {
202 Self {
203 id: server_name,
204 name: display_name,
205 description,
206 tags,
207 examples: None,
208 input_modes: None,
209 output_modes: None,
210 security: None,
211 }
212 }
213
214 pub fn mcp_server_name(&self) -> &str {
215 &self.id
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
220pub struct AgentCardSignature {
221 pub protected: String,
222 pub signature: String,
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub header: Option<serde_json::Value>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
228#[serde(rename_all = "camelCase")]
229pub struct AgentCard {
230 pub name: String,
231 pub description: String,
232 pub supported_interfaces: Vec<AgentInterface>,
233 pub version: String,
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 "1.0.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 url(&self) -> Option<&str> {
270 self.supported_interfaces.first().map(|i| i.url.as_str())
271 }
272
273 pub fn has_mcp_extension(&self) -> bool {
274 self.capabilities
275 .extensions
276 .as_ref()
277 .is_some_and(|exts| exts.iter().any(|ext| ext.uri == "systemprompt:mcp-tools"))
278 }
279
280 pub fn ensure_mcp_extension(&mut self) {
281 if self.has_mcp_extension() {
282 return;
283 }
284
285 self.capabilities
286 .extensions
287 .get_or_insert_with(Vec::new)
288 .push(AgentExtension::mcp_tools_extension());
289 }
290}
291
292#[derive(Debug)]
293pub struct AgentCardBuilder {
294 agent_card: AgentCard,
295}
296
297impl AgentCardBuilder {
298 pub fn new(name: String, description: String, url: String, version: String) -> Self {
299 Self {
300 agent_card: AgentCard {
301 name,
302 description,
303 supported_interfaces: vec![AgentInterface {
304 url,
305 protocol_binding: ProtocolBinding::JsonRpc,
306 protocol_version: "1.0.0".to_string(),
307 }],
308 version,
309 icon_url: None,
310 provider: None,
311 documentation_url: None,
312 capabilities: AgentCapabilities::default(),
313 security_schemes: None,
314 security: None,
315 default_input_modes: vec!["text/plain".to_string()],
316 default_output_modes: vec!["text/plain".to_string()],
317 skills: Vec::new(),
318 supports_authenticated_extended_card: Some(false),
319 signatures: None,
320 },
321 }
322 }
323
324 pub fn with_mcp_skills(
325 mut self,
326 mcp_servers: Vec<(String, String, String, Vec<String>)>,
327 ) -> Self {
328 for (server_name, display_name, description, tags) in mcp_servers {
329 let skill = AgentSkill::from_mcp_server(server_name, display_name, description, tags);
330 self.agent_card.skills.push(skill);
331 }
332
333 let mcp_extension = AgentExtension::mcp_tools_extension();
334 let opencode_extension = AgentExtension::opencode_integration_extension();
335 let artifact_rendering = AgentExtension::artifact_rendering_extension();
336
337 self.agent_card.capabilities.extensions =
338 Some(vec![mcp_extension, opencode_extension, artifact_rendering]);
339
340 self
341 }
342
343 pub const fn with_streaming(mut self) -> Self {
344 self.agent_card.capabilities.streaming = Some(true);
345 self
346 }
347
348 pub const fn with_push_notifications(mut self) -> Self {
349 self.agent_card.capabilities.push_notifications = Some(true);
350 self
351 }
352
353 pub fn with_provider(mut self, organization: String, url: String) -> Self {
354 self.agent_card.provider = Some(AgentProvider { organization, url });
355 self
356 }
357
358 pub fn with_oauth2_security(
359 mut self,
360 authorization_url: String,
361 token_url: String,
362 scopes: HashMap<String, String>,
363 ) -> Self {
364 let oauth2_flows = OAuth2Flows {
365 authorization_code: Some(OAuth2Flow {
366 authorization_url: Some(authorization_url),
367 token_url: Some(token_url),
368 refresh_url: None,
369 scopes,
370 }),
371 implicit: None,
372 password: None,
373 client_credentials: None,
374 };
375
376 let oauth2_scheme = SecurityScheme::OAuth2 {
377 flows: Box::new(oauth2_flows),
378 description: Some("OAuth 2.0 authorization code flow for secure access".to_string()),
379 };
380
381 self.agent_card
382 .security_schemes
383 .get_or_insert_with(HashMap::new)
384 .insert("oauth2".to_string(), oauth2_scheme);
385
386 let mut authentication_requirement = HashMap::new();
387 authentication_requirement.insert(
388 "oauth2".to_string(),
389 vec!["admin".to_string(), "user".to_string()],
390 );
391
392 self.agent_card
393 .security
394 .get_or_insert_with(Vec::new)
395 .push(authentication_requirement);
396
397 self
398 }
399
400 pub fn build(self) -> AgentCard {
401 self.agent_card
402 }
403}