1use rmcp::{ServerHandler, ServiceExt, model::*, transport::stdio};
17use serde_json::{Map, Value};
18use std::sync::Arc;
19use tokio::sync::RwLock;
20
21use raps_acc::{
22 AccClient, IssuesClient, RfiClient, admin::AccountAdminClient,
23 permissions::FolderPermissionsClient, users::ProjectUsersClient,
24};
25use raps_da::DesignAutomationClient;
26use raps_derivative::DerivativeClient;
27use raps_dm::DataManagementClient;
28use raps_kernel::auth::AuthClient;
29use raps_kernel::config::Config;
30use raps_kernel::http::HttpClientConfig;
31use raps_oss::OssClient;
32use raps_reality::RealityCaptureClient;
33use raps_webhooks::WebhooksClient;
34
35use super::definitions::get_tools;
36
37pub(crate) const MCP_BULK_CONCURRENCY: usize = 10;
39
40pub(crate) const SENSITIVE_HEADERS: &[&str] = &[
42 "set-cookie",
43 "www-authenticate",
44 "authorization",
45 "proxy-authorization",
46 "cookie",
47];
48
49#[derive(Clone)]
53pub struct RapsServer {
54 pub(crate) config: Arc<Config>,
55 pub(crate) http_config: HttpClientConfig,
56 auth_client: Arc<RwLock<Option<AuthClient>>>,
58 oss_client: Arc<RwLock<Option<OssClient>>>,
59 derivative_client: Arc<RwLock<Option<DerivativeClient>>>,
60 dm_client: Arc<RwLock<Option<DataManagementClient>>>,
61 }
63
64impl RapsServer {
65 pub fn new() -> Result<Self, anyhow::Error> {
67 let config = Config::from_env_lenient()?;
68 let http_config = HttpClientConfig::default();
69
70 Ok(Self {
71 config: Arc::new(config),
72 http_config,
73 auth_client: Arc::new(RwLock::new(None)),
74 oss_client: Arc::new(RwLock::new(None)),
75 derivative_client: Arc::new(RwLock::new(None)),
76 dm_client: Arc::new(RwLock::new(None)),
77 })
78 }
79
80 pub(crate) fn config(&self) -> &Config {
82 &self.config
83 }
84
85 pub(crate) fn http_config(&self) -> &HttpClientConfig {
87 &self.http_config
88 }
89
90 pub(crate) async fn get_auth_client(&self) -> AuthClient {
95 if let Some(client) = self.auth_client.read().await.as_ref() {
96 return client.clone();
97 }
98
99 let mut guard = self.auth_client.write().await;
100 if guard.is_none() {
101 *guard = Some(AuthClient::new_with_http_config(
102 (*self.config).clone(),
103 self.http_config.clone(),
104 ));
105 }
106 guard
107 .as_ref()
108 .expect("client was just initialized above")
109 .clone()
110 }
111
112 pub(crate) async fn get_oss_client(&self) -> OssClient {
113 if let Some(client) = self.oss_client.read().await.as_ref() {
114 return client.clone();
115 }
116
117 let auth = self.get_auth_client().await;
118 let mut guard = self.oss_client.write().await;
119 if guard.is_none() {
120 *guard = Some(OssClient::new_with_http_config(
121 (*self.config).clone(),
122 auth,
123 self.http_config.clone(),
124 ));
125 }
126 guard
127 .as_ref()
128 .expect("client was just initialized above")
129 .clone()
130 }
131
132 pub(crate) async fn get_derivative_client(&self) -> DerivativeClient {
133 if let Some(client) = self.derivative_client.read().await.as_ref() {
134 return client.clone();
135 }
136
137 let auth = self.get_auth_client().await;
138 let mut guard = self.derivative_client.write().await;
139 if guard.is_none() {
140 *guard = Some(DerivativeClient::new_with_http_config(
141 (*self.config).clone(),
142 auth,
143 self.http_config.clone(),
144 ));
145 }
146 guard
147 .as_ref()
148 .expect("client was just initialized above")
149 .clone()
150 }
151
152 pub(crate) async fn get_dm_client(&self) -> DataManagementClient {
153 if let Some(client) = self.dm_client.read().await.as_ref() {
154 return client.clone();
155 }
156
157 let auth = self.get_auth_client().await;
158 let mut guard = self.dm_client.write().await;
159 if guard.is_none() {
160 *guard = Some(DataManagementClient::new_with_http_config(
161 (*self.config).clone(),
162 auth,
163 self.http_config.clone(),
164 ));
165 }
166 guard
167 .as_ref()
168 .expect("client was just initialized above")
169 .clone()
170 }
171
172 pub(crate) async fn get_admin_client(&self) -> AccountAdminClient {
175 let auth = self.get_auth_client().await;
176 AccountAdminClient::new_with_http_config(
177 (*self.config).clone(),
178 auth,
179 self.http_config.clone(),
180 )
181 }
182
183 pub(crate) async fn get_users_client(&self) -> ProjectUsersClient {
184 let auth = self.get_auth_client().await;
185 ProjectUsersClient::new_with_http_config(
186 (*self.config).clone(),
187 auth,
188 self.http_config.clone(),
189 )
190 }
191
192 pub(crate) async fn get_issues_client(&self) -> IssuesClient {
193 let auth = self.get_auth_client().await;
194 IssuesClient::new_with_http_config((*self.config).clone(), auth, self.http_config.clone())
195 }
196
197 pub(crate) async fn get_rfi_client(&self) -> RfiClient {
198 let auth = self.get_auth_client().await;
199 RfiClient::new_with_http_config((*self.config).clone(), auth, self.http_config.clone())
200 }
201
202 pub(crate) async fn get_acc_client(&self) -> AccClient {
203 let auth = self.get_auth_client().await;
204 AccClient::new_with_http_config((*self.config).clone(), auth, self.http_config.clone())
205 }
206
207 pub(crate) async fn get_permissions_client(&self) -> FolderPermissionsClient {
208 let auth = self.get_auth_client().await;
209 FolderPermissionsClient::new_with_http_config(
210 (*self.config).clone(),
211 auth,
212 self.http_config.clone(),
213 )
214 }
215
216 pub(crate) async fn get_webhooks_client(&self) -> WebhooksClient {
217 let auth = self.get_auth_client().await;
218 WebhooksClient::new_with_http_config((*self.config).clone(), auth, self.http_config.clone())
219 }
220
221 pub(crate) async fn get_da_client(&self) -> DesignAutomationClient {
222 let auth = self.get_auth_client().await;
223 DesignAutomationClient::new_with_http_config(
224 (*self.config).clone(),
225 auth,
226 self.http_config.clone(),
227 )
228 }
229
230 pub(crate) async fn get_reality_client(&self) -> RealityCaptureClient {
231 let auth = self.get_auth_client().await;
232 raps_reality::RealityCaptureClient::new_with_http_config(
233 (*self.config).clone(),
234 auth,
235 self.http_config.clone(),
236 )
237 }
238
239 pub(crate) fn clamp_limit(limit: Option<usize>, default: usize, max: usize) -> usize {
244 let limit = limit.unwrap_or(default).max(1);
245 limit.min(max)
246 }
247
248 pub(crate) fn required_arg(args: &Map<String, Value>, key: &str) -> Result<String, String> {
249 args.get(key)
250 .and_then(|v| v.as_str())
251 .map(str::trim)
252 .filter(|v| !v.is_empty())
253 .map(|v| v.to_string())
254 .ok_or_else(|| format!("Missing required argument '{}'.", key))
255 }
256
257 pub(crate) fn optional_arg(args: &Map<String, Value>, key: &str) -> Option<String> {
258 args.get(key)
259 .and_then(|v| v.as_str())
260 .map(str::trim)
261 .filter(|v| !v.is_empty())
262 .map(|v| v.to_string())
263 }
264
265 pub(crate) fn validate_urn(urn: &str) -> Result<(), String> {
267 if urn.len() < 10 {
268 return Err("URN is too short — expected a base64-encoded APS URN.".to_string());
269 }
270 if urn.contains(' ') {
271 return Err("URN must not contain spaces.".to_string());
272 }
273 Ok(())
274 }
275
276 #[allow(dead_code)]
278 pub(crate) fn validate_id(value: &str, label: &str) -> Result<(), String> {
279 let id_part = value.rsplit('.').next().unwrap_or(value);
281 if id_part.len() < 8 {
282 return Err(format!(
283 "{} '{}' looks too short — expected a GUID or APS ID.",
284 label, value
285 ));
286 }
287 Ok(())
288 }
289}
290
291pub(crate) fn format_size(bytes: u64) -> String {
297 const KB: u64 = 1024;
298 const MB: u64 = KB * 1024;
299 const GB: u64 = MB * 1024;
300
301 if bytes >= GB {
302 format!("{:.1} GB", bytes as f64 / GB as f64)
303 } else if bytes >= MB {
304 format!("{:.1} MB", bytes as f64 / MB as f64)
305 } else if bytes >= KB {
306 format!("{:.1} KB", bytes as f64 / KB as f64)
307 } else {
308 format!("{} bytes", bytes)
309 }
310}
311
312pub(crate) fn validate_file_path(path: &std::path::Path) -> Result<(), String> {
315 let path_str = path.to_string_lossy().to_lowercase();
316
317 let blocked_patterns = [
319 ".ssh",
320 ".gnupg",
321 ".aws/credentials",
322 ".env",
323 "id_rsa",
324 "id_ed25519",
325 "authorized_keys",
326 "known_hosts",
327 "/etc/shadow",
328 "/etc/passwd",
329 "/etc/cron",
330 "credentials.json",
331 "secrets.json",
332 "token.json",
333 ];
334
335 for pattern in &blocked_patterns {
336 if path_str.contains(pattern) {
337 return Err(format!(
338 "Error: Path '{}' targets a sensitive location (matched '{}').\n\
339 MCP tools cannot read/write security-sensitive files.",
340 path.display(),
341 pattern
342 ));
343 }
344 }
345
346 Ok(())
347}
348
349impl ServerHandler for RapsServer {
354 fn get_info(&self) -> ServerInfo {
355 ServerInfo {
356 instructions: Some(format!(
357 "RAPS MCP Server v{version} - Autodesk Platform Services CLI\n\n\
358 Provides direct access to APS APIs:\n\
359 * auth_* - Authentication (2-legged and 3-legged OAuth)\n\
360 * bucket_*, object_* - OSS storage operations (incl. upload/download/copy)\n\
361 * translate_* - CAD model translation\n\
362 * hub_*, project_* - Data Management & Project Info\n\
363 * folder_*, item_* - Folder and file management\n\
364 * project_create, project_user_* - ACC Project Admin\n\
365 * template_* - Project template management\n\
366 * admin_* - Bulk account administration\n\
367 * issue_*, rfi_* - ACC Issues and RFIs\n\
368 * acc_* - ACC Assets, Submittals, Checklists\n\
369 * da_* - Design Automation\n\
370 * reality_* - Reality Capture / Photogrammetry\n\
371 * webhook_* - Event subscriptions\n\
372 * api_request - Custom APS API calls\n\
373 * report_* - Portfolio reports\n\n\
374 Set APS_CLIENT_ID and APS_CLIENT_SECRET env vars.\n\
375 For 3-legged auth, run 'raps auth login' first.",
376 version = env!("CARGO_PKG_VERSION"),
377 )),
378 capabilities: ServerCapabilities::builder().enable_tools().build(),
379 ..Default::default()
380 }
381 }
382
383 async fn list_tools(
384 &self,
385 _request: Option<PaginatedRequestParam>,
386 _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
387 ) -> Result<ListToolsResult, rmcp::ErrorData> {
388 Ok(ListToolsResult {
389 tools: get_tools(),
390 next_cursor: None,
391 meta: None,
392 })
393 }
394
395 async fn call_tool(
396 &self,
397 request: CallToolRequestParam,
398 _context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
399 ) -> Result<CallToolResult, rmcp::ErrorData> {
400 let result = self.dispatch_tool(&request.name, request.arguments).await;
401 Ok(result)
402 }
403}
404
405pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
407 let server = RapsServer::new()?;
411 let service = server.serve(stdio()).await?;
412 service.waiting().await?;
413 Ok(())
414}