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) -> reqwest::RequestBuilder {
88 let mut req = client.request(method, url);
89 let env_name = token_env.unwrap_or("ATI_SESSION_TOKEN");
90 match crate::core::token::resolve_token(env_name) {
91 Ok(Some(token)) => {
92 req = req.header("Authorization", format!("Bearer {token}"));
93 }
94 Ok(None) if env_name != "ATI_SESSION_TOKEN" => {
95 tracing::debug!(
102 env = %env_name,
103 "per-provider token env unset; falling back to ATI_SESSION_TOKEN"
104 );
105 if let Ok(Some(token)) = crate::core::token::resolve_token("ATI_SESSION_TOKEN") {
106 req = req.header("Authorization", format!("Bearer {token}"));
107 }
108 }
109 Ok(None) => {}
110 Err(e) => {
111 tracing::debug!(
120 env = %env_name,
121 error = %e,
122 "session token file unreadable; trying ATI_SESSION_TOKEN fallback"
123 );
124 if env_name != "ATI_SESSION_TOKEN" {
125 if let Ok(Some(token)) = crate::core::token::resolve_token("ATI_SESSION_TOKEN") {
126 req = req.header("Authorization", format!("Bearer {token}"));
127 }
128 }
129 }
130 }
131 req
132}
133
134pub async fn call_tool(
149 proxy_url: &str,
150 tool_name: &str,
151 args: &HashMap<String, Value>,
152 raw_args: Option<&[String]>,
153 token_env: Option<&str>,
154) -> Result<Value, ProxyError> {
155 let client = Client::builder()
156 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
157 .build()?;
158
159 let url = format!("{}/call", proxy_url.trim_end_matches('/'));
160
161 let args_value = serde_json::to_value(args).unwrap_or(Value::Object(serde_json::Map::new()));
167 let raw_args_vec = raw_args.filter(|r| !r.is_empty()).map(|r| r.to_vec());
168
169 let payload = ProxyCallRequest {
170 tool_name: tool_name.to_string(),
171 args: args_value,
172 raw_args: raw_args_vec,
173 };
174
175 let response = build_proxy_request(&client, reqwest::Method::POST, &url, token_env)
176 .json(&payload)
177 .send()
178 .await?;
179 let status = response.status();
180
181 if !status.is_success() {
182 let body = response.text().await.unwrap_or_else(|_| "empty".into());
183 return Err(ProxyError::ProxyResponse {
184 status: status.as_u16(),
185 body,
186 });
187 }
188
189 let body: ProxyCallResponse = response
190 .json()
191 .await
192 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))?;
193
194 if let Some(err) = body.error {
195 return Err(ProxyError::ProxyResponse {
196 status: 200,
197 body: err,
198 });
199 }
200
201 Ok(body.result)
202}
203
204pub async fn list_tools(proxy_url: &str, query_params: &str) -> Result<Value, ProxyError> {
206 let client = Client::builder()
207 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
208 .build()?;
209 let mut url = format!("{}/tools", proxy_url.trim_end_matches('/'));
210 if !query_params.is_empty() {
211 url.push('?');
212 url.push_str(query_params);
213 }
214 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None)
215 .send()
216 .await?;
217 let status = response.status();
218 if !status.is_success() {
219 let body = response.text().await.unwrap_or_default();
220 return Err(ProxyError::ProxyResponse {
221 status: status.as_u16(),
222 body,
223 });
224 }
225 Ok(response.json().await?)
226}
227
228pub async fn get_tool_info(proxy_url: &str, name: &str) -> Result<Value, ProxyError> {
230 let client = Client::builder()
231 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
232 .build()?;
233 let url = format!("{}/tools/{}", proxy_url.trim_end_matches('/'), name);
234 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None)
235 .send()
236 .await?;
237 let status = response.status();
238 if !status.is_success() {
239 let body = response.text().await.unwrap_or_default();
240 return Err(ProxyError::ProxyResponse {
241 status: status.as_u16(),
242 body,
243 });
244 }
245 Ok(response.json().await?)
246}
247
248pub async fn call_mcp(
252 proxy_url: &str,
253 method: &str,
254 params: Option<Value>,
255 token_env: Option<&str>,
256) -> Result<Value, ProxyError> {
257 use std::sync::atomic::{AtomicU64, Ordering};
258 static MCP_ID: AtomicU64 = AtomicU64::new(1);
259
260 let id = MCP_ID.fetch_add(1, Ordering::SeqCst);
261 let msg = serde_json::json!({
262 "jsonrpc": "2.0",
263 "id": id,
264 "method": method,
265 "params": params,
266 });
267
268 let client = Client::builder()
269 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
270 .build()?;
271
272 let url = format!("{}/mcp", proxy_url.trim_end_matches('/'));
273
274 let response = build_proxy_request(&client, reqwest::Method::POST, &url, token_env)
275 .json(&msg)
276 .send()
277 .await?;
278 let status = response.status();
279
280 if status == reqwest::StatusCode::ACCEPTED {
281 return Ok(Value::Null);
282 }
283
284 if !status.is_success() {
285 let body = response.text().await.unwrap_or_else(|_| "empty".into());
286 return Err(ProxyError::ProxyResponse {
287 status: status.as_u16(),
288 body,
289 });
290 }
291
292 let body: Value = response
293 .json()
294 .await
295 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))?;
296
297 if let Some(err) = body.get("error") {
298 let message = err
299 .get("message")
300 .and_then(|m| m.as_str())
301 .unwrap_or("MCP proxy error");
302 return Err(ProxyError::ProxyResponse {
303 status: 200,
304 body: message.to_string(),
305 });
306 }
307
308 Ok(body.get("result").cloned().unwrap_or(Value::Null))
309}
310
311pub async fn list_skills(
313 proxy_url: &str,
314 query_params: &str,
315) -> Result<serde_json::Value, ProxyError> {
316 let client = Client::builder()
317 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
318 .build()?;
319
320 let url = if query_params.is_empty() {
321 format!("{}/skills", proxy_url.trim_end_matches('/'))
322 } else {
323 format!("{}/skills?{query_params}", proxy_url.trim_end_matches('/'))
324 };
325
326 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None)
327 .send()
328 .await?;
329 let status = response.status();
330
331 if !status.is_success() {
332 let body = response.text().await.unwrap_or_else(|_| "empty".into());
333 return Err(ProxyError::ProxyResponse {
334 status: status.as_u16(),
335 body,
336 });
337 }
338
339 response
340 .json()
341 .await
342 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))
343}
344
345pub async fn get_skill(
347 proxy_url: &str,
348 name: &str,
349 query_params: &str,
350) -> Result<serde_json::Value, ProxyError> {
351 let client = Client::builder()
352 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
353 .build()?;
354
355 let url = if query_params.is_empty() {
356 format!("{}/skills/{name}", proxy_url.trim_end_matches('/'))
357 } else {
358 format!(
359 "{}/skills/{name}?{query_params}",
360 proxy_url.trim_end_matches('/')
361 )
362 };
363
364 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None)
365 .send()
366 .await?;
367 let status = response.status();
368
369 if !status.is_success() {
370 let body = response.text().await.unwrap_or_else(|_| "empty".into());
371 return Err(ProxyError::ProxyResponse {
372 status: status.as_u16(),
373 body,
374 });
375 }
376
377 response
378 .json()
379 .await
380 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))
381}
382
383async fn get_proxy_json(proxy_url: &str, path: &str) -> Result<serde_json::Value, ProxyError> {
384 let client = Client::builder()
385 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
386 .build()?;
387
388 let url = format!(
389 "{}/{}",
390 proxy_url.trim_end_matches('/'),
391 path.trim_start_matches('/')
392 );
393
394 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None)
395 .send()
396 .await?;
397 let status = response.status();
398
399 if !status.is_success() {
400 let body = response.text().await.unwrap_or_else(|_| "empty".into());
401 return Err(ProxyError::ProxyResponse {
402 status: status.as_u16(),
403 body,
404 });
405 }
406
407 response
408 .json()
409 .await
410 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))
411}
412
413async fn get_proxy_json_with_query(
414 proxy_url: &str,
415 path: &str,
416 query: &[(&str, String)],
417) -> Result<serde_json::Value, ProxyError> {
418 let client = Client::builder()
419 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
420 .build()?;
421
422 let mut url = format!(
423 "{}/{}",
424 proxy_url.trim_end_matches('/'),
425 path.trim_start_matches('/')
426 );
427
428 if !query.is_empty() {
429 let params = query
430 .iter()
431 .map(|(key, value)| format!("{key}={}", urlencoding(value)))
432 .collect::<Vec<_>>()
433 .join("&");
434 url.push('?');
435 url.push_str(¶ms);
436 }
437
438 let response = build_proxy_request(&client, reqwest::Method::GET, &url, None)
439 .send()
440 .await?;
441 let status = response.status();
442
443 if !status.is_success() {
444 let body = response.text().await.unwrap_or_else(|_| "empty".into());
445 return Err(ProxyError::ProxyResponse {
446 status: status.as_u16(),
447 body,
448 });
449 }
450
451 response
452 .json()
453 .await
454 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))
455}
456
457pub async fn get_skillati_catalog(
459 proxy_url: &str,
460 search: Option<&str>,
461) -> Result<serde_json::Value, ProxyError> {
462 let query = search
463 .map(|value| vec![("search", value.to_string())])
464 .unwrap_or_default();
465 get_proxy_json_with_query(proxy_url, "skillati/catalog", &query).await
466}
467
468pub async fn get_skillati_read(
470 proxy_url: &str,
471 name: &str,
472) -> Result<serde_json::Value, ProxyError> {
473 get_proxy_json(proxy_url, &format!("skillati/{}", urlencoding(name))).await
474}
475
476pub async fn get_skillati_resources(
478 proxy_url: &str,
479 name: &str,
480 prefix: Option<&str>,
481) -> Result<serde_json::Value, ProxyError> {
482 let query = prefix
483 .map(|value| vec![("prefix", value.to_string())])
484 .unwrap_or_default();
485 get_proxy_json_with_query(
486 proxy_url,
487 &format!("skillati/{}/resources", urlencoding(name)),
488 &query,
489 )
490 .await
491}
492
493pub async fn get_skillati_file(
495 proxy_url: &str,
496 name: &str,
497 path: &str,
498) -> Result<serde_json::Value, ProxyError> {
499 get_proxy_json_with_query(
500 proxy_url,
501 &format!("skillati/{}/file", urlencoding(name)),
502 &[("path", path.to_string())],
503 )
504 .await
505}
506
507pub async fn get_skillati_refs(
509 proxy_url: &str,
510 name: &str,
511) -> Result<serde_json::Value, ProxyError> {
512 get_proxy_json(proxy_url, &format!("skillati/{}/refs", urlencoding(name))).await
513}
514
515pub async fn get_skillati_ref(
517 proxy_url: &str,
518 name: &str,
519 reference: &str,
520) -> Result<serde_json::Value, ProxyError> {
521 get_proxy_json(
522 proxy_url,
523 &format!(
524 "skillati/{}/ref/{}",
525 urlencoding(name),
526 urlencoding(reference)
527 ),
528 )
529 .await
530}
531
532fn urlencoding(s: &str) -> String {
533 s.replace('%', "%25")
534 .replace(' ', "%20")
535 .replace('#', "%23")
536 .replace('&', "%26")
537 .replace('?', "%3F")
538 .replace('/', "%2F")
539 .replace('=', "%3D")
540}
541
542pub async fn resolve_skills(
544 proxy_url: &str,
545 scopes: &serde_json::Value,
546) -> Result<serde_json::Value, ProxyError> {
547 let client = Client::builder()
548 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
549 .build()?;
550
551 let url = format!("{}/skills/resolve", proxy_url.trim_end_matches('/'));
552
553 let response = build_proxy_request(&client, reqwest::Method::POST, &url, None)
554 .json(scopes)
555 .send()
556 .await?;
557 let status = response.status();
558
559 if !status.is_success() {
560 let body = response.text().await.unwrap_or_else(|_| "empty".into());
561 return Err(ProxyError::ProxyResponse {
562 status: status.as_u16(),
563 body,
564 });
565 }
566
567 response
568 .json()
569 .await
570 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))
571}
572
573pub async fn call_help(
575 proxy_url: &str,
576 query: &str,
577 tool: Option<&str>,
578) -> Result<String, ProxyError> {
579 let client = Client::builder()
580 .timeout(Duration::from_secs(PROXY_TIMEOUT_SECS))
581 .build()?;
582
583 let url = format!("{}/help", proxy_url.trim_end_matches('/'));
584
585 let payload = ProxyHelpRequest {
586 query: query.to_string(),
587 tool: tool.map(|t| t.to_string()),
588 };
589
590 let response = build_proxy_request(&client, reqwest::Method::POST, &url, None)
591 .json(&payload)
592 .send()
593 .await?;
594 let status = response.status();
595
596 if !status.is_success() {
597 let body = response.text().await.unwrap_or_else(|_| "empty".into());
598 return Err(ProxyError::ProxyResponse {
599 status: status.as_u16(),
600 body,
601 });
602 }
603
604 let body: ProxyHelpResponse = response
605 .json()
606 .await
607 .map_err(|e| ProxyError::InvalidResponse(e.to_string()))?;
608
609 if let Some(err) = body.error {
610 return Err(ProxyError::ProxyResponse {
611 status: 200,
612 body: err,
613 });
614 }
615
616 Ok(body.content)
617}