use rmcp::{ServerHandler, ServiceExt, model::*, transport::stdio};
use serde_json::{Map, Value};
use std::sync::Arc;
use tokio::sync::RwLock;
use raps_acc::{
AccClient, IssuesClient, RfiClient, admin::AccountAdminClient,
permissions::FolderPermissionsClient, users::ProjectUsersClient,
};
use raps_da::DesignAutomationClient;
use raps_derivative::DerivativeClient;
use raps_dm::DataManagementClient;
use raps_kernel::auth::AuthClient;
use raps_kernel::config::Config;
use raps_kernel::http::HttpClientConfig;
use raps_oss::OssClient;
use raps_reality::RealityCaptureClient;
use raps_webhooks::WebhooksClient;
use super::definitions::get_tools;
pub(crate) const MCP_BULK_CONCURRENCY: usize = 10;
pub(crate) const SENSITIVE_HEADERS: &[&str] = &[
"set-cookie",
"www-authenticate",
"authorization",
"proxy-authorization",
"cookie",
];
#[derive(Clone)]
pub struct RapsServer {
pub(crate) config: Arc<Config>,
pub(crate) http_config: HttpClientConfig,
auth_client: Arc<RwLock<Option<AuthClient>>>,
oss_client: Arc<RwLock<Option<OssClient>>>,
derivative_client: Arc<RwLock<Option<DerivativeClient>>>,
dm_client: Arc<RwLock<Option<DataManagementClient>>>,
}
impl RapsServer {
pub fn new() -> Result<Self, anyhow::Error> {
let config = Config::from_env_lenient()?;
let http_config = HttpClientConfig::default();
Ok(Self {
config: Arc::new(config),
http_config,
auth_client: Arc::new(RwLock::new(None)),
oss_client: Arc::new(RwLock::new(None)),
derivative_client: Arc::new(RwLock::new(None)),
dm_client: Arc::new(RwLock::new(None)),
})
}
pub(crate) fn config(&self) -> &Config {
&self.config
}
pub(crate) fn http_config(&self) -> &HttpClientConfig {
&self.http_config
}
pub(crate) async fn get_auth_client(&self) -> AuthClient {
if let Some(client) = self.auth_client.read().await.as_ref() {
return client.clone();
}
let mut guard = self.auth_client.write().await;
if guard.is_none() {
*guard = Some(AuthClient::new_with_http_config(
(*self.config).clone(),
self.http_config.clone(),
));
}
guard
.as_ref()
.expect("client was just initialized above")
.clone()
}
pub(crate) async fn get_oss_client(&self) -> OssClient {
if let Some(client) = self.oss_client.read().await.as_ref() {
return client.clone();
}
let auth = self.get_auth_client().await;
let mut guard = self.oss_client.write().await;
if guard.is_none() {
*guard = Some(OssClient::new_with_http_config(
(*self.config).clone(),
auth,
self.http_config.clone(),
));
}
guard
.as_ref()
.expect("client was just initialized above")
.clone()
}
pub(crate) async fn get_derivative_client(&self) -> DerivativeClient {
if let Some(client) = self.derivative_client.read().await.as_ref() {
return client.clone();
}
let auth = self.get_auth_client().await;
let mut guard = self.derivative_client.write().await;
if guard.is_none() {
*guard = Some(DerivativeClient::new_with_http_config(
(*self.config).clone(),
auth,
self.http_config.clone(),
));
}
guard
.as_ref()
.expect("client was just initialized above")
.clone()
}
pub(crate) async fn get_dm_client(&self) -> DataManagementClient {
if let Some(client) = self.dm_client.read().await.as_ref() {
return client.clone();
}
let auth = self.get_auth_client().await;
let mut guard = self.dm_client.write().await;
if guard.is_none() {
*guard = Some(DataManagementClient::new_with_http_config(
(*self.config).clone(),
auth,
self.http_config.clone(),
));
}
guard
.as_ref()
.expect("client was just initialized above")
.clone()
}
pub(crate) async fn get_admin_client(&self) -> AccountAdminClient {
let auth = self.get_auth_client().await;
AccountAdminClient::new_with_http_config(
(*self.config).clone(),
auth,
self.http_config.clone(),
)
}
pub(crate) async fn get_users_client(&self) -> ProjectUsersClient {
let auth = self.get_auth_client().await;
ProjectUsersClient::new_with_http_config(
(*self.config).clone(),
auth,
self.http_config.clone(),
)
}
pub(crate) async fn get_issues_client(&self) -> IssuesClient {
let auth = self.get_auth_client().await;
IssuesClient::new_with_http_config((*self.config).clone(), auth, self.http_config.clone())
}
pub(crate) async fn get_rfi_client(&self) -> RfiClient {
let auth = self.get_auth_client().await;
RfiClient::new_with_http_config((*self.config).clone(), auth, self.http_config.clone())
}
pub(crate) async fn get_acc_client(&self) -> AccClient {
let auth = self.get_auth_client().await;
AccClient::new_with_http_config((*self.config).clone(), auth, self.http_config.clone())
}
pub(crate) async fn get_permissions_client(&self) -> FolderPermissionsClient {
let auth = self.get_auth_client().await;
FolderPermissionsClient::new_with_http_config(
(*self.config).clone(),
auth,
self.http_config.clone(),
)
}
pub(crate) async fn get_webhooks_client(&self) -> WebhooksClient {
let auth = self.get_auth_client().await;
WebhooksClient::new_with_http_config((*self.config).clone(), auth, self.http_config.clone())
}
pub(crate) async fn get_da_client(&self) -> DesignAutomationClient {
let auth = self.get_auth_client().await;
DesignAutomationClient::new_with_http_config(
(*self.config).clone(),
auth,
self.http_config.clone(),
)
}
pub(crate) async fn get_reality_client(&self) -> RealityCaptureClient {
let auth = self.get_auth_client().await;
raps_reality::RealityCaptureClient::new_with_http_config(
(*self.config).clone(),
auth,
self.http_config.clone(),
)
}
pub(crate) fn clamp_limit(limit: Option<usize>, default: usize, max: usize) -> usize {
let limit = limit.unwrap_or(default).max(1);
limit.min(max)
}
pub(crate) fn required_arg(args: &Map<String, Value>, key: &str) -> Result<String, String> {
args.get(key)
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|v| !v.is_empty())
.map(|v| v.to_string())
.ok_or_else(|| format!("Missing required argument '{}'.", key))
}
pub(crate) fn optional_arg(args: &Map<String, Value>, key: &str) -> Option<String> {
args.get(key)
.and_then(|v| v.as_str())
.map(str::trim)
.filter(|v| !v.is_empty())
.map(|v| v.to_string())
}
pub(crate) fn validate_urn(urn: &str) -> Result<(), String> {
if urn.len() < 10 {
return Err("URN is too short — expected a base64-encoded APS URN.".to_string());
}
if urn.contains(' ') {
return Err("URN must not contain spaces.".to_string());
}
Ok(())
}
#[allow(dead_code)]
pub(crate) fn validate_id(value: &str, label: &str) -> Result<(), String> {
let id_part = value.rsplit('.').next().unwrap_or(value);
if id_part.len() < 8 {
return Err(format!(
"{} '{}' looks too short — expected a GUID or APS ID.",
label, value
));
}
Ok(())
}
}
pub(crate) fn format_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{} bytes", bytes)
}
}
pub(crate) fn validate_file_path(path: &std::path::Path) -> Result<(), String> {
let path_str = path.to_string_lossy().to_lowercase();
let blocked_patterns = [
".ssh",
".gnupg",
".aws/credentials",
".env",
"id_rsa",
"id_ed25519",
"authorized_keys",
"known_hosts",
"/etc/shadow",
"/etc/passwd",
"/etc/cron",
"credentials.json",
"secrets.json",
"token.json",
];
for pattern in &blocked_patterns {
if path_str.contains(pattern) {
return Err(format!(
"Error: Path '{}' targets a sensitive location (matched '{}').\n\
MCP tools cannot read/write security-sensitive files.",
path.display(),
pattern
));
}
}
Ok(())
}
impl ServerHandler for RapsServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some(format!(
"RAPS MCP Server v{version} - Autodesk Platform Services CLI\n\n\
Provides direct access to APS APIs:\n\
* auth_* - Authentication (2-legged and 3-legged OAuth)\n\
* bucket_*, object_* - OSS storage operations (incl. upload/download/copy)\n\
* translate_* - CAD model translation\n\
* hub_*, project_* - Data Management & Project Info\n\
* folder_*, item_* - Folder and file management\n\
* project_create, project_user_* - ACC Project Admin\n\
* template_* - Project template management\n\
* admin_* - Bulk account administration\n\
* issue_*, rfi_* - ACC Issues and RFIs\n\
* acc_* - ACC Assets, Submittals, Checklists\n\
* da_* - Design Automation\n\
* reality_* - Reality Capture / Photogrammetry\n\
* webhook_* - Event subscriptions\n\
* api_request - Custom APS API calls\n\
* report_* - Portfolio reports\n\n\
Set APS_CLIENT_ID and APS_CLIENT_SECRET env vars.\n\
For 3-legged auth, run 'raps auth login' first.",
version = env!("CARGO_PKG_VERSION"),
)),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
async fn list_tools(
&self,
_request: Option<PaginatedRequestParam>,
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
) -> Result<ListToolsResult, rmcp::ErrorData> {
Ok(ListToolsResult {
tools: get_tools(),
next_cursor: None,
meta: None,
})
}
async fn call_tool(
&self,
request: CallToolRequestParam,
_context: rmcp::service::RequestContext<rmcp::service::RoleServer>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let result = self.dispatch_tool(&request.name, request.arguments).await;
Ok(result)
}
}
pub async fn run_server() -> Result<(), Box<dyn std::error::Error>> {
let server = RapsServer::new()?;
let service = server.serve(stdio()).await?;
service.waiting().await?;
Ok(())
}