use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::time::Duration;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ProxyError {
#[error("Proxy request failed: {0}")]
Request(#[from] reqwest::Error),
#[error("Proxy error ({status}): {body}")]
ProxyResponse { status: u16, body: String },
#[error("Invalid proxy URL: {0}")]
InvalidUrl(String),
#[error("Proxy returned invalid response: {0}")]
InvalidResponse(String),
}
#[derive(Debug, Serialize)]
pub struct ProxyCallRequest {
pub tool_name: String,
pub args: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_args: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub struct ProxyCallResponse {
pub result: Value,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ProxyHelpRequest {
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct ProxyHelpResponse {
pub content: String,
#[serde(default)]
pub error: Option<String>,
}
const PROXY_TIMEOUT_SECS: u64 = 120;
fn build_proxy_request(
client: &Client,
method: reqwest::Method,
url: &str,
) -> reqwest::RequestBuilder {
let mut req = client.request(method, url);
if let Ok(token) = std::env::var("ATI_SESSION_TOKEN") {
if !token.is_empty() {
req = req.header("Authorization", format!("Bearer {token}"));
}
}
req
}
pub async fn call_tool(
proxy_url: &str,
tool_name: &str,
args: &HashMap<String, Value>,
raw_args: Option<&[String]>,
) -> Result<Value, ProxyError> {
let client = Client::builder()
.timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
.build()?;
let url = format!("{}/call", proxy_url.trim_end_matches('/'));
let args_value = serde_json::to_value(args).unwrap_or(Value::Object(serde_json::Map::new()));
let raw_args_vec = raw_args.filter(|r| !r.is_empty()).map(|r| r.to_vec());
let payload = ProxyCallRequest {
tool_name: tool_name.to_string(),
args: args_value,
raw_args: raw_args_vec,
};
let response = build_proxy_request(&client, reqwest::Method::POST, &url)
.json(&payload)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_else(|_| "empty".into());
return Err(ProxyError::ProxyResponse {
status: status.as_u16(),
body,
});
}
let body: ProxyCallResponse = response
.json()
.await
.map_err(|e| ProxyError::InvalidResponse(e.to_string()))?;
if let Some(err) = body.error {
return Err(ProxyError::ProxyResponse {
status: 200,
body: err,
});
}
Ok(body.result)
}
pub async fn list_tools(proxy_url: &str, query_params: &str) -> Result<Value, ProxyError> {
let client = Client::builder()
.timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
.build()?;
let mut url = format!("{}/tools", proxy_url.trim_end_matches('/'));
if !query_params.is_empty() {
url.push('?');
url.push_str(query_params);
}
let response = build_proxy_request(&client, reqwest::Method::GET, &url)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(ProxyError::ProxyResponse {
status: status.as_u16(),
body,
});
}
Ok(response.json().await?)
}
pub async fn get_tool_info(proxy_url: &str, name: &str) -> Result<Value, ProxyError> {
let client = Client::builder()
.timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
.build()?;
let url = format!("{}/tools/{}", proxy_url.trim_end_matches('/'), name);
let response = build_proxy_request(&client, reqwest::Method::GET, &url)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(ProxyError::ProxyResponse {
status: status.as_u16(),
body,
});
}
Ok(response.json().await?)
}
pub async fn call_mcp(
proxy_url: &str,
method: &str,
params: Option<Value>,
) -> Result<Value, ProxyError> {
use std::sync::atomic::{AtomicU64, Ordering};
static MCP_ID: AtomicU64 = AtomicU64::new(1);
let id = MCP_ID.fetch_add(1, Ordering::SeqCst);
let msg = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
});
let client = Client::builder()
.timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
.build()?;
let url = format!("{}/mcp", proxy_url.trim_end_matches('/'));
let response = build_proxy_request(&client, reqwest::Method::POST, &url)
.json(&msg)
.send()
.await?;
let status = response.status();
if status == reqwest::StatusCode::ACCEPTED {
return Ok(Value::Null);
}
if !status.is_success() {
let body = response.text().await.unwrap_or_else(|_| "empty".into());
return Err(ProxyError::ProxyResponse {
status: status.as_u16(),
body,
});
}
let body: Value = response
.json()
.await
.map_err(|e| ProxyError::InvalidResponse(e.to_string()))?;
if let Some(err) = body.get("error") {
let message = err
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("MCP proxy error");
return Err(ProxyError::ProxyResponse {
status: 200,
body: message.to_string(),
});
}
Ok(body.get("result").cloned().unwrap_or(Value::Null))
}
pub async fn list_skills(
proxy_url: &str,
query_params: &str,
) -> Result<serde_json::Value, ProxyError> {
let client = Client::builder()
.timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
.build()?;
let url = if query_params.is_empty() {
format!("{}/skills", proxy_url.trim_end_matches('/'))
} else {
format!("{}/skills?{query_params}", proxy_url.trim_end_matches('/'))
};
let response = build_proxy_request(&client, reqwest::Method::GET, &url)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_else(|_| "empty".into());
return Err(ProxyError::ProxyResponse {
status: status.as_u16(),
body,
});
}
response
.json()
.await
.map_err(|e| ProxyError::InvalidResponse(e.to_string()))
}
pub async fn get_skill(
proxy_url: &str,
name: &str,
query_params: &str,
) -> Result<serde_json::Value, ProxyError> {
let client = Client::builder()
.timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
.build()?;
let url = if query_params.is_empty() {
format!("{}/skills/{name}", proxy_url.trim_end_matches('/'))
} else {
format!(
"{}/skills/{name}?{query_params}",
proxy_url.trim_end_matches('/')
)
};
let response = build_proxy_request(&client, reqwest::Method::GET, &url)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_else(|_| "empty".into());
return Err(ProxyError::ProxyResponse {
status: status.as_u16(),
body,
});
}
response
.json()
.await
.map_err(|e| ProxyError::InvalidResponse(e.to_string()))
}
async fn get_proxy_json(proxy_url: &str, path: &str) -> Result<serde_json::Value, ProxyError> {
let client = Client::builder()
.timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
.build()?;
let url = format!(
"{}/{}",
proxy_url.trim_end_matches('/'),
path.trim_start_matches('/')
);
let response = build_proxy_request(&client, reqwest::Method::GET, &url)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_else(|_| "empty".into());
return Err(ProxyError::ProxyResponse {
status: status.as_u16(),
body,
});
}
response
.json()
.await
.map_err(|e| ProxyError::InvalidResponse(e.to_string()))
}
async fn get_proxy_json_with_query(
proxy_url: &str,
path: &str,
query: &[(&str, String)],
) -> Result<serde_json::Value, ProxyError> {
let client = Client::builder()
.timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
.build()?;
let mut url = format!(
"{}/{}",
proxy_url.trim_end_matches('/'),
path.trim_start_matches('/')
);
if !query.is_empty() {
let params = query
.iter()
.map(|(key, value)| format!("{key}={}", urlencoding(value)))
.collect::<Vec<_>>()
.join("&");
url.push('?');
url.push_str(¶ms);
}
let response = build_proxy_request(&client, reqwest::Method::GET, &url)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_else(|_| "empty".into());
return Err(ProxyError::ProxyResponse {
status: status.as_u16(),
body,
});
}
response
.json()
.await
.map_err(|e| ProxyError::InvalidResponse(e.to_string()))
}
pub async fn get_skillati_catalog(
proxy_url: &str,
search: Option<&str>,
) -> Result<serde_json::Value, ProxyError> {
let query = search
.map(|value| vec![("search", value.to_string())])
.unwrap_or_default();
get_proxy_json_with_query(proxy_url, "skillati/catalog", &query).await
}
pub async fn get_skillati_read(
proxy_url: &str,
name: &str,
) -> Result<serde_json::Value, ProxyError> {
get_proxy_json(proxy_url, &format!("skillati/{}", urlencoding(name))).await
}
pub async fn get_skillati_resources(
proxy_url: &str,
name: &str,
prefix: Option<&str>,
) -> Result<serde_json::Value, ProxyError> {
let query = prefix
.map(|value| vec![("prefix", value.to_string())])
.unwrap_or_default();
get_proxy_json_with_query(
proxy_url,
&format!("skillati/{}/resources", urlencoding(name)),
&query,
)
.await
}
pub async fn get_skillati_file(
proxy_url: &str,
name: &str,
path: &str,
) -> Result<serde_json::Value, ProxyError> {
get_proxy_json_with_query(
proxy_url,
&format!("skillati/{}/file", urlencoding(name)),
&[("path", path.to_string())],
)
.await
}
pub async fn get_skillati_refs(
proxy_url: &str,
name: &str,
) -> Result<serde_json::Value, ProxyError> {
get_proxy_json(proxy_url, &format!("skillati/{}/refs", urlencoding(name))).await
}
pub async fn get_skillati_ref(
proxy_url: &str,
name: &str,
reference: &str,
) -> Result<serde_json::Value, ProxyError> {
get_proxy_json(
proxy_url,
&format!(
"skillati/{}/ref/{}",
urlencoding(name),
urlencoding(reference)
),
)
.await
}
fn urlencoding(s: &str) -> String {
s.replace('%', "%25")
.replace(' ', "%20")
.replace('#', "%23")
.replace('&', "%26")
.replace('?', "%3F")
.replace('/', "%2F")
.replace('=', "%3D")
}
pub async fn resolve_skills(
proxy_url: &str,
scopes: &serde_json::Value,
) -> Result<serde_json::Value, ProxyError> {
let client = Client::builder()
.timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
.build()?;
let url = format!("{}/skills/resolve", proxy_url.trim_end_matches('/'));
let response = build_proxy_request(&client, reqwest::Method::POST, &url)
.json(scopes)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_else(|_| "empty".into());
return Err(ProxyError::ProxyResponse {
status: status.as_u16(),
body,
});
}
response
.json()
.await
.map_err(|e| ProxyError::InvalidResponse(e.to_string()))
}
pub async fn call_help(
proxy_url: &str,
query: &str,
tool: Option<&str>,
) -> Result<String, ProxyError> {
let client = Client::builder()
.timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
.build()?;
let url = format!("{}/help", proxy_url.trim_end_matches('/'));
let payload = ProxyHelpRequest {
query: query.to_string(),
tool: tool.map(|t| t.to_string()),
};
let response = build_proxy_request(&client, reqwest::Method::POST, &url)
.json(&payload)
.send()
.await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_else(|_| "empty".into());
return Err(ProxyError::ProxyResponse {
status: status.as_u16(),
body,
});
}
let body: ProxyHelpResponse = response
.json()
.await
.map_err(|e| ProxyError::InvalidResponse(e.to_string()))?;
if let Some(err) = body.error {
return Err(ProxyError::ProxyResponse {
status: 200,
body: err,
});
}
Ok(body.content)
}