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")]
24#[derive(Default)]
25pub enum AuthType {
26 Bearer,
27 Header,
28 Query,
29 Basic,
30 #[default]
31 None,
32 Oauth2,
33}
34
35#[derive(Debug, Clone, Deserialize)]
36pub struct Provider {
37 pub name: String,
38 pub description: String,
39 #[serde(default)]
41 pub base_url: String,
42 #[serde(default)]
43 pub auth_type: AuthType,
44 #[serde(default)]
45 pub auth_key_name: Option<String>,
46 #[serde(default)]
49 pub auth_header_name: Option<String>,
50 #[serde(default)]
52 pub auth_query_name: Option<String>,
53 #[serde(default)]
56 pub auth_value_prefix: Option<String>,
57 #[serde(default)]
60 pub extra_headers: HashMap<String, String>,
61 #[serde(default)]
63 pub oauth2_token_url: Option<String>,
64 #[serde(default)]
66 pub auth_secret_name: Option<String>,
67 #[serde(default)]
70 pub oauth2_basic_auth: bool,
71 #[serde(default)]
72 pub internal: bool,
73 #[serde(default = "default_handler")]
74 pub handler: String,
75
76 #[serde(default)]
79 pub mcp_transport: Option<String>,
80 #[serde(default)]
82 pub mcp_command: Option<String>,
83 #[serde(default)]
85 pub mcp_args: Vec<String>,
86 #[serde(default)]
88 pub mcp_url: Option<String>,
89 #[serde(default)]
91 pub mcp_env: HashMap<String, String>,
92
93 #[serde(default)]
96 pub cli_command: Option<String>,
97 #[serde(default)]
99 pub cli_default_args: Vec<String>,
100 #[serde(default)]
102 pub cli_env: HashMap<String, String>,
103 #[serde(default)]
105 pub cli_timeout_secs: Option<u64>,
106
107 #[serde(default)]
110 pub openapi_spec: Option<String>,
111 #[serde(default)]
113 pub openapi_include_tags: Vec<String>,
114 #[serde(default)]
116 pub openapi_exclude_tags: Vec<String>,
117 #[serde(default)]
119 pub openapi_include_operations: Vec<String>,
120 #[serde(default)]
122 pub openapi_exclude_operations: Vec<String>,
123 #[serde(default)]
125 pub openapi_max_operations: Option<usize>,
126 #[serde(default)]
128 pub openapi_overrides: HashMap<String, OpenApiToolOverride>,
129
130 #[serde(default)]
134 pub auth_generator: Option<AuthGenerator>,
135
136 #[serde(default)]
139 pub category: Option<String>,
140
141 #[serde(default)]
144 pub skills: Vec<String>,
145}
146
147fn default_handler() -> String {
148 "http".to_string()
149}
150
151#[derive(Debug, Clone, Deserialize, Default)]
153pub struct OpenApiToolOverride {
154 pub hint: Option<String>,
155 #[serde(default)]
156 pub tags: Vec<String>,
157 #[serde(default)]
158 pub examples: Vec<String>,
159 pub description: Option<String>,
160 pub scope: Option<String>,
161 pub response_extract: Option<String>,
162 pub response_format: Option<String>,
163}
164
165#[derive(Debug, Clone, Deserialize)]
178pub struct AuthGenerator {
179 #[serde(rename = "type")]
180 pub gen_type: AuthGenType,
181 pub command: Option<String>,
183 #[serde(default)]
185 pub args: Vec<String>,
186 pub interpreter: Option<String>,
188 pub script: Option<String>,
190 #[serde(default)]
192 pub cache_ttl_secs: u64,
193 #[serde(default)]
195 pub output_format: AuthOutputFormat,
196 #[serde(default)]
198 pub env: HashMap<String, String>,
199 #[serde(default)]
201 pub inject: HashMap<String, InjectTarget>,
202 #[serde(default = "default_gen_timeout")]
204 pub timeout_secs: u64,
205}
206
207fn default_gen_timeout() -> u64 {
208 30
209}
210
211#[derive(Debug, Clone, Deserialize)]
212#[serde(rename_all = "snake_case")]
213pub enum AuthGenType {
214 Command,
215 Script,
216}
217
218#[derive(Debug, Clone, Deserialize, Default)]
219#[serde(rename_all = "snake_case")]
220pub enum AuthOutputFormat {
221 #[default]
222 Text,
223 Json,
224}
225
226#[derive(Debug, Clone, Deserialize)]
228pub struct InjectTarget {
229 #[serde(rename = "type")]
231 pub inject_type: String,
232 pub name: String,
234}
235
236#[derive(Debug, Clone, Deserialize)]
237#[serde(rename_all = "UPPERCASE")]
238#[derive(Default)]
239pub enum HttpMethod {
240 #[serde(alias = "get", alias = "Get")]
241 #[default]
242 Get,
243 #[serde(alias = "post", alias = "Post")]
244 Post,
245 #[serde(alias = "put", alias = "Put")]
246 Put,
247 #[serde(alias = "delete", alias = "Delete")]
248 Delete,
249}
250
251impl std::fmt::Display for HttpMethod {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 match self {
254 HttpMethod::Get => write!(f, "GET"),
255 HttpMethod::Post => write!(f, "POST"),
256 HttpMethod::Put => write!(f, "PUT"),
257 HttpMethod::Delete => write!(f, "DELETE"),
258 }
259 }
260}
261
262#[derive(Debug, Clone, Deserialize, Default)]
263#[serde(rename_all = "snake_case")]
264pub enum ResponseFormat {
265 MarkdownTable,
266 Json,
267 #[default]
268 Text,
269 Raw,
270}
271
272#[derive(Debug, Clone, Deserialize, Default)]
273pub struct ResponseConfig {
274 #[serde(default)]
276 pub extract: Option<String>,
277 #[serde(default)]
279 pub format: ResponseFormat,
280}
281
282#[derive(Debug, Clone, Deserialize)]
283pub struct Tool {
284 pub name: String,
285 pub description: String,
286 #[serde(default)]
287 pub endpoint: String,
288 #[serde(default)]
289 pub method: HttpMethod,
290 #[serde(default)]
292 pub scope: Option<String>,
293 #[serde(default)]
295 pub input_schema: Option<serde_json::Value>,
296 #[serde(default)]
298 pub response: Option<ResponseConfig>,
299
300 #[serde(default)]
303 pub tags: Vec<String>,
304 #[serde(default)]
306 pub hint: Option<String>,
307 #[serde(default)]
309 pub examples: Vec<String>,
310}
311
312#[derive(Debug, Clone, Deserialize)]
315pub struct Manifest {
316 pub provider: Provider,
317 #[serde(default, rename = "tools")]
318 pub tools: Vec<Tool>,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct CachedProvider {
326 pub name: String,
327 pub provider_type: String,
329 #[serde(default)]
330 pub base_url: String,
331 #[serde(default)]
332 pub auth_type: String,
333 #[serde(default)]
334 pub auth_key_name: Option<String>,
335 #[serde(default)]
336 pub auth_header_name: Option<String>,
337 #[serde(default)]
338 pub auth_query_name: Option<String>,
339 #[serde(default)]
341 pub spec_content: Option<String>,
342 #[serde(default)]
344 pub mcp_transport: Option<String>,
345 #[serde(default)]
346 pub mcp_url: Option<String>,
347 #[serde(default)]
348 pub mcp_command: Option<String>,
349 #[serde(default)]
350 pub mcp_args: Vec<String>,
351 #[serde(default)]
352 pub mcp_env: HashMap<String, String>,
353 #[serde(default)]
355 pub cli_command: Option<String>,
356 #[serde(default)]
357 pub cli_default_args: Vec<String>,
358 #[serde(default)]
359 pub cli_env: HashMap<String, String>,
360 #[serde(default)]
361 pub cli_timeout_secs: Option<u64>,
362 #[serde(default)]
364 pub auth: Option<String>,
365 #[serde(default)]
367 pub skills: Vec<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: self.skills.clone(),
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 tracing::warn!(
516 provider = %manifest.provider.name,
517 error = %e,
518 "failed to load OpenAPI spec for provider"
519 );
520 }
522 }
523 }
524 }
525
526 if manifest.provider.is_cli() && manifest.tools.is_empty() {
528 let tool_name = manifest.provider.name.clone();
529 manifest.tools.push(Tool {
530 name: tool_name.clone(),
531 description: manifest.provider.description.clone(),
532 endpoint: String::new(),
533 method: HttpMethod::Get,
534 scope: Some(format!("tool:{tool_name}")),
535 input_schema: None,
536 response: None,
537 tags: Vec::new(),
538 hint: None,
539 examples: Vec::new(),
540 });
541 }
542
543 let provider_name = &manifest.provider.name;
546 for tool in &mut manifest.tools {
547 if tool.scope.is_none() && !manifest.provider.internal {
548 tool.scope = Some(format!("tool:{}", tool.name));
549 tracing::trace!(
550 tool = %tool.name,
551 provider = %provider_name,
552 scope = ?tool.scope,
553 "auto-assigned scope to tool"
554 );
555 }
556 }
557
558 let mi = manifests.len();
559 for (ti, tool) in manifest.tools.iter().enumerate() {
560 tool_index.insert(tool.name.clone(), (mi, ti));
561 }
562 manifests.push(manifest);
563 }
564
565 if let Some(parent) = dir.parent() {
568 let cache_dir = parent.join("cache").join("providers");
569 if cache_dir.is_dir() {
570 let cache_pattern = cache_dir.join("*.json");
571 if let Ok(cache_entries) = glob::glob(cache_pattern.to_str().unwrap_or("")) {
572 for entry in cache_entries {
573 let path = match entry {
574 Ok(p) => p,
575 Err(_) => continue,
576 };
577 let content = match std::fs::read_to_string(&path) {
578 Ok(c) => c,
579 Err(_) => continue,
580 };
581 let cached: CachedProvider = match serde_json::from_str(&content) {
582 Ok(c) => c,
583 Err(_) => continue,
584 };
585
586 if cached.is_expired() {
588 let _ = std::fs::remove_file(&path);
589 continue;
590 }
591
592 if manifests.iter().any(|m| m.provider.name == cached.name) {
594 continue;
595 }
596
597 let provider = cached.to_provider();
598
599 let mut cached_tools = Vec::new();
600 if cached.provider_type == "openapi" {
601 if let Some(spec_content) = &cached.spec_content {
602 if let Ok(spec) = crate::core::openapi::parse_spec(spec_content) {
603 let filters = crate::core::openapi::OpenApiFilters {
604 include_tags: vec![],
605 exclude_tags: vec![],
606 include_operations: vec![],
607 exclude_operations: vec![],
608 max_operations: None,
609 };
610 let defs = crate::core::openapi::extract_tools(&spec, &filters);
611 cached_tools = defs
612 .into_iter()
613 .map(|def| {
614 crate::core::openapi::to_ati_tool(
615 def,
616 &cached.name,
617 &HashMap::new(),
618 )
619 })
620 .collect();
621 }
622 }
623 }
624 let mi = manifests.len();
627 for (ti, tool) in cached_tools.iter().enumerate() {
628 tool_index.insert(tool.name.clone(), (mi, ti));
629 }
630 manifests.push(Manifest {
631 provider,
632 tools: cached_tools,
633 });
634 }
635 }
636 }
637 }
638
639 Ok(ManifestRegistry {
640 manifests,
641 tool_index,
642 })
643 }
644
645 pub fn empty() -> Self {
647 ManifestRegistry {
648 manifests: Vec::new(),
649 tool_index: HashMap::new(),
650 }
651 }
652
653 pub fn get_tool(&self, name: &str) -> Option<(&Provider, &Tool)> {
655 self.tool_index.get(name).map(|(mi, ti)| {
656 let m = &self.manifests[*mi];
657 (&m.provider, &m.tools[*ti])
658 })
659 }
660
661 pub fn list_tools(&self) -> Vec<(&Provider, &Tool)> {
663 self.manifests
664 .iter()
665 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
666 .collect()
667 }
668
669 pub fn list_providers(&self) -> Vec<&Provider> {
671 self.manifests.iter().map(|m| &m.provider).collect()
672 }
673
674 pub fn list_public_tools(&self) -> Vec<(&Provider, &Tool)> {
676 self.manifests
677 .iter()
678 .filter(|m| !m.provider.internal)
679 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
680 .collect()
681 }
682
683 pub fn tool_count(&self) -> usize {
685 self.tool_index.len()
686 }
687
688 pub fn provider_count(&self) -> usize {
690 self.manifests.len()
691 }
692
693 pub fn list_mcp_providers(&self) -> Vec<&Provider> {
695 self.manifests
696 .iter()
697 .filter(|m| m.provider.handler == "mcp")
698 .map(|m| &m.provider)
699 .collect()
700 }
701
702 pub fn find_mcp_provider_for_tool(&self, tool_name: &str) -> Option<&Provider> {
704 let prefix = tool_name.split(TOOL_SEP).next()?;
705 self.manifests
706 .iter()
707 .find(|m| m.provider.handler == "mcp" && m.provider.name == prefix)
708 .map(|m| &m.provider)
709 }
710
711 pub fn list_openapi_providers(&self) -> Vec<&Provider> {
713 self.manifests
714 .iter()
715 .filter(|m| m.provider.handler == "openapi")
716 .map(|m| &m.provider)
717 .collect()
718 }
719
720 pub fn has_provider(&self, name: &str) -> bool {
722 self.manifests.iter().any(|m| m.provider.name == name)
723 }
724
725 pub fn tools_by_provider(&self, provider_name: &str) -> Vec<(&Provider, &Tool)> {
727 self.manifests
728 .iter()
729 .filter(|m| m.provider.name == provider_name)
730 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
731 .collect()
732 }
733
734 pub fn list_cli_providers(&self) -> Vec<&Provider> {
736 self.manifests
737 .iter()
738 .filter(|m| m.provider.handler == "cli")
739 .map(|m| &m.provider)
740 .collect()
741 }
742
743 pub fn register_mcp_tools(&mut self, provider_name: &str, mcp_tools: Vec<McpToolDef>) {
746 let mi = match self
748 .manifests
749 .iter()
750 .position(|m| m.provider.name == provider_name)
751 {
752 Some(idx) => idx,
753 None => return,
754 };
755
756 for mcp_tool in mcp_tools {
757 let prefixed_name = format!("{}{}{}", provider_name, TOOL_SEP_STR, mcp_tool.name);
758
759 let tool = Tool {
760 name: prefixed_name.clone(),
761 description: mcp_tool.description.unwrap_or_default(),
762 endpoint: String::new(),
763 method: HttpMethod::Post,
764 scope: Some(format!("tool:{prefixed_name}")),
765 input_schema: mcp_tool.input_schema,
766 response: None,
767 tags: Vec::new(),
768 hint: None,
769 examples: Vec::new(),
770 };
771
772 let ti = self.manifests[mi].tools.len();
773 self.manifests[mi].tools.push(tool);
774 self.tool_index.insert(prefixed_name, (mi, ti));
775 }
776 }
777}
778
779impl Provider {
780 pub fn is_mcp(&self) -> bool {
782 self.handler == "mcp"
783 }
784
785 pub fn is_openapi(&self) -> bool {
787 self.handler == "openapi"
788 }
789
790 pub fn is_cli(&self) -> bool {
792 self.handler == "cli"
793 }
794
795 pub fn mcp_transport_type(&self) -> &str {
797 self.mcp_transport.as_deref().unwrap_or("stdio")
798 }
799}