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