qualia_client_core/
qapp_mcp.rs1use crate::api::{
2 inspect_installed_qapp_readiness, list_installed_qapps, list_qapp_update_offers,
3 load_installed_qapp_package,
4};
5use crate::qapp_registry::QappPackageManifest;
6use serde::Serialize;
7
8#[derive(Debug, Clone, Serialize)]
9struct QappCatalogueEntry {
10 name: String,
11 version: String,
12 did: String,
13 display_name: String,
14 category: String,
15 preferred_launch_mode: String,
16 launch_modes: Vec<String>,
17 supports_offline: bool,
18 required_shapes: Vec<String>,
19 required_ontologies: Vec<String>,
20 required_models: Vec<String>,
21 ui_surfaces: Vec<String>,
22 entrypoints: Vec<String>,
23 notes: Vec<String>,
24}
25
26#[derive(Debug, Clone, Serialize)]
27struct QappHostSurfaceSchema {
28 host_shell: String,
29 package_manifest: String,
30 layout_strategies: Vec<&'static str>,
31 presentation_modes: Vec<&'static str>,
32 coordinate_spaces: Vec<&'static str>,
33 layer_behaviors: Vec<&'static str>,
34 theme_scopes: Vec<&'static str>,
35 manifest_surfaces: Vec<&'static str>,
36 mcp_tools: Vec<&'static str>,
37}
38
39fn package_did(manifest: &QappPackageManifest) -> String {
40 manifest
41 .x_qualia
42 .as_ref()
43 .and_then(|extension| {
44 (!extension.app_id.trim().is_empty()).then_some(extension.app_id.clone())
45 })
46 .unwrap_or_else(|| {
47 format!(
48 "did:qualia:qapp:{}",
49 manifest.name.to_lowercase().replace(' ', "-")
50 )
51 })
52}
53
54fn catalogue_entry(manifest: &QappPackageManifest) -> QappCatalogueEntry {
55 let extension = manifest.x_qualia.clone().unwrap_or_default();
56 let mut entrypoints = extension.entrypoints.keys().cloned().collect::<Vec<_>>();
57 entrypoints.sort();
58
59 QappCatalogueEntry {
60 name: manifest.name.clone(),
61 version: manifest.version.clone(),
62 did: package_did(manifest),
63 display_name: if extension.display_name.is_empty() {
64 manifest.name.clone()
65 } else {
66 extension.display_name
67 },
68 category: extension.category,
69 preferred_launch_mode: extension.preferred_launch_mode,
70 launch_modes: extension.launch_modes,
71 supports_offline: extension.supports_offline,
72 required_shapes: manifest.required_shapes.clone(),
73 required_ontologies: extension.required_ontologies,
74 required_models: extension.required_models,
75 ui_surfaces: extension.ui_surfaces,
76 entrypoints,
77 notes: extension.notes,
78 }
79}
80
81pub fn list_qapp_catalogue_json() -> Result<String, String> {
82 let mut installed = list_installed_qapps();
83 installed.sort();
84
85 let mut entries = Vec::new();
86 for qapp_name in installed {
87 let manifest = load_installed_qapp_package(&qapp_name)?;
88 entries.push(catalogue_entry(&manifest));
89 }
90
91 serde_json::to_string(&entries).map_err(|e| e.to_string())
92}
93
94pub fn get_qapp_manifest_json(qapp_name: &str) -> Result<String, String> {
95 let manifest = load_installed_qapp_package(qapp_name)?;
96 serde_json::to_string(&manifest).map_err(|e| e.to_string())
97}
98
99pub fn inspect_qapp_readiness_json(qapp_name: &str) -> Result<String, String> {
100 inspect_installed_qapp_readiness(qapp_name.to_string())
101}
102
103pub fn list_qapp_updates_json() -> Result<String, String> {
104 list_qapp_update_offers()
105}
106
107pub fn describe_qapp_surface_schema_json() -> Result<String, String> {
108 let schema = QappHostSurfaceSchema {
109 host_shell: "webizen-studio".to_string(),
110 package_manifest: "qapp.json".to_string(),
111 layout_strategies: vec!["PointGrid", "CssGrid", "FlexBox", "Masonry"],
112 presentation_modes: vec!["GridBound", "NodeRelational", "Spatial"],
113 coordinate_spaces: vec!["GlobalCartesian", "RelativeAnchored"],
114 layer_behaviors: vec!["Docked", "FloatingOverlay", "ModalOverlay", "FullCanvas"],
115 theme_scopes: vec!["environment", "app", "page", "module"],
116 manifest_surfaces: vec![
117 "static-web",
118 "wasm-local",
119 "online-daemon-aware",
120 "native-dioxus-pane",
121 ],
122 mcp_tools: vec![
123 "list_qapps",
124 "get_qapp_manifest",
125 "inspect_qapp_readiness",
126 "list_qapp_updates",
127 "describe_qapp_surface_schema",
128 "query_sparql",
129 "computational_geometry",
130 ],
131 };
132 serde_json::to_string(&schema).map_err(|e| e.to_string())
133}