gestura_core_mcp/
integrator.rs1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::{path::PathBuf, sync::RwLock};
8
9use crate::error::AppError;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TokenInfo {
14 pub token: String,
16 pub created_at: chrono::DateTime<chrono::Utc>,
18 pub expires_at: chrono::DateTime<chrono::Utc>,
20 pub haptic_permission: bool,
22 pub client_id: String,
24 pub scopes: Vec<String>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
30pub struct MdhResource {
31 pub uri: String,
33 pub data: serde_json::Value,
35}
36
37#[async_trait::async_trait]
39pub trait McpIntegrator: Send + Sync {
40 async fn expose_tool(&self, tool: &str) -> Result<(), AppError>;
42 async fn authenticate_haptic(&self, token: &str) -> Result<bool, AppError>;
44 async fn validate_token(&self, token: &str) -> Result<bool, AppError>;
46 async fn grant_haptic_permission(&self, token: &str) -> Result<(), AppError>;
48}
49
50pub fn mdh_translate(ld_file: PathBuf) -> Result<MdhResource, AppError> {
53 let content = std::fs::read_to_string(&ld_file)
54 .map_err(|e| AppError::Io(std::io::Error::other(format!("read mdh file: {e}"))))?;
55 let mut value: serde_json::Value = serde_json::from_str(&content).map_err(AppError::Json)?;
56
57 let mut uri_type = None;
59 if let serde_json::Value::Object(map) = &value
60 && let Some(t) = map.get("@type").and_then(|v| v.as_str())
61 {
62 uri_type = Some(t.to_string());
63 }
64 if uri_type.is_none() {
65 uri_type = Some(
67 ld_file
68 .file_stem()
69 .and_then(|s| s.to_str())
70 .unwrap_or("local")
71 .to_string(),
72 );
73 }
74
75 if let serde_json::Value::Object(map) = &mut value
77 && map.contains_key("@context")
78 {
79 map.remove("@context");
80 }
81
82 let uri = format!("mcp://mdh/{}", uri_type.unwrap());
83 Ok(MdhResource { uri, data: value })
84}
85
86pub struct LocalMcp {
88 tools: RwLock<Vec<String>>,
89 tokens: RwLock<HashMap<String, TokenInfo>>,
90}
91
92impl Default for LocalMcp {
93 fn default() -> Self {
94 Self {
95 tools: RwLock::new(Vec::new()),
96 tokens: RwLock::new(HashMap::new()),
97 }
98 }
99}
100
101impl LocalMcp {
102 pub fn new() -> Self {
104 Self::default()
105 }
106
107 pub fn generate_token(
109 &self,
110 client_id: &str,
111 scopes: Vec<String>,
112 duration_hours: i64,
113 ) -> Result<TokenInfo, AppError> {
114 use rand::Rng;
115 use rand::distributions::Alphanumeric;
116
117 let token: String = rand::thread_rng()
118 .sample_iter(&Alphanumeric)
119 .take(32)
120 .map(char::from)
121 .collect();
122
123 let now = chrono::Utc::now();
124 let expires_at = now + chrono::Duration::hours(duration_hours);
125
126 let token_info = TokenInfo {
127 token: token.clone(),
128 created_at: now,
129 expires_at,
130 haptic_permission: false,
131 client_id: client_id.to_string(),
132 scopes,
133 };
134
135 let mut tokens = self
136 .tokens
137 .write()
138 .map_err(|e| AppError::Io(std::io::Error::other(format!("rwlock: {e}"))))?;
139 tokens.insert(token.clone(), token_info.clone());
140
141 tracing::info!(
142 "Generated token for client {}: expires at {}",
143 client_id,
144 expires_at
145 );
146
147 Ok(token_info)
148 }
149
150 pub fn cleanup_expired_tokens(&self) -> Result<usize, AppError> {
152 let mut tokens = self
153 .tokens
154 .write()
155 .map_err(|e| AppError::Io(std::io::Error::other(format!("rwlock: {e}"))))?;
156
157 let now = chrono::Utc::now();
158 let initial_count = tokens.len();
159 tokens.retain(|_, info| info.expires_at > now);
160 let removed = initial_count - tokens.len();
161
162 if removed > 0 {
163 tracing::info!("Cleaned up {} expired tokens", removed);
164 }
165
166 Ok(removed)
167 }
168
169 pub fn get_token_info(&self, token: &str) -> Result<Option<TokenInfo>, AppError> {
171 let tokens = self
172 .tokens
173 .read()
174 .map_err(|e| AppError::Io(std::io::Error::other(format!("rwlock: {e}"))))?;
175
176 if let Some(info) = tokens.get(token)
177 && info.expires_at > chrono::Utc::now()
178 {
179 return Ok(Some(info.clone()));
180 }
181
182 Ok(None)
183 }
184
185 pub fn list_active_tokens(&self) -> Result<Vec<TokenInfo>, AppError> {
187 let tokens = self
188 .tokens
189 .read()
190 .map_err(|e| AppError::Io(std::io::Error::other(format!("rwlock: {e}"))))?;
191
192 let now = chrono::Utc::now();
193 Ok(tokens
194 .values()
195 .filter(|info| info.expires_at > now)
196 .cloned()
197 .collect())
198 }
199}
200
201#[async_trait::async_trait]
202impl McpIntegrator for LocalMcp {
203 async fn expose_tool(&self, tool: &str) -> Result<(), AppError> {
204 let mut guard = self
205 .tools
206 .write()
207 .map_err(|e| AppError::Io(std::io::Error::other(format!("rwlock: {e}"))))?;
208 guard.push(tool.to_string());
209 tracing::info!("Exposed MCP tool: {}", tool);
210 Ok(())
211 }
212
213 async fn authenticate_haptic(&self, token: &str) -> Result<bool, AppError> {
214 if let Some(info) = self.get_token_info(token)? {
216 if info.haptic_permission {
217 tracing::info!(
218 "Haptic authentication successful for client: {}",
219 info.client_id
220 );
221 return Ok(true);
222 }
223 tracing::warn!(
224 "Haptic permission not granted for client: {}",
225 info.client_id
226 );
227 return Ok(false);
228 }
229
230 tracing::warn!("Invalid or expired token for haptic authentication");
231 Ok(false)
232 }
233
234 async fn validate_token(&self, token: &str) -> Result<bool, AppError> {
235 if let Some(info) = self.get_token_info(token)? {
237 tracing::debug!("Token validated for client: {}", info.client_id);
238 return Ok(true);
239 }
240
241 tracing::debug!("Token validation failed: invalid or expired");
242 Ok(false)
243 }
244
245 async fn grant_haptic_permission(&self, token: &str) -> Result<(), AppError> {
246 let mut tokens = self
247 .tokens
248 .write()
249 .map_err(|e| AppError::Io(std::io::Error::other(format!("rwlock: {e}"))))?;
250
251 if let Some(info) = tokens.get_mut(token) {
252 if info.expires_at > chrono::Utc::now() {
253 info.haptic_permission = true;
254 tracing::info!("Granted haptic permission for client: {}", info.client_id);
255 return Ok(());
256 }
257 return Err(AppError::Io(std::io::Error::other("Token expired")));
258 }
259
260 Err(AppError::Io(std::io::Error::other("Token not found")))
261 }
262}
263
264static MCP_INSTANCE: std::sync::OnceLock<LocalMcp> = std::sync::OnceLock::new();
266
267pub fn get_mcp() -> &'static LocalMcp {
269 MCP_INSTANCE.get_or_init(LocalMcp::new)
270}