1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::Path;
5use thiserror::Error;
6
7pub const TOOL_SEP: char = ':';
10pub const TOOL_SEP_STR: &str = ":";
11
12#[derive(Error, Debug)]
13pub enum ManifestError {
14 #[error("Failed to read manifest file {0}: {1}")]
15 Io(String, std::io::Error),
16 #[error("Failed to parse manifest {0}: {1}")]
17 Parse(String, toml::de::Error),
18 #[error("No manifests directory found at {0}")]
19 NoDirectory(String),
20}
21
22#[derive(Debug, Clone, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum AuthType {
25 Bearer,
26 Header,
27 Query,
28 Basic,
29 None,
30 Oauth2,
31}
32
33impl Default for AuthType {
34 fn default() -> Self {
35 AuthType::None
36 }
37}
38
39#[derive(Debug, Clone, Deserialize)]
40pub struct Provider {
41 pub name: String,
42 pub description: String,
43 #[serde(default)]
45 pub base_url: String,
46 #[serde(default)]
47 pub auth_type: AuthType,
48 #[serde(default)]
49 pub auth_key_name: Option<String>,
50 #[serde(default)]
53 pub auth_header_name: Option<String>,
54 #[serde(default)]
56 pub auth_query_name: Option<String>,
57 #[serde(default)]
60 pub auth_value_prefix: Option<String>,
61 #[serde(default)]
64 pub extra_headers: HashMap<String, String>,
65 #[serde(default)]
67 pub oauth2_token_url: Option<String>,
68 #[serde(default)]
70 pub auth_secret_name: Option<String>,
71 #[serde(default)]
74 pub oauth2_basic_auth: bool,
75 #[serde(default)]
76 pub internal: bool,
77 #[serde(default = "default_handler")]
78 pub handler: String,
79
80 #[serde(default)]
83 pub mcp_transport: Option<String>,
84 #[serde(default)]
86 pub mcp_command: Option<String>,
87 #[serde(default)]
89 pub mcp_args: Vec<String>,
90 #[serde(default)]
92 pub mcp_url: Option<String>,
93 #[serde(default)]
95 pub mcp_env: HashMap<String, String>,
96
97 #[serde(default)]
100 pub cli_command: Option<String>,
101 #[serde(default)]
103 pub cli_default_args: Vec<String>,
104 #[serde(default)]
106 pub cli_env: HashMap<String, String>,
107 #[serde(default)]
109 pub cli_timeout_secs: Option<u64>,
110
111 #[serde(default)]
114 pub openapi_spec: Option<String>,
115 #[serde(default)]
117 pub openapi_include_tags: Vec<String>,
118 #[serde(default)]
120 pub openapi_exclude_tags: Vec<String>,
121 #[serde(default)]
123 pub openapi_include_operations: Vec<String>,
124 #[serde(default)]
126 pub openapi_exclude_operations: Vec<String>,
127 #[serde(default)]
129 pub openapi_max_operations: Option<usize>,
130 #[serde(default)]
132 pub openapi_overrides: HashMap<String, OpenApiToolOverride>,
133
134 #[serde(default)]
138 pub auth_generator: Option<AuthGenerator>,
139
140 #[serde(default)]
143 pub category: Option<String>,
144
145 #[serde(default)]
148 pub skills: Vec<String>,
149}
150
151fn default_handler() -> String {
152 "http".to_string()
153}
154
155#[derive(Debug, Clone, Deserialize, Default)]
157pub struct OpenApiToolOverride {
158 pub hint: Option<String>,
159 #[serde(default)]
160 pub tags: Vec<String>,
161 #[serde(default)]
162 pub examples: Vec<String>,
163 pub description: Option<String>,
164 pub scope: Option<String>,
165 pub response_extract: Option<String>,
166 pub response_format: Option<String>,
167}
168
169#[derive(Debug, Clone, Deserialize)]
182pub struct AuthGenerator {
183 #[serde(rename = "type")]
184 pub gen_type: AuthGenType,
185 pub command: Option<String>,
187 #[serde(default)]
189 pub args: Vec<String>,
190 pub interpreter: Option<String>,
192 pub script: Option<String>,
194 #[serde(default)]
196 pub cache_ttl_secs: u64,
197 #[serde(default)]
199 pub output_format: AuthOutputFormat,
200 #[serde(default)]
202 pub env: HashMap<String, String>,
203 #[serde(default)]
205 pub inject: HashMap<String, InjectTarget>,
206 #[serde(default = "default_gen_timeout")]
208 pub timeout_secs: u64,
209}
210
211fn default_gen_timeout() -> u64 {
212 30
213}
214
215#[derive(Debug, Clone, Deserialize)]
216#[serde(rename_all = "snake_case")]
217pub enum AuthGenType {
218 Command,
219 Script,
220}
221
222#[derive(Debug, Clone, Deserialize, Default)]
223#[serde(rename_all = "snake_case")]
224pub enum AuthOutputFormat {
225 #[default]
226 Text,
227 Json,
228}
229
230#[derive(Debug, Clone, Deserialize)]
232pub struct InjectTarget {
233 #[serde(rename = "type")]
235 pub inject_type: String,
236 pub name: String,
238}
239
240#[derive(Debug, Clone, Deserialize)]
241#[serde(rename_all = "UPPERCASE")]
242pub enum HttpMethod {
243 #[serde(alias = "get", alias = "Get")]
244 Get,
245 #[serde(alias = "post", alias = "Post")]
246 Post,
247 #[serde(alias = "put", alias = "Put")]
248 Put,
249 #[serde(alias = "delete", alias = "Delete")]
250 Delete,
251}
252
253impl Default for HttpMethod {
254 fn default() -> Self {
255 HttpMethod::Get
256 }
257}
258
259impl std::fmt::Display for HttpMethod {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 match self {
262 HttpMethod::Get => write!(f, "GET"),
263 HttpMethod::Post => write!(f, "POST"),
264 HttpMethod::Put => write!(f, "PUT"),
265 HttpMethod::Delete => write!(f, "DELETE"),
266 }
267 }
268}
269
270#[derive(Debug, Clone, Deserialize, Default)]
271#[serde(rename_all = "snake_case")]
272pub enum ResponseFormat {
273 MarkdownTable,
274 Json,
275 #[default]
276 Text,
277 Raw,
278}
279
280#[derive(Debug, Clone, Deserialize, Default)]
281pub struct ResponseConfig {
282 #[serde(default)]
284 pub extract: Option<String>,
285 #[serde(default)]
287 pub format: ResponseFormat,
288}
289
290#[derive(Debug, Clone, Deserialize)]
291pub struct Tool {
292 pub name: String,
293 pub description: String,
294 #[serde(default)]
295 pub endpoint: String,
296 #[serde(default)]
297 pub method: HttpMethod,
298 #[serde(default)]
300 pub scope: Option<String>,
301 #[serde(default)]
303 pub input_schema: Option<serde_json::Value>,
304 #[serde(default)]
306 pub response: Option<ResponseConfig>,
307
308 #[serde(default)]
311 pub tags: Vec<String>,
312 #[serde(default)]
314 pub hint: Option<String>,
315 #[serde(default)]
317 pub examples: Vec<String>,
318}
319
320#[derive(Debug, Clone, Deserialize)]
323pub struct Manifest {
324 pub provider: Provider,
325 #[serde(default, rename = "tools")]
326 pub tools: Vec<Tool>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct CachedProvider {
334 pub name: String,
335 pub provider_type: String,
337 #[serde(default)]
338 pub base_url: String,
339 #[serde(default)]
340 pub auth_type: String,
341 #[serde(default)]
342 pub auth_key_name: Option<String>,
343 #[serde(default)]
344 pub auth_header_name: Option<String>,
345 #[serde(default)]
346 pub auth_query_name: Option<String>,
347 #[serde(default)]
349 pub spec_content: Option<String>,
350 #[serde(default)]
352 pub mcp_transport: Option<String>,
353 #[serde(default)]
354 pub mcp_url: Option<String>,
355 #[serde(default)]
356 pub mcp_command: Option<String>,
357 #[serde(default)]
358 pub mcp_args: Vec<String>,
359 #[serde(default)]
360 pub mcp_env: HashMap<String, String>,
361 #[serde(default)]
363 pub cli_command: Option<String>,
364 #[serde(default)]
365 pub cli_default_args: Vec<String>,
366 #[serde(default)]
367 pub cli_env: HashMap<String, String>,
368 #[serde(default)]
369 pub cli_timeout_secs: Option<u64>,
370 #[serde(default)]
372 pub auth: Option<String>,
373 pub created_at: String,
375 pub ttl_seconds: u64,
376}
377
378impl CachedProvider {
379 pub fn is_expired(&self) -> bool {
381 let created = match DateTime::parse_from_rfc3339(&self.created_at) {
382 Ok(dt) => dt.with_timezone(&Utc),
383 Err(_) => return true, };
385 let now = Utc::now();
386 let elapsed = now.signed_duration_since(created);
387 elapsed.num_seconds() as u64 > self.ttl_seconds
388 }
389
390 pub fn expires_at(&self) -> Option<String> {
392 let created = DateTime::parse_from_rfc3339(&self.created_at).ok()?;
393 let expires = created + chrono::Duration::seconds(self.ttl_seconds as i64);
394 Some(expires.to_rfc3339())
395 }
396
397 pub fn remaining_seconds(&self) -> u64 {
399 let created = match DateTime::parse_from_rfc3339(&self.created_at) {
400 Ok(dt) => dt.with_timezone(&Utc),
401 Err(_) => return 0,
402 };
403 let now = Utc::now();
404 let elapsed = now.signed_duration_since(created).num_seconds() as u64;
405 self.ttl_seconds.saturating_sub(elapsed)
406 }
407
408 pub fn to_provider(&self) -> Provider {
410 let auth_type = match self.auth_type.as_str() {
411 "bearer" => AuthType::Bearer,
412 "header" => AuthType::Header,
413 "query" => AuthType::Query,
414 "basic" => AuthType::Basic,
415 "oauth2" => AuthType::Oauth2,
416 _ => AuthType::None,
417 };
418
419 let handler = match self.provider_type.as_str() {
420 "mcp" => "mcp".to_string(),
421 "openapi" => "openapi".to_string(),
422 _ => "http".to_string(),
423 };
424
425 Provider {
426 name: self.name.clone(),
427 description: format!("{} (cached)", self.name),
428 base_url: self.base_url.clone(),
429 auth_type,
430 auth_key_name: self.auth_key_name.clone(),
431 auth_header_name: self.auth_header_name.clone(),
432 auth_query_name: self.auth_query_name.clone(),
433 auth_value_prefix: None,
434 extra_headers: HashMap::new(),
435 oauth2_token_url: None,
436 auth_secret_name: None,
437 oauth2_basic_auth: false,
438 internal: false,
439 handler,
440 mcp_transport: self.mcp_transport.clone(),
441 mcp_command: self.mcp_command.clone(),
442 mcp_args: self.mcp_args.clone(),
443 mcp_url: self.mcp_url.clone(),
444 mcp_env: self.mcp_env.clone(),
445 openapi_spec: None,
446 openapi_include_tags: Vec::new(),
447 openapi_exclude_tags: Vec::new(),
448 openapi_include_operations: Vec::new(),
449 openapi_exclude_operations: Vec::new(),
450 openapi_max_operations: None,
451 openapi_overrides: HashMap::new(),
452 cli_command: self.cli_command.clone(),
453 cli_default_args: self.cli_default_args.clone(),
454 cli_env: self.cli_env.clone(),
455 cli_timeout_secs: self.cli_timeout_secs,
456 auth_generator: None,
457 category: None,
458 skills: Vec::new(),
459 }
460 }
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
466pub struct McpToolDef {
467 pub name: String,
468 #[serde(default)]
469 pub description: Option<String>,
470 #[serde(default, rename = "inputSchema")]
471 pub input_schema: Option<serde_json::Value>,
472}
473
474pub struct ManifestRegistry {
476 manifests: Vec<Manifest>,
477 tool_index: HashMap<String, (usize, usize)>,
479}
480
481impl ManifestRegistry {
482 pub fn load(dir: &Path) -> Result<Self, ManifestError> {
485 if !dir.is_dir() {
486 return Err(ManifestError::NoDirectory(dir.display().to_string()));
487 }
488
489 let mut manifests = Vec::new();
490 let mut tool_index = HashMap::new();
491
492 let pattern = dir.join("*.toml");
493 let entries = glob::glob(pattern.to_str().unwrap_or(""))
494 .map_err(|e| ManifestError::NoDirectory(e.to_string()))?;
495
496 let specs_dir = dir.parent().map(|p| p.join("specs"));
498
499 for entry in entries {
500 let path = entry.map_err(|e| {
501 ManifestError::Io(format!("{e}"), std::io::Error::other("glob error"))
502 })?;
503 let contents = std::fs::read_to_string(&path)
504 .map_err(|e| ManifestError::Io(path.display().to_string(), e))?;
505 let mut manifest: Manifest = toml::from_str(&contents)
506 .map_err(|e| ManifestError::Parse(path.display().to_string(), e))?;
507
508 if manifest.provider.is_openapi() {
510 if let Some(spec_ref) = &manifest.provider.openapi_spec {
511 match crate::core::openapi::load_and_register(
512 &manifest.provider,
513 spec_ref,
514 specs_dir.as_deref(),
515 ) {
516 Ok(tools) => {
517 manifest.tools = tools;
518 }
519 Err(e) => {
520 eprintln!(
521 "Warning: failed to load OpenAPI spec for provider '{}': {e}",
522 manifest.provider.name
523 );
524 }
526 }
527 }
528 }
529
530 if manifest.provider.is_cli() && manifest.tools.is_empty() {
532 manifest.tools.push(Tool {
533 name: manifest.provider.name.clone(),
534 description: manifest.provider.description.clone(),
535 endpoint: String::new(),
536 method: HttpMethod::Get,
537 scope: None,
538 input_schema: None,
539 response: None,
540 tags: Vec::new(),
541 hint: None,
542 examples: Vec::new(),
543 });
544 }
545
546 let mi = manifests.len();
547 for (ti, tool) in manifest.tools.iter().enumerate() {
548 tool_index.insert(tool.name.clone(), (mi, ti));
549 }
550 manifests.push(manifest);
551 }
552
553 if let Some(parent) = dir.parent() {
556 let cache_dir = parent.join("cache").join("providers");
557 if cache_dir.is_dir() {
558 let cache_pattern = cache_dir.join("*.json");
559 if let Ok(cache_entries) = glob::glob(cache_pattern.to_str().unwrap_or("")) {
560 for entry in cache_entries {
561 let path = match entry {
562 Ok(p) => p,
563 Err(_) => continue,
564 };
565 let content = match std::fs::read_to_string(&path) {
566 Ok(c) => c,
567 Err(_) => continue,
568 };
569 let cached: CachedProvider = match serde_json::from_str(&content) {
570 Ok(c) => c,
571 Err(_) => continue,
572 };
573
574 if cached.is_expired() {
576 let _ = std::fs::remove_file(&path);
577 continue;
578 }
579
580 if manifests.iter().any(|m| m.provider.name == cached.name) {
582 continue;
583 }
584
585 let provider = cached.to_provider();
586
587 let mut cached_tools = Vec::new();
588 if cached.provider_type == "openapi" {
589 if let Some(spec_content) = &cached.spec_content {
590 if let Ok(spec) = crate::core::openapi::parse_spec(spec_content) {
591 let filters = crate::core::openapi::OpenApiFilters {
592 include_tags: vec![],
593 exclude_tags: vec![],
594 include_operations: vec![],
595 exclude_operations: vec![],
596 max_operations: None,
597 };
598 let defs = crate::core::openapi::extract_tools(&spec, &filters);
599 cached_tools = defs
600 .into_iter()
601 .map(|def| {
602 crate::core::openapi::to_ati_tool(
603 def,
604 &cached.name,
605 &HashMap::new(),
606 )
607 })
608 .collect();
609 }
610 }
611 }
612 let mi = manifests.len();
615 for (ti, tool) in cached_tools.iter().enumerate() {
616 tool_index.insert(tool.name.clone(), (mi, ti));
617 }
618 manifests.push(Manifest {
619 provider,
620 tools: cached_tools,
621 });
622 }
623 }
624 }
625 }
626
627 Ok(ManifestRegistry {
628 manifests,
629 tool_index,
630 })
631 }
632
633 pub fn empty() -> Self {
635 ManifestRegistry {
636 manifests: Vec::new(),
637 tool_index: HashMap::new(),
638 }
639 }
640
641 pub fn get_tool(&self, name: &str) -> Option<(&Provider, &Tool)> {
643 self.tool_index.get(name).map(|(mi, ti)| {
644 let m = &self.manifests[*mi];
645 (&m.provider, &m.tools[*ti])
646 })
647 }
648
649 pub fn list_tools(&self) -> Vec<(&Provider, &Tool)> {
651 self.manifests
652 .iter()
653 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
654 .collect()
655 }
656
657 pub fn list_providers(&self) -> Vec<&Provider> {
659 self.manifests.iter().map(|m| &m.provider).collect()
660 }
661
662 pub fn list_public_tools(&self) -> Vec<(&Provider, &Tool)> {
664 self.manifests
665 .iter()
666 .filter(|m| !m.provider.internal)
667 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
668 .collect()
669 }
670
671 pub fn tool_count(&self) -> usize {
673 self.tool_index.len()
674 }
675
676 pub fn provider_count(&self) -> usize {
678 self.manifests.len()
679 }
680
681 pub fn list_mcp_providers(&self) -> Vec<&Provider> {
683 self.manifests
684 .iter()
685 .filter(|m| m.provider.handler == "mcp")
686 .map(|m| &m.provider)
687 .collect()
688 }
689
690 pub fn find_mcp_provider_for_tool(&self, tool_name: &str) -> Option<&Provider> {
692 let prefix = tool_name.split(TOOL_SEP).next()?;
693 self.manifests
694 .iter()
695 .find(|m| m.provider.handler == "mcp" && m.provider.name == prefix)
696 .map(|m| &m.provider)
697 }
698
699 pub fn list_openapi_providers(&self) -> Vec<&Provider> {
701 self.manifests
702 .iter()
703 .filter(|m| m.provider.handler == "openapi")
704 .map(|m| &m.provider)
705 .collect()
706 }
707
708 pub fn has_provider(&self, name: &str) -> bool {
710 self.manifests.iter().any(|m| m.provider.name == name)
711 }
712
713 pub fn tools_by_provider(&self, provider_name: &str) -> Vec<(&Provider, &Tool)> {
715 self.manifests
716 .iter()
717 .filter(|m| m.provider.name == provider_name)
718 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
719 .collect()
720 }
721
722 pub fn list_cli_providers(&self) -> Vec<&Provider> {
724 self.manifests
725 .iter()
726 .filter(|m| m.provider.handler == "cli")
727 .map(|m| &m.provider)
728 .collect()
729 }
730
731 pub fn register_mcp_tools(&mut self, provider_name: &str, mcp_tools: Vec<McpToolDef>) {
734 let mi = match self
736 .manifests
737 .iter()
738 .position(|m| m.provider.name == provider_name)
739 {
740 Some(idx) => idx,
741 None => return,
742 };
743
744 for mcp_tool in mcp_tools {
745 let prefixed_name = format!("{}{}{}", provider_name, TOOL_SEP_STR, mcp_tool.name);
746
747 let tool = Tool {
748 name: prefixed_name.clone(),
749 description: mcp_tool.description.unwrap_or_default(),
750 endpoint: String::new(),
751 method: HttpMethod::Post,
752 scope: Some(format!("tool:{prefixed_name}")),
753 input_schema: mcp_tool.input_schema,
754 response: None,
755 tags: Vec::new(),
756 hint: None,
757 examples: Vec::new(),
758 };
759
760 let ti = self.manifests[mi].tools.len();
761 self.manifests[mi].tools.push(tool);
762 self.tool_index.insert(prefixed_name, (mi, ti));
763 }
764 }
765}
766
767impl Provider {
768 pub fn is_mcp(&self) -> bool {
770 self.handler == "mcp"
771 }
772
773 pub fn is_openapi(&self) -> bool {
775 self.handler == "openapi"
776 }
777
778 pub fn is_cli(&self) -> bool {
780 self.handler == "cli"
781 }
782
783 pub fn mcp_transport_type(&self) -> &str {
785 self.mcp_transport.as_deref().unwrap_or("stdio")
786 }
787}