use rmcp::{ServerHandler, ServiceExt, model::*, transport::stdio};
use serde_json::{Map, Value, json};
use std::{str::FromStr, sync::Arc};
use tokio::sync::RwLock;
use crate::api::derivative::OutputFormat;
use crate::api::oss::Region;
use crate::api::{AuthClient, DataManagementClient, DerivativeClient, OssClient};
use crate::config::Config;
use crate::http::HttpClientConfig;
#[derive(Clone)]
pub struct RapsServer {
config: Arc<Config>,
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()?;
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)),
})
}
async fn get_auth_client(&self) -> AuthClient {
if let Some(client) = self.auth_client.read().await.clone() {
return client;
}
let mut guard = self.auth_client.write().await;
guard
.get_or_insert_with(|| {
AuthClient::new_with_http_config((*self.config).clone(), self.http_config.clone())
})
.clone()
}
async fn get_oss_client(&self) -> OssClient {
if let Some(client) = self.oss_client.read().await.clone() {
return client;
}
let auth = self.get_auth_client().await;
let mut guard = self.oss_client.write().await;
guard
.get_or_insert_with(|| {
OssClient::new_with_http_config(
(*self.config).clone(),
auth,
self.http_config.clone(),
)
})
.clone()
}
async fn get_derivative_client(&self) -> DerivativeClient {
if let Some(client) = self.derivative_client.read().await.clone() {
return client;
}
let auth = self.get_auth_client().await;
let mut guard = self.derivative_client.write().await;
guard
.get_or_insert_with(|| {
DerivativeClient::new_with_http_config(
(*self.config).clone(),
auth,
self.http_config.clone(),
)
})
.clone()
}
async fn get_dm_client(&self) -> DataManagementClient {
if let Some(client) = self.dm_client.read().await.clone() {
return client;
}
let auth = self.get_auth_client().await;
let mut guard = self.dm_client.write().await;
guard
.get_or_insert_with(|| {
DataManagementClient::new_with_http_config(
(*self.config).clone(),
auth,
self.http_config.clone(),
)
})
.clone()
}
fn clamp_limit(limit: Option<usize>, default: usize, max: usize) -> usize {
let limit = limit.unwrap_or(default).max(1);
limit.min(max)
}
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))
}
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())
}
async fn auth_test(&self) -> String {
let auth = self.get_auth_client().await;
match auth.get_token().await {
Ok(_) => {
"✅ Authentication successful! 2-legged OAuth credentials are valid.".to_string()
}
Err(e) => format!("❌ Authentication failed: {}", e),
}
}
async fn auth_status(&self) -> String {
let auth = self.get_auth_client().await;
let mut status = String::new();
match auth.get_token().await {
Ok(_) => status.push_str("✅ 2-legged OAuth: Valid\n"),
Err(_) => status.push_str("❌ 2-legged OAuth: Not configured or invalid\n"),
}
match auth.get_3leg_token().await {
Ok(_) => status.push_str("✅ 3-legged OAuth: Valid (user logged in)\n"),
Err(_) => status
.push_str("⚠️ 3-legged OAuth: Not logged in (run 'raps auth login' to log in)\n"),
}
status
}
async fn bucket_list(&self, region: Option<String>, limit: Option<usize>) -> String {
let client = self.get_oss_client().await;
let limit = Self::clamp_limit(limit, 100, 500);
match client.list_buckets().await {
Ok(buckets) => {
let buckets: Vec<_> = buckets
.into_iter()
.filter(|b| {
if let Some(ref r) = region {
b.region
.as_ref()
.map(|br| br.eq_ignore_ascii_case(r))
.unwrap_or(true)
} else {
true
}
})
.take(limit)
.collect();
let mut output = format!("Found {} bucket(s):\n\n", buckets.len());
for b in &buckets {
output.push_str(&format!(
"• {} (policy: {}, region: {})\n",
b.bucket_key,
b.policy_key,
b.region.as_deref().unwrap_or("unknown")
));
}
output
}
Err(e) => format!("Error listing buckets: {}", e),
}
}
async fn bucket_create(&self, bucket_key: String, policy: String, region: String) -> String {
let client = self.get_oss_client().await;
let retention = match policy.to_lowercase().as_str() {
"transient" => crate::api::oss::RetentionPolicy::Transient,
"temporary" => crate::api::oss::RetentionPolicy::Temporary,
"persistent" => crate::api::oss::RetentionPolicy::Persistent,
_ => {
return "❌ Invalid policy. Use transient, temporary, or persistent.".to_string();
}
};
let reg = match region.to_uppercase().as_str() {
"EMEA" => Region::EMEA,
"US" => Region::US,
_ => return "❌ Invalid region. Use US or EMEA.".to_string(),
};
match client.create_bucket(&bucket_key, retention, reg).await {
Ok(bucket) => format!(
"✅ Bucket created successfully:\n• Key: {}\n• Owner: {}\n• Policy: {}",
bucket.bucket_key, bucket.bucket_owner, bucket.policy_key
),
Err(e) => format!("❌ Failed to create bucket: {}", e),
}
}
async fn bucket_get(&self, bucket_key: String) -> String {
let client = self.get_oss_client().await;
match client.get_bucket_details(&bucket_key).await {
Ok(bucket) => format!(
"Bucket: {}\n• Owner: {}\n• Policy: {}\n• Created: {}",
bucket.bucket_key, bucket.bucket_owner, bucket.policy_key, bucket.created_date
),
Err(e) => format!("❌ Bucket not found or error: {e}"),
}
}
async fn bucket_delete(&self, bucket_key: String) -> String {
let client = self.get_oss_client().await;
match client.delete_bucket(&bucket_key).await {
Ok(()) => format!("✅ Bucket '{}' deleted successfully", bucket_key),
Err(e) => format!("❌ Failed to delete bucket: {}", e),
}
}
async fn object_list(&self, bucket_key: String, limit: Option<usize>) -> String {
let client = self.get_oss_client().await;
let limit = Self::clamp_limit(limit, 100, 1000);
match client.list_objects(&bucket_key).await {
Ok(objects) => {
let objects: Vec<_> = objects.into_iter().take(limit).collect();
let mut output =
format!("Found {} object(s) in '{}':\n\n", objects.len(), bucket_key);
for obj in &objects {
output.push_str(&format!("• {} ({} bytes)\n", obj.object_key, obj.size));
}
output
}
Err(e) => format!("Error listing objects: {}", e),
}
}
async fn object_delete(&self, bucket_key: String, object_key: String) -> String {
let client = self.get_oss_client().await;
match client.delete_object(&bucket_key, &object_key).await {
Ok(()) => format!(
"✅ Object '{}' deleted from bucket '{}'",
object_key, bucket_key
),
Err(e) => format!("❌ Failed to delete object: {}", e),
}
}
async fn object_signed_url(
&self,
bucket_key: String,
object_key: String,
minutes: u32,
) -> String {
let client = self.get_oss_client().await;
let minutes = minutes.clamp(2, 60);
match client
.get_signed_download_url(&bucket_key, &object_key, Some(minutes))
.await
{
Ok(response) => {
if let Some(url) = response.url {
format!(
"Pre-signed download URL (expires in {} minutes):\n{}",
minutes, url
)
} else {
"No URL returned. The object may have been uploaded in chunks.".to_string()
}
}
Err(e) => format!("❌ Failed to generate signed URL: {}", e),
}
}
async fn object_urn(&self, bucket_key: String, object_key: String) -> String {
let client = self.get_oss_client().await;
let urn = client.get_urn(&bucket_key, &object_key);
format!("URN for {}/{}:\n{}", bucket_key, object_key, urn)
}
async fn translate_start(&self, urn: String, format: String) -> String {
let client = self.get_derivative_client().await;
let output_format = match OutputFormat::from_str(&format) {
Ok(format) => format,
Err(_) => {
return "❌ Invalid output format. Supported: svf2, svf, thumbnail, obj, stl, step, iges, ifc.".to_string();
}
};
match client.translate(&urn, output_format, None).await {
Ok(result) => format!(
"Translation job started:\n• Result: {}\n• URN: {}",
result.result, result.urn
),
Err(e) => format!("❌ Translation failed: {}", e),
}
}
async fn translate_status(&self, urn: String) -> String {
let client = self.get_derivative_client().await;
match client.get_manifest(&urn).await {
Ok(manifest) => {
let status = &manifest.status;
let progress = &manifest.progress;
format!("Translation status: {} ({})", status, progress)
}
Err(e) => format!("❌ Could not get translation status: {}", e),
}
}
async fn hub_list(&self, limit: Option<usize>) -> String {
let client = self.get_dm_client().await;
let limit = Self::clamp_limit(limit, 50, 200);
match client.list_hubs().await {
Ok(hubs) => {
let hubs: Vec<_> = hubs.into_iter().take(limit).collect();
let mut output = format!("Found {} hub(s):\n\n", hubs.len());
for hub in &hubs {
let region = hub.attributes.region.as_deref().unwrap_or("unknown");
output.push_str(&format!(
"• {} (id: {}, region: {})\n",
hub.attributes.name, hub.id, region
));
}
output
}
Err(e) => format!(
"❌ Failed to list hubs (ensure you're logged in with 'raps auth login'): {}",
e
),
}
}
async fn project_list(&self, hub_id: String, limit: Option<usize>) -> String {
let client = self.get_dm_client().await;
let limit = Self::clamp_limit(limit, 50, 200);
match client.list_projects(&hub_id).await {
Ok(projects) => {
let projects: Vec<_> = projects.into_iter().take(limit).collect();
let mut output = format!("Found {} project(s):\n\n", projects.len());
for proj in &projects {
output.push_str(&format!("• {} (id: {})\n", proj.attributes.name, proj.id));
}
output
}
Err(e) => format!("❌ Failed to list projects: {}", e),
}
}
async fn dispatch_tool(&self, name: &str, args: Option<Map<String, Value>>) -> CallToolResult {
let args = args.unwrap_or_default();
let result = match name {
"auth_test" => self.auth_test().await,
"auth_status" => self.auth_status().await,
"bucket_list" => {
let region = Self::optional_arg(&args, "region");
let limit = args
.get("limit")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
self.bucket_list(region, limit).await
}
"bucket_create" => {
let bucket_key = match Self::required_arg(&args, "bucket_key") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
let policy =
Self::optional_arg(&args, "policy").unwrap_or_else(|| "transient".to_string());
let region =
Self::optional_arg(&args, "region").unwrap_or_else(|| "US".to_string());
self.bucket_create(bucket_key, policy, region).await
}
"bucket_get" => {
let bucket_key = match Self::required_arg(&args, "bucket_key") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
self.bucket_get(bucket_key).await
}
"bucket_delete" => {
let bucket_key = match Self::required_arg(&args, "bucket_key") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
self.bucket_delete(bucket_key).await
}
"object_list" => {
let bucket_key = match Self::required_arg(&args, "bucket_key") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
let limit = args
.get("limit")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
self.object_list(bucket_key, limit).await
}
"object_delete" => {
let bucket_key = match Self::required_arg(&args, "bucket_key") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
let object_key = match Self::required_arg(&args, "object_key") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
self.object_delete(bucket_key, object_key).await
}
"object_signed_url" => {
let bucket_key = match Self::required_arg(&args, "bucket_key") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
let object_key = match Self::required_arg(&args, "object_key") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
let minutes = args.get("minutes").and_then(|v| v.as_u64()).unwrap_or(10) as u32;
self.object_signed_url(bucket_key, object_key, minutes)
.await
}
"object_urn" => {
let bucket_key = match Self::required_arg(&args, "bucket_key") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
let object_key = match Self::required_arg(&args, "object_key") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
self.object_urn(bucket_key, object_key).await
}
"translate_start" => {
let urn = match Self::required_arg(&args, "urn") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
let format =
Self::optional_arg(&args, "format").unwrap_or_else(|| "svf2".to_string());
self.translate_start(urn, format).await
}
"translate_status" => {
let urn = match Self::required_arg(&args, "urn") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
self.translate_status(urn).await
}
"hub_list" => {
let limit = args
.get("limit")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
self.hub_list(limit).await
}
"project_list" => {
let hub_id = match Self::required_arg(&args, "hub_id") {
Ok(val) => val,
Err(err) => return CallToolResult::success(vec![Content::text(err)]),
};
let limit = args
.get("limit")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
self.project_list(hub_id, limit).await
}
_ => format!("Unknown tool: {}", name),
};
CallToolResult::success(vec![Content::text(result)])
}
}
fn schema(props: Value, required: &[&str]) -> Arc<Map<String, Value>> {
let mut obj = Map::new();
obj.insert("type".to_string(), json!("object"));
obj.insert("properties".to_string(), props);
obj.insert("required".to_string(), json!(required));
Arc::new(obj)
}
fn get_tools() -> Vec<Tool> {
vec![
Tool::new(
"auth_test",
"Test 2-legged OAuth authentication with APS",
schema(json!({}), &[]),
),
Tool::new(
"auth_status",
"Check authentication status (2-legged and 3-legged)",
schema(json!({}), &[]),
),
Tool::new(
"bucket_list",
"List OSS buckets. Buckets are containers for storing files.",
schema(
json!({
"region": {"type": "string", "description": "Filter by region: US or EMEA"},
"limit": {"type": "integer", "description": "Max buckets (default: 100)"}
}),
&[],
),
),
Tool::new(
"bucket_create",
"Create a new OSS bucket. Keys must be globally unique, 3-128 chars.",
schema(
json!({
"bucket_key": {"type": "string", "description": "Unique bucket key"},
"policy": {"type": "string", "description": "transient (24h), temporary (30d), or persistent"},
"region": {"type": "string", "description": "US or EMEA (default: US)"}
}),
&["bucket_key"],
),
),
Tool::new(
"bucket_get",
"Get detailed bucket information",
schema(
json!({
"bucket_key": {"type": "string", "description": "The bucket key"}
}),
&["bucket_key"],
),
),
Tool::new(
"bucket_delete",
"Delete an OSS bucket (must be empty)",
schema(
json!({
"bucket_key": {"type": "string", "description": "Bucket key to delete"}
}),
&["bucket_key"],
),
),
Tool::new(
"object_list",
"List objects (files) in an OSS bucket",
schema(
json!({
"bucket_key": {"type": "string", "description": "The bucket key"},
"limit": {"type": "integer", "description": "Max objects (default: 100)"}
}),
&["bucket_key"],
),
),
Tool::new(
"object_delete",
"Delete an object from an OSS bucket",
schema(
json!({
"bucket_key": {"type": "string", "description": "The bucket key"},
"object_key": {"type": "string", "description": "Object key (filename)"}
}),
&["bucket_key", "object_key"],
),
),
Tool::new(
"object_signed_url",
"Generate pre-signed S3 URL for direct download",
schema(
json!({
"bucket_key": {"type": "string", "description": "The bucket key"},
"object_key": {"type": "string", "description": "The object key"},
"minutes": {"type": "integer", "description": "Expiry (2-60 min, default: 10)"}
}),
&["bucket_key", "object_key"],
),
),
Tool::new(
"object_urn",
"Get Base64-encoded URN for an object (used for translation)",
schema(
json!({
"bucket_key": {"type": "string", "description": "The bucket key"},
"object_key": {"type": "string", "description": "The object key"}
}),
&["bucket_key", "object_key"],
),
),
Tool::new(
"translate_start",
"Start CAD translation. Formats: svf2, obj, stl, step, iges, ifc",
schema(
json!({
"urn": {"type": "string", "description": "Base64-encoded URN"},
"format": {"type": "string", "description": "Output format (default: svf2)"}
}),
&["urn"],
),
),
Tool::new(
"translate_status",
"Check translation status: pending, inprogress, success, failed",
schema(
json!({
"urn": {"type": "string", "description": "Base64-encoded URN"}
}),
&["urn"],
),
),
Tool::new(
"hub_list",
"List accessible hubs (BIM 360/ACC). Requires 3-legged auth.",
schema(
json!({
"limit": {"type": "integer", "description": "Max hubs (default: 50)"}
}),
&[],
),
),
Tool::new(
"project_list",
"List projects in a hub. Requires 3-legged auth.",
schema(
json!({
"hub_id": {"type": "string", "description": "The hub ID"},
"limit": {"type": "integer", "description": "Max projects (default: 50)"}
}),
&["hub_id"],
),
),
]
}
impl ServerHandler for RapsServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some(
"🌼 RAPS MCP Server - Autodesk Platform Services CLI\n\n\
Provides direct access to APS APIs:\n\
• auth_test, auth_status - Authentication\n\
• bucket_* - OSS storage buckets\n\
• object_* - Files in buckets\n\
• translate_* - CAD translation\n\
• hub_list, project_list - BIM 360/ACC data\n\n\
Set APS_CLIENT_ID and APS_CLIENT_SECRET env vars."
.into(),
),
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>> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(tracing::Level::WARN.into()),
)
.with_writer(std::io::stderr)
.init();
let server = RapsServer::new()?;
let service = server.serve(stdio()).await?;
service.waiting().await?;
Ok(())
}