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 Url,
38}
39
40#[derive(Debug, Clone, Deserialize)]
41pub struct Provider {
42 pub name: String,
43 pub description: String,
44 #[serde(default)]
46 pub base_url: String,
47 #[serde(default)]
48 pub auth_type: AuthType,
49 #[serde(default)]
50 pub auth_key_name: Option<String>,
51 #[serde(default)]
54 pub auth_header_name: Option<String>,
55 #[serde(default)]
57 pub auth_query_name: Option<String>,
58 #[serde(default)]
61 pub auth_value_prefix: Option<String>,
62 #[serde(default)]
65 pub extra_headers: HashMap<String, String>,
66 #[serde(default)]
68 pub oauth2_token_url: Option<String>,
69 #[serde(default)]
71 pub auth_secret_name: Option<String>,
72 #[serde(default)]
75 pub oauth2_basic_auth: bool,
76 #[serde(default)]
77 pub internal: bool,
78 #[serde(default = "default_handler")]
79 pub handler: String,
80
81 #[serde(default)]
84 pub mcp_transport: Option<String>,
85 #[serde(default)]
87 pub mcp_command: Option<String>,
88 #[serde(default)]
90 pub mcp_args: Vec<String>,
91 #[serde(default)]
93 pub mcp_url: Option<String>,
94 #[serde(default)]
96 pub mcp_env: HashMap<String, String>,
97
98 #[serde(default)]
101 pub cli_command: Option<String>,
102 #[serde(default)]
104 pub cli_default_args: Vec<String>,
105 #[serde(default)]
107 pub cli_env: HashMap<String, String>,
108 #[serde(default)]
110 pub cli_timeout_secs: Option<u64>,
111
112 #[serde(default)]
115 pub openapi_spec: Option<String>,
116 #[serde(default)]
118 pub openapi_include_tags: Vec<String>,
119 #[serde(default)]
121 pub openapi_exclude_tags: Vec<String>,
122 #[serde(default)]
124 pub openapi_include_operations: Vec<String>,
125 #[serde(default)]
127 pub openapi_exclude_operations: Vec<String>,
128 #[serde(default)]
130 pub openapi_max_operations: Option<usize>,
131 #[serde(default)]
133 pub openapi_overrides: HashMap<String, OpenApiToolOverride>,
134
135 #[serde(default)]
139 pub auth_generator: Option<AuthGenerator>,
140
141 #[serde(default)]
144 pub category: Option<String>,
145
146 #[serde(default)]
149 pub skills: Vec<String>,
150}
151
152fn default_handler() -> String {
153 "http".to_string()
154}
155
156#[derive(Debug, Clone, Deserialize, Default)]
158pub struct OpenApiToolOverride {
159 pub hint: Option<String>,
160 #[serde(default)]
161 pub tags: Vec<String>,
162 #[serde(default)]
163 pub examples: Vec<String>,
164 pub description: Option<String>,
165 pub scope: Option<String>,
166 pub response_extract: Option<String>,
167 pub response_format: Option<String>,
168}
169
170#[derive(Debug, Clone, Deserialize)]
183pub struct AuthGenerator {
184 #[serde(rename = "type")]
185 pub gen_type: AuthGenType,
186 pub command: Option<String>,
188 #[serde(default)]
190 pub args: Vec<String>,
191 pub interpreter: Option<String>,
193 pub script: Option<String>,
195 #[serde(default)]
197 pub cache_ttl_secs: u64,
198 #[serde(default)]
200 pub output_format: AuthOutputFormat,
201 #[serde(default)]
203 pub env: HashMap<String, String>,
204 #[serde(default)]
206 pub inject: HashMap<String, InjectTarget>,
207 #[serde(default = "default_gen_timeout")]
209 pub timeout_secs: u64,
210}
211
212fn default_gen_timeout() -> u64 {
213 30
214}
215
216#[derive(Debug, Clone, Deserialize)]
217#[serde(rename_all = "snake_case")]
218pub enum AuthGenType {
219 Command,
220 Script,
221}
222
223#[derive(Debug, Clone, Deserialize, Default)]
224#[serde(rename_all = "snake_case")]
225pub enum AuthOutputFormat {
226 #[default]
227 Text,
228 Json,
229}
230
231#[derive(Debug, Clone, Deserialize)]
233pub struct InjectTarget {
234 #[serde(rename = "type")]
236 pub inject_type: String,
237 pub name: String,
239}
240
241#[derive(Debug, Clone, Deserialize)]
242#[serde(rename_all = "UPPERCASE")]
243#[derive(Default)]
244pub enum HttpMethod {
245 #[serde(alias = "get", alias = "Get")]
246 #[default]
247 Get,
248 #[serde(alias = "post", alias = "Post")]
249 Post,
250 #[serde(alias = "put", alias = "Put")]
251 Put,
252 #[serde(alias = "delete", alias = "Delete")]
253 Delete,
254}
255
256impl std::fmt::Display for HttpMethod {
257 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258 match self {
259 HttpMethod::Get => write!(f, "GET"),
260 HttpMethod::Post => write!(f, "POST"),
261 HttpMethod::Put => write!(f, "PUT"),
262 HttpMethod::Delete => write!(f, "DELETE"),
263 }
264 }
265}
266
267#[derive(Debug, Clone, Deserialize, Default)]
268#[serde(rename_all = "snake_case")]
269pub enum ResponseFormat {
270 MarkdownTable,
271 Json,
272 #[default]
273 Text,
274 Raw,
275}
276
277#[derive(Debug, Clone, Deserialize, Default)]
278pub struct ResponseConfig {
279 #[serde(default)]
281 pub extract: Option<String>,
282 #[serde(default)]
284 pub format: ResponseFormat,
285}
286
287#[derive(Debug, Clone, Deserialize)]
288pub struct Tool {
289 pub name: String,
290 pub description: String,
291 #[serde(default)]
292 pub endpoint: String,
293 #[serde(default)]
294 pub method: HttpMethod,
295 #[serde(default)]
297 pub scope: Option<String>,
298 #[serde(default)]
300 pub input_schema: Option<serde_json::Value>,
301 #[serde(default)]
303 pub response: Option<ResponseConfig>,
304
305 #[serde(default)]
308 pub tags: Vec<String>,
309 #[serde(default)]
311 pub hint: Option<String>,
312 #[serde(default)]
314 pub examples: Vec<String>,
315}
316
317#[derive(Debug, Clone, Deserialize)]
320pub struct Manifest {
321 pub provider: Provider,
322 #[serde(default, rename = "tools")]
323 pub tools: Vec<Tool>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct CachedProvider {
331 pub name: String,
332 pub provider_type: String,
334 #[serde(default)]
335 pub base_url: String,
336 #[serde(default)]
337 pub auth_type: String,
338 #[serde(default)]
339 pub auth_key_name: Option<String>,
340 #[serde(default)]
341 pub auth_header_name: Option<String>,
342 #[serde(default)]
343 pub auth_query_name: Option<String>,
344 #[serde(default)]
346 pub spec_content: Option<String>,
347 #[serde(default)]
349 pub mcp_transport: Option<String>,
350 #[serde(default)]
351 pub mcp_url: Option<String>,
352 #[serde(default)]
353 pub mcp_command: Option<String>,
354 #[serde(default)]
355 pub mcp_args: Vec<String>,
356 #[serde(default)]
357 pub mcp_env: HashMap<String, String>,
358 #[serde(default)]
360 pub cli_command: Option<String>,
361 #[serde(default)]
362 pub cli_default_args: Vec<String>,
363 #[serde(default)]
364 pub cli_env: HashMap<String, String>,
365 #[serde(default)]
366 pub cli_timeout_secs: Option<u64>,
367 #[serde(default)]
369 pub auth: Option<String>,
370 #[serde(default)]
372 pub skills: Vec<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: self.skills.clone(),
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 tracing::warn!(
521 provider = %manifest.provider.name,
522 error = %e,
523 "failed to load OpenAPI spec for provider"
524 );
525 }
527 }
528 }
529 }
530
531 if manifest.provider.is_cli() && manifest.tools.is_empty() {
533 let tool_name = manifest.provider.name.clone();
534 manifest.tools.push(Tool {
535 name: tool_name.clone(),
536 description: manifest.provider.description.clone(),
537 endpoint: String::new(),
538 method: HttpMethod::Get,
539 scope: Some(format!("tool:{tool_name}")),
540 input_schema: None,
541 response: None,
542 tags: Vec::new(),
543 hint: None,
544 examples: Vec::new(),
545 });
546 }
547
548 let provider_name = &manifest.provider.name;
551 for tool in &mut manifest.tools {
552 if tool.scope.is_none() && !manifest.provider.internal {
553 tool.scope = Some(format!("tool:{}", tool.name));
554 tracing::trace!(
555 tool = %tool.name,
556 provider = %provider_name,
557 scope = ?tool.scope,
558 "auto-assigned scope to tool"
559 );
560 }
561 }
562
563 let mi = manifests.len();
564 for (ti, tool) in manifest.tools.iter().enumerate() {
565 tool_index.insert(tool.name.clone(), (mi, ti));
566 }
567 manifests.push(manifest);
568 }
569
570 if let Some(parent) = dir.parent() {
573 let cache_dir = parent.join("cache").join("providers");
574 if cache_dir.is_dir() {
575 let cache_pattern = cache_dir.join("*.json");
576 if let Ok(cache_entries) = glob::glob(cache_pattern.to_str().unwrap_or("")) {
577 for entry in cache_entries {
578 let path = match entry {
579 Ok(p) => p,
580 Err(_) => continue,
581 };
582 let content = match std::fs::read_to_string(&path) {
583 Ok(c) => c,
584 Err(_) => continue,
585 };
586 let cached: CachedProvider = match serde_json::from_str(&content) {
587 Ok(c) => c,
588 Err(_) => continue,
589 };
590
591 if cached.is_expired() {
593 let _ = std::fs::remove_file(&path);
594 continue;
595 }
596
597 if manifests.iter().any(|m| m.provider.name == cached.name) {
599 continue;
600 }
601
602 let provider = cached.to_provider();
603
604 let mut cached_tools = Vec::new();
605 if cached.provider_type == "openapi" {
606 if let Some(spec_content) = &cached.spec_content {
607 if let Ok(spec) = crate::core::openapi::parse_spec(spec_content) {
608 let filters = crate::core::openapi::OpenApiFilters {
609 include_tags: vec![],
610 exclude_tags: vec![],
611 include_operations: vec![],
612 exclude_operations: vec![],
613 max_operations: None,
614 };
615 let defs = crate::core::openapi::extract_tools(&spec, &filters);
616 cached_tools = defs
617 .into_iter()
618 .map(|def| {
619 crate::core::openapi::to_ati_tool(
620 def,
621 &cached.name,
622 &HashMap::new(),
623 )
624 })
625 .collect();
626 }
627 }
628 }
629 let mi = manifests.len();
632 for (ti, tool) in cached_tools.iter().enumerate() {
633 tool_index.insert(tool.name.clone(), (mi, ti));
634 }
635 manifests.push(Manifest {
636 provider,
637 tools: cached_tools,
638 });
639 }
640 }
641 }
642 }
643
644 Ok(ManifestRegistry {
645 manifests,
646 tool_index,
647 })
648 }
649
650 pub fn empty() -> Self {
652 ManifestRegistry {
653 manifests: Vec::new(),
654 tool_index: HashMap::new(),
655 }
656 }
657
658 pub fn get_tool(&self, name: &str) -> Option<(&Provider, &Tool)> {
660 self.tool_index.get(name).map(|(mi, ti)| {
661 let m = &self.manifests[*mi];
662 (&m.provider, &m.tools[*ti])
663 })
664 }
665
666 pub fn list_tools(&self) -> Vec<(&Provider, &Tool)> {
668 self.manifests
669 .iter()
670 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
671 .collect()
672 }
673
674 pub fn list_providers(&self) -> Vec<&Provider> {
676 self.manifests.iter().map(|m| &m.provider).collect()
677 }
678
679 pub fn list_public_tools(&self) -> Vec<(&Provider, &Tool)> {
681 self.manifests
682 .iter()
683 .filter(|m| !m.provider.internal)
684 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
685 .collect()
686 }
687
688 pub fn tool_count(&self) -> usize {
690 self.tool_index.len()
691 }
692
693 pub fn provider_count(&self) -> usize {
695 self.manifests.len()
696 }
697
698 pub fn list_mcp_providers(&self) -> Vec<&Provider> {
700 self.manifests
701 .iter()
702 .filter(|m| m.provider.handler == "mcp")
703 .map(|m| &m.provider)
704 .collect()
705 }
706
707 pub fn find_mcp_provider_for_tool(&self, tool_name: &str) -> Option<&Provider> {
709 let prefix = tool_name.split(TOOL_SEP).next()?;
710 self.manifests
711 .iter()
712 .find(|m| m.provider.handler == "mcp" && m.provider.name == prefix)
713 .map(|m| &m.provider)
714 }
715
716 pub fn list_openapi_providers(&self) -> Vec<&Provider> {
718 self.manifests
719 .iter()
720 .filter(|m| m.provider.handler == "openapi")
721 .map(|m| &m.provider)
722 .collect()
723 }
724
725 pub fn has_provider(&self, name: &str) -> bool {
727 self.manifests.iter().any(|m| m.provider.name == name)
728 }
729
730 pub fn tools_by_provider(&self, provider_name: &str) -> Vec<(&Provider, &Tool)> {
732 self.manifests
733 .iter()
734 .filter(|m| m.provider.name == provider_name)
735 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
736 .collect()
737 }
738
739 pub fn list_cli_providers(&self) -> Vec<&Provider> {
741 self.manifests
742 .iter()
743 .filter(|m| m.provider.handler == "cli")
744 .map(|m| &m.provider)
745 .collect()
746 }
747
748 pub fn register_mcp_tools(&mut self, provider_name: &str, mcp_tools: Vec<McpToolDef>) {
751 let mi = match self
753 .manifests
754 .iter()
755 .position(|m| m.provider.name == provider_name)
756 {
757 Some(idx) => idx,
758 None => return,
759 };
760
761 for mcp_tool in mcp_tools {
762 let prefixed_name = format!("{}{}{}", provider_name, TOOL_SEP_STR, mcp_tool.name);
763
764 let tool = Tool {
765 name: prefixed_name.clone(),
766 description: mcp_tool.description.unwrap_or_default(),
767 endpoint: String::new(),
768 method: HttpMethod::Post,
769 scope: Some(format!("tool:{prefixed_name}")),
770 input_schema: mcp_tool.input_schema,
771 response: None,
772 tags: Vec::new(),
773 hint: None,
774 examples: Vec::new(),
775 };
776
777 let ti = self.manifests[mi].tools.len();
778 self.manifests[mi].tools.push(tool);
779 self.tool_index.insert(prefixed_name, (mi, ti));
780 }
781 }
782}
783
784impl Provider {
785 pub fn is_mcp(&self) -> bool {
787 self.handler == "mcp"
788 }
789
790 pub fn is_openapi(&self) -> bool {
792 self.handler == "openapi"
793 }
794
795 pub fn is_cli(&self) -> bool {
797 self.handler == "cli"
798 }
799
800 pub fn mcp_transport_type(&self) -> &str {
802 self.mcp_transport.as_deref().unwrap_or("stdio")
803 }
804}