1use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
49pub struct WebMcpTool {
50 pub name: String,
52 pub description: String,
54 pub input_schema: WebMcpInputSchema,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub origin: Option<String>,
59 #[serde(default)]
61 pub annotations: WebMcpToolAnnotations,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
71pub struct WebMcpInputSchema {
72 #[serde(rename = "type")]
74 pub schema_type: String,
75 #[serde(default)]
77 pub properties: HashMap<String, WebMcpPropertySchema>,
78 #[serde(default)]
80 pub required: Vec<String>,
81}
82
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
87pub struct WebMcpPropertySchema {
88 #[serde(rename = "type")]
90 pub prop_type: String,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub description: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub default: Option<serde_json::Value>,
97 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
99 pub enum_values: Option<Vec<serde_json::Value>>,
100}
101
102#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
109pub struct WebMcpToolAnnotations {
110 #[serde(default)]
112 pub read_only: bool,
113 #[serde(default)]
115 pub destructive: bool,
116 #[serde(default)]
118 pub required_scopes: Vec<String>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub estimated_cost: Option<u64>,
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
136pub struct WebMcpContext {
137 pub url: String,
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub title: Option<String>,
142 #[serde(default)]
144 pub state: HashMap<String, serde_json::Value>,
145 #[serde(default)]
147 pub available_actions: Vec<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub user_context: Option<WebMcpUserContext>,
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
157pub struct WebMcpUserContext {
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub user_id: Option<String>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub display_name: Option<String>,
164 #[serde(default)]
166 pub scopes: Vec<String>,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
175#[serde(rename_all = "snake_case")]
176#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
177pub enum WebMcpDiscoveryPhase {
178 Detecting,
180 Parsing,
182 Ready,
184 Failed,
186 Revoked,
188}
189
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
194pub struct WebMcpDiscoveryState {
195 pub url: String,
197 pub phase: WebMcpDiscoveryPhase,
199 #[serde(default)]
201 pub tools: Vec<WebMcpTool>,
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub context: Option<WebMcpContext>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub error: Option<String>,
208}
209
210#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
218pub struct WebMcpToolCall {
219 pub tool_name: String,
221 #[serde(default)]
223 pub arguments: serde_json::Value,
224 #[serde(skip_serializing_if = "Option::is_none")]
226 pub origin: Option<String>,
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub request_id: Option<String>,
230}
231
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
236pub struct WebMcpToolResult {
237 pub success: bool,
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub result: Option<serde_json::Value>,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub error: Option<String>,
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub request_id: Option<String>,
248}
249
250#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
260#[serde(rename_all = "camelCase")]
261#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
262pub struct WebMcpSecurityPolicy {
263 #[serde(default)]
265 pub allowed_origins: Vec<String>,
266 #[serde(default)]
268 pub required_scopes: Vec<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub max_tokens_per_call: Option<u64>,
272 #[serde(default)]
274 pub audit_logging: bool,
275 #[serde(default)]
277 pub sandbox_mode: WebMcpSandboxMode,
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
282#[serde(rename_all = "snake_case")]
283#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
284pub enum WebMcpSandboxMode {
285 #[default]
287 PageContext,
288 Isolated,
290 ServerSide,
292}
293
294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
304#[serde(rename_all = "camelCase")]
305#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
306pub struct WebMcpManifest {
307 pub version: String,
309 pub name: String,
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub description: Option<String>,
314 #[serde(default)]
316 pub tools: Vec<WebMcpTool>,
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub security: Option<WebMcpSecurityPolicy>,
320}
321
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
334pub struct WebMcpAgentCapabilities {
335 pub agent_id: String,
337 #[serde(default)]
339 pub provided_tools: Vec<String>,
340 #[serde(default)]
342 pub memory_capabilities: Vec<WebMcpMemoryCapability>,
343 #[serde(default)]
345 pub model_routing: Vec<String>,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
350#[serde(rename_all = "snake_case")]
351#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
352pub enum WebMcpMemoryCapability {
353 Recall,
355 Store,
357 Search,
359 ContextAssembly,
361 Summarize,
363 Ingest,
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use std::collections::HashMap;
371
372 #[test]
373 fn web_mcp_tool_serde_roundtrip() {
374 let tool = WebMcpTool {
375 name: "search".into(),
376 description: "Search the web".into(),
377 input_schema: WebMcpInputSchema {
378 schema_type: "object".into(),
379 properties: HashMap::from([(
380 "query".into(),
381 WebMcpPropertySchema {
382 prop_type: "string".into(),
383 description: Some("Search query".into()),
384 default: None,
385 enum_values: None,
386 },
387 )]),
388 required: vec!["query".into()],
389 },
390 origin: None,
391 annotations: WebMcpToolAnnotations::default(),
392 };
393 let json = serde_json::to_string(&tool).unwrap();
394 let d: WebMcpTool = serde_json::from_str(&json).unwrap();
395 assert_eq!(tool.name, d.name);
396 assert_eq!(d.input_schema.required, vec!["query"]);
397 }
398
399 #[test]
400 fn web_mcp_tool_annotations_default_is_safe() {
401 let ann = WebMcpToolAnnotations::default();
402 assert!(!ann.read_only);
403 assert!(!ann.destructive);
404 assert!(ann.required_scopes.is_empty());
405 assert!(ann.estimated_cost.is_none());
406 }
407
408 #[test]
409 fn web_mcp_discovery_phase_serde_roundtrip() {
410 let phases = vec![
411 WebMcpDiscoveryPhase::Detecting,
412 WebMcpDiscoveryPhase::Parsing,
413 WebMcpDiscoveryPhase::Ready,
414 WebMcpDiscoveryPhase::Failed,
415 WebMcpDiscoveryPhase::Revoked,
416 ];
417 for phase in phases {
418 let json = serde_json::to_string(&phase).unwrap();
419 let d: WebMcpDiscoveryPhase = serde_json::from_str(&json).unwrap();
420 assert_eq!(phase, d);
421 }
422 }
423
424 #[test]
425 fn web_mcp_sandbox_mode_default_is_page_context() {
426 assert_eq!(WebMcpSandboxMode::default(), WebMcpSandboxMode::PageContext);
427 }
428
429 #[test]
430 fn web_mcp_sandbox_mode_serde_roundtrip() {
431 let modes = vec![
432 WebMcpSandboxMode::PageContext,
433 WebMcpSandboxMode::Isolated,
434 WebMcpSandboxMode::ServerSide,
435 ];
436 for mode in modes {
437 let json = serde_json::to_string(&mode).unwrap();
438 let d: WebMcpSandboxMode = serde_json::from_str(&json).unwrap();
439 assert_eq!(mode, d);
440 }
441 }
442
443 #[test]
444 fn web_mcp_memory_capability_serde_roundtrip() {
445 let caps = vec![
446 WebMcpMemoryCapability::Recall,
447 WebMcpMemoryCapability::Store,
448 WebMcpMemoryCapability::Search,
449 WebMcpMemoryCapability::ContextAssembly,
450 WebMcpMemoryCapability::Summarize,
451 WebMcpMemoryCapability::Ingest,
452 ];
453 for cap in caps {
454 let json = serde_json::to_string(&cap).unwrap();
455 let d: WebMcpMemoryCapability = serde_json::from_str(&json).unwrap();
456 assert_eq!(cap, d);
457 }
458 }
459
460 #[test]
461 fn web_mcp_security_policy_defaults() {
462 let policy: WebMcpSecurityPolicy = serde_json::from_str("{}").unwrap();
463 assert!(policy.allowed_origins.is_empty());
464 assert!(!policy.audit_logging);
465 assert_eq!(policy.sandbox_mode, WebMcpSandboxMode::PageContext);
466 }
467
468 #[test]
469 fn web_mcp_manifest_serde_roundtrip() {
470 let manifest = WebMcpManifest {
471 version: "1.0.0".into(),
472 name: "test-server".into(),
473 description: Some("A test MCP server".into()),
474 tools: vec![],
475 security: Some(WebMcpSecurityPolicy {
476 allowed_origins: vec!["https://example.com".into()],
477 required_scopes: vec![],
478 max_tokens_per_call: Some(1000),
479 audit_logging: true,
480 sandbox_mode: WebMcpSandboxMode::Isolated,
481 }),
482 };
483 let json = serde_json::to_string(&manifest).unwrap();
484 let d: WebMcpManifest = serde_json::from_str(&json).unwrap();
485 assert_eq!(manifest.name, d.name);
486 assert_eq!(manifest.version, d.version);
487 assert!(d.security.is_some());
488 }
489
490 #[test]
491 fn web_mcp_tool_call_serde_roundtrip() {
492 let call = WebMcpToolCall {
493 tool_name: "search".into(),
494 arguments: serde_json::json!({"query": "test"}),
495 origin: Some("https://example.com".into()),
496 request_id: Some("req-123".into()),
497 };
498 let json = serde_json::to_string(&call).unwrap();
499 let d: WebMcpToolCall = serde_json::from_str(&json).unwrap();
500 assert_eq!(call.tool_name, d.tool_name);
501 }
502
503 #[test]
504 fn web_mcp_tool_result_success() {
505 let result = WebMcpToolResult {
506 success: true,
507 result: Some(serde_json::json!({"data": [1, 2, 3]})),
508 error: None,
509 request_id: Some("req-123".into()),
510 };
511 let json = serde_json::to_string(&result).unwrap();
512 let d: WebMcpToolResult = serde_json::from_str(&json).unwrap();
513 assert!(d.success);
514 assert!(d.error.is_none());
515 }
516
517 #[test]
518 fn web_mcp_tool_result_failure() {
519 let result = WebMcpToolResult {
520 success: false,
521 result: None,
522 error: Some("tool not found".into()),
523 request_id: None,
524 };
525 assert!(!result.success);
526 assert!(result.error.is_some());
527 }
528
529 #[test]
530 fn web_mcp_agent_capabilities_serde_roundtrip() {
531 let caps = WebMcpAgentCapabilities {
532 agent_id: "agent-1".into(),
533 provided_tools: vec!["recall".into(), "search".into()],
534 memory_capabilities: vec![
535 WebMcpMemoryCapability::Recall,
536 WebMcpMemoryCapability::Store,
537 ],
538 model_routing: vec!["openai".into()],
539 };
540 let json = serde_json::to_string(&caps).unwrap();
541 let d: WebMcpAgentCapabilities = serde_json::from_str(&json).unwrap();
542 assert_eq!(caps.agent_id, d.agent_id);
543 assert_eq!(d.memory_capabilities.len(), 2);
544 }
545
546 #[test]
547 fn web_mcp_context_serde_roundtrip() {
548 let ctx = WebMcpContext {
549 url: "https://example.com/dashboard".into(),
550 title: Some("Dashboard".into()),
551 state: HashMap::from([("page".into(), serde_json::json!("home"))]),
552 available_actions: vec!["click_button".into()],
553 user_context: Some(WebMcpUserContext {
554 user_id: Some("user-1".into()),
555 display_name: Some("Test User".into()),
556 scopes: vec!["read".into()],
557 }),
558 };
559 let json = serde_json::to_string(&ctx).unwrap();
560 let d: WebMcpContext = serde_json::from_str(&json).unwrap();
561 assert_eq!(ctx.url, d.url);
562 assert!(d.user_context.is_some());
563 }
564}