1use reqwest::Client;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::time::Duration;
11use thiserror::Error;
12
13#[derive(Error, Debug)]
14pub enum ProxyError {
15 #[error("Proxy request failed: {0}")]
16 Request(#[from] reqwest::Error),
17 #[error("Proxy error ({status}): {body}")]
18 ProxyResponse { status: u16, body: String },
19 #[error("Invalid proxy URL: {0}")]
20 InvalidUrl(String),
21 #[error("Proxy returned invalid response: {0}")]
22 InvalidResponse(String),
23}
24
25#[derive(Debug, Serialize)]
27pub struct ProxyCallRequest {
28 pub tool_name: String,
29 pub args: Value,
31 #[serde(skip_serializing_if = "Option::is_none")]
36 pub raw_args: Option<Vec<String>>,
37}
38
39#[derive(Debug, Deserialize)]
41pub struct ProxyCallResponse {
42 pub result: Value,
43 #[serde(default)]
44 pub error: Option<String>,
45}
46
47#[derive(Debug, Serialize)]
49pub struct ProxyHelpRequest {
50 pub query: String,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub tool: Option<String>,
53}
54
55#[derive(Debug, Deserialize)]
57pub struct ProxyHelpResponse {
58 pub content: String,
59 #[serde(default)]
60 pub error: Option<String>,
61}
62
63const PROXY_TIMEOUT_SECS: u64 = 120;
64
65fn build_proxy_request(
83 client: &Client,
84 method: reqwest::Method,
85 url: &str,
86 token_env: Option<&str>,
87 override_mcp_url: Option<&str>,
88) -> reqwest::RequestBuilder {
89 let mut req = client.request(method, url);
90 if let Some(upstream) = override_mcp_url {
95 req = req.header("X-Ati-Upstream-Url", upstream);
96 }
97 let env_name = token_env.unwrap_or("ATI_SESSION_TOKEN");
98 match crate::core::token::resolve_token(env_name) {
99 Ok(Some(token)) => {
100 req = req.header("Authorization", format!("Bearer {token}"));
101 }
102 Ok(None) if env_name != "ATI_SESSION_TOKEN" => {
103 tracing::debug!(
110 env = %env_name,
111 "per-provider token env unset; falling back to ATI_SESSION_TOKEN"
112 );
113 if let Ok(Some(token)) = crate::core::token::resolve_token("ATI_SESSION_TOKEN") {
114 req = req.header("Authorization", format!("Bearer {token}"));
115 }
116 }
117 Ok(None) => {}
118 Err(e) => {
119 tracing::debug!(
128 env = %env_name,
129 error = %e,
130 "session token file unreadable; trying ATI_SESSION_TOKEN fallback"
131 );
132 if env_name != "ATI_SESSION_TOKEN" {
133 if let Ok(Some(token)) = crate::core::token::resolve_token("ATI_SESSION_TOKEN") {
134 req = req.header("Authorization", format!("Bearer {token}"));
135 }
136 }
137 }
138 }
139 req
140}
141
142pub async fn call_tool(
164 proxy_url: &str,
165 tool_name: &str,
166 args: &HashMap<String, Value>,
167 raw_args: Option<&[String]>,
168 token_env: Option<&str>,
169 override_mcp_url: Option<&str>,
170) -> Result<Value, ProxyError> {
171 let client = Client::builder()
172 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
173 .build()?;
174
175 let url = format!("{}/call", proxy_url.trim_end_matches('/'));
176
177 let args_value = serde_json::to_value(args).unwrap_or(Value::Object(serde_json::Map::new()));
183 let raw_args_vec = raw_args.filter(|r| !r.is_empty()).map(|r| r.to_vec());
184
185 let payload = ProxyCallRequest {
186 tool_name: tool_name.to_string(),
187 args: args_value,
188 raw_args: raw_args_vec,
189 };
190
191 let response = build_proxy_request(
192 &client,
193 reqwest::Method::POST,
194 &url,
195 token_env,
196 override_mcp_url,
197 )
198 .json(&payload)
199 .send()
200 .await?;
201 let status = response.status();
202
203 if !status.is_success() {
204 let body = response.text().await.unwrap_or_else(|_| "empty".into());
205 return Err(ProxyError::ProxyResponse {
206 status: status.as_u16(),
207 body,
208 });
209 }
210
211 let body: ProxyCallResponse = response
212 .json()
213 .await
214 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))?;
215
216 if let Some(err) = body.error {
217 return Err(ProxyError::ProxyResponse {
218 status: 200,
219 body: err,
220 });
221 }
222
223 Ok(body.result)
224}
225
226pub async fn list_tools(proxy_url: &str, query_params: &str) -> Result<Value, ProxyError> {
228 let client = Client::builder()
229 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
230 .build()?;
231 let mut url = format!("{}/tools", proxy_url.trim_end_matches('/'));
232 if !query_params.is_empty() {
233 url.push('?');
234 url.push_str(query_params);
235 }
236 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None, None)
237 .send()
238 .await?;
239 let status = response.status();
240 if !status.is_success() {
241 let body = response.text().await.unwrap_or_default();
242 return Err(ProxyError::ProxyResponse {
243 status: status.as_u16(),
244 body,
245 });
246 }
247 Ok(response.json().await?)
248}
249
250pub async fn get_tool_info(proxy_url: &str, name: &str) -> Result<Value, ProxyError> {
252 let client = Client::builder()
253 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
254 .build()?;
255 let url = format!("{}/tools/{}", proxy_url.trim_end_matches('/'), name);
256 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None, None)
257 .send()
258 .await?;
259 let status = response.status();
260 if !status.is_success() {
261 let body = response.text().await.unwrap_or_default();
262 return Err(ProxyError::ProxyResponse {
263 status: status.as_u16(),
264 body,
265 });
266 }
267 Ok(response.json().await?)
268}
269
270pub async fn call_mcp(
276 proxy_url: &str,
277 method: &str,
278 params: Option<Value>,
279 token_env: Option<&str>,
280 override_mcp_url: Option<&str>,
281) -> Result<Value, ProxyError> {
282 use std::sync::atomic::{AtomicU64, Ordering};
283 static MCP_ID: AtomicU64 = AtomicU64::new(1);
284
285 let id = MCP_ID.fetch_add(1, Ordering::SeqCst);
286 let msg = serde_json::json!({
287 "jsonrpc": "2.0",
288 "id": id,
289 "method": method,
290 "params": params,
291 });
292
293 let client = Client::builder()
294 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
295 .build()?;
296
297 let url = format!("{}/mcp", proxy_url.trim_end_matches('/'));
298
299 let response = build_proxy_request(
300 &client,
301 reqwest::Method::POST,
302 &url,
303 token_env,
304 override_mcp_url,
305 )
306 .json(&msg)
307 .send()
308 .await?;
309 let status = response.status();
310
311 if status == reqwest::StatusCode::ACCEPTED {
312 return Ok(Value::Null);
313 }
314
315 if !status.is_success() {
316 let body = response.text().await.unwrap_or_else(|_| "empty".into());
317 return Err(ProxyError::ProxyResponse {
318 status: status.as_u16(),
319 body,
320 });
321 }
322
323 let body: Value = response
324 .json()
325 .await
326 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))?;
327
328 if let Some(err) = body.get("error") {
329 let message = err
330 .get("message")
331 .and_then(|m| m.as_str())
332 .unwrap_or("MCP proxy error");
333 return Err(ProxyError::ProxyResponse {
334 status: 200,
335 body: message.to_string(),
336 });
337 }
338
339 Ok(body.get("result").cloned().unwrap_or(Value::Null))
340}
341
342pub async fn list_skills(
344 proxy_url: &str,
345 query_params: &str,
346) -> Result<serde_json::Value, ProxyError> {
347 let client = Client::builder()
348 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
349 .build()?;
350
351 let url = if query_params.is_empty() {
352 format!("{}/skills", proxy_url.trim_end_matches('/'))
353 } else {
354 format!("{}/skills?{query_params}", proxy_url.trim_end_matches('/'))
355 };
356
357 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None, None)
358 .send()
359 .await?;
360 let status = response.status();
361
362 if !status.is_success() {
363 let body = response.text().await.unwrap_or_else(|_| "empty".into());
364 return Err(ProxyError::ProxyResponse {
365 status: status.as_u16(),
366 body,
367 });
368 }
369
370 response
371 .json()
372 .await
373 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))
374}
375
376pub async fn get_skill(
378 proxy_url: &str,
379 name: &str,
380 query_params: &str,
381) -> Result<serde_json::Value, ProxyError> {
382 let client = Client::builder()
383 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
384 .build()?;
385
386 let url = if query_params.is_empty() {
387 format!("{}/skills/{name}", proxy_url.trim_end_matches('/'))
388 } else {
389 format!(
390 "{}/skills/{name}?{query_params}",
391 proxy_url.trim_end_matches('/')
392 )
393 };
394
395 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None, None)
396 .send()
397 .await?;
398 let status = response.status();
399
400 if !status.is_success() {
401 let body = response.text().await.unwrap_or_else(|_| "empty".into());
402 return Err(ProxyError::ProxyResponse {
403 status: status.as_u16(),
404 body,
405 });
406 }
407
408 response
409 .json()
410 .await
411 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))
412}
413
414async fn get_proxy_json(proxy_url: &str, path: &str) -> Result<serde_json::Value, ProxyError> {
415 let client = Client::builder()
416 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
417 .build()?;
418
419 let url = format!(
420 "{}/{}",
421 proxy_url.trim_end_matches('/'),
422 path.trim_start_matches('/')
423 );
424
425 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None, None)
426 .send()
427 .await?;
428 let status = response.status();
429
430 if !status.is_success() {
431 let body = response.text().await.unwrap_or_else(|_| "empty".into());
432 return Err(ProxyError::ProxyResponse {
433 status: status.as_u16(),
434 body,
435 });
436 }
437
438 response
439 .json()
440 .await
441 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))
442}
443
444async fn get_proxy_json_with_query(
445 proxy_url: &str,
446 path: &str,
447 query: &[(&str, String)],
448) -> Result<serde_json::Value, ProxyError> {
449 let client = Client::builder()
450 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
451 .build()?;
452
453 let mut url = format!(
454 "{}/{}",
455 proxy_url.trim_end_matches('/'),
456 path.trim_start_matches('/')
457 );
458
459 if !query.is_empty() {
460 let params = query
461 .iter()
462 .map(|(key, value)| format!("{key}={}", urlencoding(value)))
463 .collect::<Vec<_>>()
464 .join("&");
465 url.push('?');
466 url.push_str(¶ms);
467 }
468
469 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None, None)
470 .send()
471 .await?;
472 let status = response.status();
473
474 if !status.is_success() {
475 let body = response.text().await.unwrap_or_else(|_| "empty".into());
476 return Err(ProxyError::ProxyResponse {
477 status: status.as_u16(),
478 body,
479 });
480 }
481
482 response
483 .json()
484 .await
485 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))
486}
487
488pub async fn get_skillati_catalog(
490 proxy_url: &str,
491 search: Option<&str>,
492) -> Result<serde_json::Value, ProxyError> {
493 let query = search
494 .map(|value| vec![("search", value.to_string())])
495 .unwrap_or_default();
496 get_proxy_json_with_query(proxy_url, "skillati/catalog", &query).await
497}
498
499pub async fn get_skillati_read(
501 proxy_url: &str,
502 name: &str,
503) -> Result<serde_json::Value, ProxyError> {
504 get_proxy_json(proxy_url, &format!("skillati/{}", urlencoding(name))).await
505}
506
507pub async fn get_skillati_resources(
509 proxy_url: &str,
510 name: &str,
511 prefix: Option<&str>,
512) -> Result<serde_json::Value, ProxyError> {
513 let query = prefix
514 .map(|value| vec![("prefix", value.to_string())])
515 .unwrap_or_default();
516 get_proxy_json_with_query(
517 proxy_url,
518 &format!("skillati/{}/resources", urlencoding(name)),
519 &query,
520 )
521 .await
522}
523
524pub async fn get_skillati_file(
526 proxy_url: &str,
527 name: &str,
528 path: &str,
529) -> Result<serde_json::Value, ProxyError> {
530 get_proxy_json_with_query(
531 proxy_url,
532 &format!("skillati/{}/file", urlencoding(name)),
533 &[("path", path.to_string())],
534 )
535 .await
536}
537
538pub async fn get_skillati_refs(
540 proxy_url: &str,
541 name: &str,
542) -> Result<serde_json::Value, ProxyError> {
543 get_proxy_json(proxy_url, &format!("skillati/{}/refs", urlencoding(name))).await
544}
545
546pub async fn get_skillati_ref(
548 proxy_url: &str,
549 name: &str,
550 reference: &str,
551) -> Result<serde_json::Value, ProxyError> {
552 get_proxy_json(
553 proxy_url,
554 &format!(
555 "skillati/{}/ref/{}",
556 urlencoding(name),
557 urlencoding(reference)
558 ),
559 )
560 .await
561}
562
563fn urlencoding(s: &str) -> String {
564 s.replace('%', "%25")
565 .replace(' ', "%20")
566 .replace('#', "%23")
567 .replace('&', "%26")
568 .replace('?', "%3F")
569 .replace('/', "%2F")
570 .replace('=', "%3D")
571}
572
573pub async fn resolve_skills(
575 proxy_url: &str,
576 scopes: &serde_json::Value,
577) -> Result<serde_json::Value, ProxyError> {
578 let client = Client::builder()
579 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
580 .build()?;
581
582 let url = format!("{}/skills/resolve", proxy_url.trim_end_matches('/'));
583
584 let response = build_proxy_request(&client, reqwest::Method::POST, &url, None, None)
585 .json(scopes)
586 .send()
587 .await?;
588 let status = response.status();
589
590 if !status.is_success() {
591 let body = response.text().await.unwrap_or_else(|_| "empty".into());
592 return Err(ProxyError::ProxyResponse {
593 status: status.as_u16(),
594 body,
595 });
596 }
597
598 response
599 .json()
600 .await
601 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))
602}
603
604pub async fn call_help(
606 proxy_url: &str,
607 query: &str,
608 tool: Option<&str>,
609) -> Result<String, ProxyError> {
610 let client = Client::builder()
611 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
612 .build()?;
613
614 let url = format!("{}/help", proxy_url.trim_end_matches('/'));
615
616 let payload = ProxyHelpRequest {
617 query: query.to_string(),
618 tool: tool.map(|t| t.to_string()),
619 };
620
621 let response = build_proxy_request(&client, reqwest::Method::POST, &url, None, None)
622 .json(&payload)
623 .send()
624 .await?;
625 let status = response.status();
626
627 if !status.is_success() {
628 let body = response.text().await.unwrap_or_else(|_| "empty".into());
629 return Err(ProxyError::ProxyResponse {
630 status: status.as_u16(),
631 body,
632 });
633 }
634
635 let body: ProxyHelpResponse = response
636 .json()
637 .await
638 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))?;
639
640 if let Some(err) = body.error {
641 return Err(ProxyError::ProxyResponse {
642 status: 200,
643 body: err,
644 });
645 }
646
647 Ok(body.content)
648}