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 #[error("Manifest {0} is invalid: {1}")]
21 Invalid(String, String),
22}
23
24#[derive(Debug, Clone, Deserialize)]
25#[serde(rename_all = "snake_case")]
26#[derive(Default)]
27pub enum AuthType {
28 Bearer,
29 Header,
30 Query,
31 Basic,
32 #[default]
33 None,
34 Oauth2,
35 Url,
40}
41
42#[derive(Debug, Clone, Deserialize)]
43pub struct Provider {
44 pub name: String,
45 pub description: String,
46 #[serde(default)]
48 pub base_url: String,
49 #[serde(default)]
50 pub auth_type: AuthType,
51 #[serde(default)]
52 pub auth_key_name: Option<String>,
53 #[serde(default)]
56 pub auth_header_name: Option<String>,
57 #[serde(default)]
59 pub auth_query_name: Option<String>,
60 #[serde(default)]
63 pub auth_value_prefix: Option<String>,
64 #[serde(default)]
67 pub extra_headers: HashMap<String, String>,
68 #[serde(default)]
70 pub oauth2_token_url: Option<String>,
71 #[serde(default)]
73 pub auth_secret_name: Option<String>,
74 #[serde(default)]
90 pub auth_session_token_env: Option<String>,
91 #[serde(default)]
94 pub oauth2_basic_auth: bool,
95 #[serde(default)]
96 pub internal: bool,
97 #[serde(default = "default_handler")]
98 pub handler: String,
99
100 #[serde(default)]
103 pub mcp_transport: Option<String>,
104 #[serde(default)]
106 pub mcp_command: Option<String>,
107 #[serde(default)]
109 pub mcp_args: Vec<String>,
110 #[serde(default)]
112 pub mcp_url: Option<String>,
113 #[serde(default)]
129 pub mcp_url_env: Option<String>,
130 #[serde(default)]
132 pub mcp_env: HashMap<String, String>,
133
134 #[serde(default)]
137 pub cli_command: Option<String>,
138 #[serde(default)]
140 pub cli_default_args: Vec<String>,
141 #[serde(default)]
143 pub cli_env: HashMap<String, String>,
144 #[serde(default)]
146 pub cli_timeout_secs: Option<u64>,
147 #[serde(default)]
153 pub cli_output_args: Vec<String>,
154 #[serde(default)]
159 pub cli_output_positional: HashMap<String, usize>,
160
161 #[serde(default)]
167 pub upload_destinations: HashMap<String, crate::core::file_manager::UploadDestination>,
168 #[serde(default)]
171 pub upload_default_destination: Option<String>,
172
173 #[serde(default)]
176 pub openapi_spec: Option<String>,
177 #[serde(default)]
179 pub openapi_include_tags: Vec<String>,
180 #[serde(default)]
182 pub openapi_exclude_tags: Vec<String>,
183 #[serde(default)]
185 pub openapi_include_operations: Vec<String>,
186 #[serde(default)]
188 pub openapi_exclude_operations: Vec<String>,
189 #[serde(default)]
191 pub openapi_max_operations: Option<usize>,
192 #[serde(default)]
194 pub openapi_overrides: HashMap<String, OpenApiToolOverride>,
195
196 #[serde(default)]
200 pub auth_generator: Option<AuthGenerator>,
201
202 #[serde(default)]
205 pub category: Option<String>,
206
207 #[serde(default)]
210 pub skills: Vec<String>,
211}
212
213fn default_handler() -> String {
214 "http".to_string()
215}
216
217#[derive(Debug, Clone, Deserialize, Default)]
219pub struct OpenApiToolOverride {
220 pub hint: Option<String>,
221 #[serde(default)]
222 pub tags: Vec<String>,
223 #[serde(default)]
224 pub examples: Vec<String>,
225 pub description: Option<String>,
226 pub scope: Option<String>,
227 pub response_extract: Option<String>,
228 pub response_format: Option<String>,
229}
230
231#[derive(Debug, Clone, Deserialize)]
244pub struct AuthGenerator {
245 #[serde(rename = "type")]
246 pub gen_type: AuthGenType,
247 pub command: Option<String>,
249 #[serde(default)]
251 pub args: Vec<String>,
252 pub interpreter: Option<String>,
254 pub script: Option<String>,
256 #[serde(default)]
258 pub cache_ttl_secs: u64,
259 #[serde(default)]
261 pub output_format: AuthOutputFormat,
262 #[serde(default)]
264 pub env: HashMap<String, String>,
265 #[serde(default)]
267 pub inject: HashMap<String, InjectTarget>,
268 #[serde(default = "default_gen_timeout")]
270 pub timeout_secs: u64,
271}
272
273fn default_gen_timeout() -> u64 {
274 30
275}
276
277#[derive(Debug, Clone, Deserialize)]
278#[serde(rename_all = "snake_case")]
279pub enum AuthGenType {
280 Command,
281 Script,
282}
283
284#[derive(Debug, Clone, Deserialize, Default)]
285#[serde(rename_all = "snake_case")]
286pub enum AuthOutputFormat {
287 #[default]
288 Text,
289 Json,
290}
291
292#[derive(Debug, Clone, Deserialize)]
294pub struct InjectTarget {
295 #[serde(rename = "type")]
297 pub inject_type: String,
298 pub name: String,
300}
301
302#[derive(Debug, Clone, Deserialize)]
303#[serde(rename_all = "UPPERCASE")]
304#[derive(Default)]
305pub enum HttpMethod {
306 #[serde(alias = "get", alias = "Get")]
307 #[default]
308 Get,
309 #[serde(alias = "post", alias = "Post")]
310 Post,
311 #[serde(alias = "put", alias = "Put")]
312 Put,
313 #[serde(alias = "delete", alias = "Delete")]
314 Delete,
315}
316
317impl std::fmt::Display for HttpMethod {
318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 match self {
320 HttpMethod::Get => write!(f, "GET"),
321 HttpMethod::Post => write!(f, "POST"),
322 HttpMethod::Put => write!(f, "PUT"),
323 HttpMethod::Delete => write!(f, "DELETE"),
324 }
325 }
326}
327
328#[derive(Debug, Clone, Deserialize, Default)]
329#[serde(rename_all = "snake_case")]
330pub enum ResponseFormat {
331 MarkdownTable,
332 Json,
333 #[default]
334 Text,
335 Raw,
336}
337
338#[derive(Debug, Clone, Deserialize, Default)]
339pub struct ResponseConfig {
340 #[serde(default)]
342 pub extract: Option<String>,
343 #[serde(default)]
345 pub format: ResponseFormat,
346}
347
348#[derive(Debug, Clone, Deserialize)]
349pub struct Tool {
350 pub name: String,
351 pub description: String,
352 #[serde(default)]
353 pub endpoint: String,
354 #[serde(default)]
355 pub method: HttpMethod,
356 #[serde(default)]
358 pub scope: Option<String>,
359 #[serde(default)]
361 pub input_schema: Option<serde_json::Value>,
362 #[serde(default)]
364 pub response: Option<ResponseConfig>,
365
366 #[serde(default)]
369 pub tags: Vec<String>,
370 #[serde(default)]
372 pub hint: Option<String>,
373 #[serde(default)]
375 pub examples: Vec<String>,
376}
377
378#[derive(Debug, Clone, Deserialize)]
381pub struct Manifest {
382 pub provider: Provider,
383 #[serde(default, rename = "tools")]
384 pub tools: Vec<Tool>,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct CachedProvider {
392 pub name: String,
393 pub provider_type: String,
395 #[serde(default)]
396 pub base_url: String,
397 #[serde(default)]
398 pub auth_type: String,
399 #[serde(default)]
400 pub auth_key_name: Option<String>,
401 #[serde(default)]
402 pub auth_header_name: Option<String>,
403 #[serde(default)]
404 pub auth_query_name: Option<String>,
405 #[serde(default)]
407 pub spec_content: Option<String>,
408 #[serde(default)]
410 pub mcp_transport: Option<String>,
411 #[serde(default)]
412 pub mcp_url: Option<String>,
413 #[serde(default)]
414 pub mcp_command: Option<String>,
415 #[serde(default)]
416 pub mcp_args: Vec<String>,
417 #[serde(default)]
418 pub mcp_env: HashMap<String, String>,
419 #[serde(default)]
421 pub cli_command: Option<String>,
422 #[serde(default)]
423 pub cli_default_args: Vec<String>,
424 #[serde(default)]
425 pub cli_env: HashMap<String, String>,
426 #[serde(default)]
427 pub cli_timeout_secs: Option<u64>,
428 #[serde(default)]
430 pub auth: Option<String>,
431 #[serde(default)]
433 pub skills: Vec<String>,
434 pub created_at: String,
436 pub ttl_seconds: u64,
437}
438
439impl CachedProvider {
440 pub fn is_expired(&self) -> bool {
442 let created = match DateTime::parse_from_rfc3339(&self.created_at) {
443 Ok(dt) => dt.with_timezone(&Utc),
444 Err(_) => return true, };
446 let now = Utc::now();
447 let elapsed = now.signed_duration_since(created);
448 elapsed.num_seconds() as u64 > self.ttl_seconds
449 }
450
451 pub fn expires_at(&self) -> Option<String> {
453 let created = DateTime::parse_from_rfc3339(&self.created_at).ok()?;
454 let expires = created + chrono::Duration::seconds(self.ttl_seconds as i64);
455 Some(expires.to_rfc3339())
456 }
457
458 pub fn remaining_seconds(&self) -> u64 {
460 let created = match DateTime::parse_from_rfc3339(&self.created_at) {
461 Ok(dt) => dt.with_timezone(&Utc),
462 Err(_) => return 0,
463 };
464 let now = Utc::now();
465 let elapsed = now.signed_duration_since(created).num_seconds() as u64;
466 self.ttl_seconds.saturating_sub(elapsed)
467 }
468
469 pub fn to_provider(&self) -> Provider {
471 let auth_type = match self.auth_type.as_str() {
472 "bearer" => AuthType::Bearer,
473 "header" => AuthType::Header,
474 "query" => AuthType::Query,
475 "basic" => AuthType::Basic,
476 "oauth2" => AuthType::Oauth2,
477 _ => AuthType::None,
478 };
479
480 let handler = match self.provider_type.as_str() {
481 "mcp" => "mcp".to_string(),
482 "openapi" => "openapi".to_string(),
483 _ => "http".to_string(),
484 };
485
486 Provider {
487 name: self.name.clone(),
488 description: format!("{} (cached)", self.name),
489 base_url: self.base_url.clone(),
490 auth_type,
491 auth_key_name: self.auth_key_name.clone(),
492 auth_header_name: self.auth_header_name.clone(),
493 auth_query_name: self.auth_query_name.clone(),
494 auth_value_prefix: None,
495 extra_headers: HashMap::new(),
496 oauth2_token_url: None,
497 auth_secret_name: None,
498 auth_session_token_env: None,
499 mcp_url_env: None,
500 oauth2_basic_auth: false,
501 internal: false,
502 handler,
503 mcp_transport: self.mcp_transport.clone(),
504 mcp_command: self.mcp_command.clone(),
505 mcp_args: self.mcp_args.clone(),
506 mcp_url: self.mcp_url.clone(),
507 mcp_env: self.mcp_env.clone(),
508 openapi_spec: None,
509 openapi_include_tags: Vec::new(),
510 openapi_exclude_tags: Vec::new(),
511 openapi_include_operations: Vec::new(),
512 openapi_exclude_operations: Vec::new(),
513 openapi_max_operations: None,
514 openapi_overrides: HashMap::new(),
515 cli_command: self.cli_command.clone(),
516 cli_default_args: self.cli_default_args.clone(),
517 cli_env: self.cli_env.clone(),
518 cli_timeout_secs: self.cli_timeout_secs,
519 cli_output_args: Vec::new(),
520 cli_output_positional: HashMap::new(),
521 upload_destinations: HashMap::new(),
522 upload_default_destination: None,
523 auth_generator: None,
524 category: None,
525 skills: self.skills.clone(),
526 }
527 }
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
533pub struct McpToolDef {
534 pub name: String,
535 #[serde(default)]
536 pub description: Option<String>,
537 #[serde(default, rename = "inputSchema")]
538 pub input_schema: Option<serde_json::Value>,
539}
540
541pub struct ManifestRegistry {
543 manifests: Vec<Manifest>,
544 tool_index: HashMap<String, (usize, usize)>,
546}
547
548impl ManifestRegistry {
549 pub fn load(dir: &Path) -> Result<Self, ManifestError> {
552 if !dir.is_dir() {
553 return Err(ManifestError::NoDirectory(dir.display().to_string()));
554 }
555
556 let mut manifests = Vec::new();
557 let mut tool_index = HashMap::new();
558
559 let pattern = dir.join("*.toml");
560 let entries = glob::glob(pattern.to_str().unwrap_or(""))
561 .map_err(|e| ManifestError::NoDirectory(e.to_string()))?;
562
563 let specs_dir = dir.parent().map(|p| p.join("specs"));
565
566 for entry in entries {
567 let path = entry.map_err(|e| {
568 ManifestError::Io(format!("{e}"), std::io::Error::other("glob error"))
569 })?;
570 let contents = std::fs::read_to_string(&path)
571 .map_err(|e| ManifestError::Io(path.display().to_string(), e))?;
572 let mut manifest: Manifest = toml::from_str(&contents)
573 .map_err(|e| ManifestError::Parse(path.display().to_string(), e))?;
574
575 if manifest.provider.is_openapi() {
577 if let Some(spec_ref) = &manifest.provider.openapi_spec {
578 match crate::core::openapi::load_and_register(
579 &manifest.provider,
580 spec_ref,
581 specs_dir.as_deref(),
582 ) {
583 Ok(tools) => {
584 manifest.tools = tools;
585 }
586 Err(e) => {
587 tracing::warn!(
588 provider = %manifest.provider.name,
589 error = %e,
590 "failed to load OpenAPI spec for provider"
591 );
592 }
594 }
595 }
596 }
597
598 if manifest.provider.handler == "file_manager" {
602 if let Some(ref default) = manifest.provider.upload_default_destination {
603 if !manifest.provider.upload_destinations.contains_key(default) {
604 return Err(ManifestError::Invalid(
605 path.display().to_string(),
606 format!(
607 "upload_default_destination '{default}' is not present in [provider.upload_destinations]"
608 ),
609 ));
610 }
611 }
612 }
613
614 if let Some(ref env_name) = manifest.provider.mcp_url_env {
618 let trimmed = env_name.trim();
619 if trimmed.is_empty() {
620 return Err(ManifestError::Invalid(
621 path.display().to_string(),
622 "mcp_url_env must not be empty when set".to_string(),
623 ));
624 }
625 let valid_name = trimmed.chars().enumerate().all(|(i, c)| {
629 if i == 0 {
630 c.is_ascii_alphabetic() || c == '_'
631 } else {
632 c.is_ascii_alphanumeric() || c == '_'
633 }
634 });
635 if !valid_name {
636 return Err(ManifestError::Invalid(
637 path.display().to_string(),
638 format!("mcp_url_env '{env_name}' is not a valid POSIX env var name"),
639 ));
640 }
641 let transport = manifest.provider.mcp_transport.as_deref().unwrap_or("");
642 if !manifest.provider.is_mcp() || transport != "http" {
643 return Err(ManifestError::Invalid(
644 path.display().to_string(),
645 format!(
646 "mcp_url_env requires handler = \"mcp\" and mcp_transport = \"http\" (got handler = \"{}\", transport = \"{}\")",
647 manifest.provider.handler, transport
648 ),
649 ));
650 }
651 }
652
653 if manifest.provider.is_cli() && manifest.tools.is_empty() {
655 let tool_name = manifest.provider.name.clone();
656 manifest.tools.push(Tool {
657 name: tool_name.clone(),
658 description: manifest.provider.description.clone(),
659 endpoint: String::new(),
660 method: HttpMethod::Get,
661 scope: Some(format!("tool:{tool_name}")),
662 input_schema: None,
663 response: None,
664 tags: Vec::new(),
665 hint: None,
666 examples: Vec::new(),
667 });
668 }
669
670 let provider_name = &manifest.provider.name;
673 for tool in &mut manifest.tools {
674 if tool.scope.is_none() && !manifest.provider.internal {
675 tool.scope = Some(format!("tool:{}", tool.name));
676 tracing::trace!(
677 tool = %tool.name,
678 provider = %provider_name,
679 scope = ?tool.scope,
680 "auto-assigned scope to tool"
681 );
682 }
683 }
684
685 let mi = manifests.len();
686 for (ti, tool) in manifest.tools.iter().enumerate() {
687 tool_index.insert(tool.name.clone(), (mi, ti));
688 }
689 manifests.push(manifest);
690 }
691
692 if let Some(parent) = dir.parent() {
695 let cache_dir = parent.join("cache").join("providers");
696 if cache_dir.is_dir() {
697 let cache_pattern = cache_dir.join("*.json");
698 if let Ok(cache_entries) = glob::glob(cache_pattern.to_str().unwrap_or("")) {
699 for entry in cache_entries {
700 let path = match entry {
701 Ok(p) => p,
702 Err(_) => continue,
703 };
704 let content = match std::fs::read_to_string(&path) {
705 Ok(c) => c,
706 Err(_) => continue,
707 };
708 let cached: CachedProvider = match serde_json::from_str(&content) {
709 Ok(c) => c,
710 Err(_) => continue,
711 };
712
713 if cached.is_expired() {
715 let _ = std::fs::remove_file(&path);
716 continue;
717 }
718
719 if manifests.iter().any(|m| m.provider.name == cached.name) {
721 continue;
722 }
723
724 let provider = cached.to_provider();
725
726 let mut cached_tools = Vec::new();
727 if cached.provider_type == "openapi" {
728 if let Some(spec_content) = &cached.spec_content {
729 if let Ok(spec) = crate::core::openapi::parse_spec(spec_content) {
730 let filters = crate::core::openapi::OpenApiFilters {
731 include_tags: vec![],
732 exclude_tags: vec![],
733 include_operations: vec![],
734 exclude_operations: vec![],
735 max_operations: None,
736 };
737 let defs = crate::core::openapi::extract_tools(&spec, &filters);
738 cached_tools = defs
739 .into_iter()
740 .map(|def| {
741 crate::core::openapi::to_ati_tool(
742 def,
743 &cached.name,
744 &HashMap::new(),
745 )
746 })
747 .collect();
748 }
749 }
750 }
751 let mi = manifests.len();
754 for (ti, tool) in cached_tools.iter().enumerate() {
755 tool_index.insert(tool.name.clone(), (mi, ti));
756 }
757 manifests.push(Manifest {
758 provider,
759 tools: cached_tools,
760 });
761 }
762 }
763 }
764 }
765
766 let mut registry = ManifestRegistry {
767 manifests,
768 tool_index,
769 };
770 register_file_manager_provider(&mut registry);
771 Ok(registry)
772 }
773
774 pub fn empty() -> Self {
776 let mut registry = ManifestRegistry {
777 manifests: Vec::new(),
778 tool_index: HashMap::new(),
779 };
780 register_file_manager_provider(&mut registry);
781 registry
782 }
783
784 pub fn get_tool(&self, name: &str) -> Option<(&Provider, &Tool)> {
786 self.tool_index.get(name).map(|(mi, ti)| {
787 let m = &self.manifests[*mi];
788 (&m.provider, &m.tools[*ti])
789 })
790 }
791
792 pub fn list_tools(&self) -> Vec<(&Provider, &Tool)> {
794 self.manifests
795 .iter()
796 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
797 .collect()
798 }
799
800 pub fn list_providers(&self) -> Vec<&Provider> {
802 self.manifests.iter().map(|m| &m.provider).collect()
803 }
804
805 pub fn list_public_tools(&self) -> Vec<(&Provider, &Tool)> {
807 self.manifests
808 .iter()
809 .filter(|m| !m.provider.internal)
810 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
811 .collect()
812 }
813
814 pub fn tool_count(&self) -> usize {
816 self.tool_index.len()
817 }
818
819 pub fn provider_count(&self) -> usize {
821 self.manifests.len()
822 }
823
824 pub fn list_mcp_providers(&self) -> Vec<&Provider> {
826 self.manifests
827 .iter()
828 .filter(|m| m.provider.handler == "mcp")
829 .map(|m| &m.provider)
830 .collect()
831 }
832
833 pub fn find_mcp_provider_for_tool(&self, tool_name: &str) -> Option<&Provider> {
835 let prefix = tool_name.split(TOOL_SEP).next()?;
836 self.manifests
837 .iter()
838 .find(|m| m.provider.handler == "mcp" && m.provider.name == prefix)
839 .map(|m| &m.provider)
840 }
841
842 pub fn list_openapi_providers(&self) -> Vec<&Provider> {
844 self.manifests
845 .iter()
846 .filter(|m| m.provider.handler == "openapi")
847 .map(|m| &m.provider)
848 .collect()
849 }
850
851 pub fn has_provider(&self, name: &str) -> bool {
853 self.manifests.iter().any(|m| m.provider.name == name)
854 }
855
856 pub fn tools_by_provider(&self, provider_name: &str) -> Vec<(&Provider, &Tool)> {
858 self.manifests
859 .iter()
860 .filter(|m| m.provider.name == provider_name)
861 .flat_map(|m| m.tools.iter().map(move |t| (&m.provider, t)))
862 .collect()
863 }
864
865 pub fn list_cli_providers(&self) -> Vec<&Provider> {
867 self.manifests
868 .iter()
869 .filter(|m| m.provider.handler == "cli")
870 .map(|m| &m.provider)
871 .collect()
872 }
873
874 pub fn register_mcp_tools(&mut self, provider_name: &str, mcp_tools: Vec<McpToolDef>) {
877 let mi = match self
879 .manifests
880 .iter()
881 .position(|m| m.provider.name == provider_name)
882 {
883 Some(idx) => idx,
884 None => return,
885 };
886
887 for mcp_tool in mcp_tools {
888 let prefixed_name = format!("{}{}{}", provider_name, TOOL_SEP_STR, mcp_tool.name);
889
890 let tool = Tool {
891 name: prefixed_name.clone(),
892 description: mcp_tool.description.unwrap_or_default(),
893 endpoint: String::new(),
894 method: HttpMethod::Post,
895 scope: Some(format!("tool:{prefixed_name}")),
896 input_schema: mcp_tool.input_schema,
897 response: None,
898 tags: Vec::new(),
899 hint: None,
900 examples: Vec::new(),
901 };
902
903 let ti = self.manifests[mi].tools.len();
904 self.manifests[mi].tools.push(tool);
905 self.tool_index.insert(prefixed_name, (mi, ti));
906 }
907 }
908}
909
910impl Provider {
911 pub fn is_mcp(&self) -> bool {
913 self.handler == "mcp"
914 }
915
916 pub fn is_openapi(&self) -> bool {
918 self.handler == "openapi"
919 }
920
921 pub fn is_cli(&self) -> bool {
923 self.handler == "cli"
924 }
925
926 pub fn mcp_transport_type(&self) -> &str {
928 self.mcp_transport.as_deref().unwrap_or("stdio")
929 }
930
931 pub fn is_file_manager(&self) -> bool {
933 self.handler == "file_manager"
934 }
935}
936
937pub(crate) fn register_file_manager_provider(registry: &mut ManifestRegistry) {
948 let download_tool = build_file_manager_download_tool();
949 let upload_tool = build_file_manager_upload_tool();
950
951 if let Some(mi) = registry
952 .manifests
953 .iter()
954 .position(|m| m.provider.handler == "file_manager")
955 {
956 if registry.manifests[mi].tools.is_empty() {
958 let tools = vec![download_tool, upload_tool];
959 for (ti, tool) in tools.iter().enumerate() {
960 registry.tool_index.insert(tool.name.clone(), (mi, ti));
961 }
962 registry.manifests[mi].tools = tools;
963 }
964 return;
965 }
966
967 let provider = Provider {
968 name: "file_manager".to_string(),
969 description: "Generic binary download/upload for agents".to_string(),
970 base_url: String::new(),
971 auth_type: AuthType::None,
972 auth_key_name: None,
973 auth_header_name: None,
974 auth_query_name: None,
975 auth_value_prefix: None,
976 extra_headers: HashMap::new(),
977 oauth2_token_url: None,
978 auth_secret_name: None,
979 auth_session_token_env: None,
980 mcp_url_env: None,
981 oauth2_basic_auth: false,
982 internal: false,
983 handler: "file_manager".to_string(),
984 mcp_transport: None,
985 mcp_command: None,
986 mcp_args: Vec::new(),
987 mcp_url: None,
988 mcp_env: HashMap::new(),
989 cli_command: None,
990 cli_default_args: Vec::new(),
991 cli_env: HashMap::new(),
992 cli_timeout_secs: None,
993 cli_output_args: Vec::new(),
994 cli_output_positional: HashMap::new(),
995 upload_destinations: HashMap::new(),
996 upload_default_destination: None,
997 openapi_spec: None,
998 openapi_include_tags: Vec::new(),
999 openapi_exclude_tags: Vec::new(),
1000 openapi_include_operations: Vec::new(),
1001 openapi_exclude_operations: Vec::new(),
1002 openapi_max_operations: None,
1003 openapi_overrides: HashMap::new(),
1004 auth_generator: None,
1005 category: Some("file_manager".to_string()),
1006 skills: Vec::new(),
1007 };
1008
1009 let tools = vec![download_tool, upload_tool];
1010 let mi = registry.manifests.len();
1011 for (ti, tool) in tools.iter().enumerate() {
1012 registry.tool_index.insert(tool.name.clone(), (mi, ti));
1013 }
1014 registry.manifests.push(Manifest { provider, tools });
1015}
1016
1017fn build_file_manager_download_tool() -> Tool {
1018 let schema = serde_json::json!({
1019 "type": "object",
1020 "required": ["url"],
1021 "properties": {
1022 "url": {"type": "string", "description": "URL to fetch bytes from"},
1023 "out": {"type": "string", "description": "Local path to write bytes; if omitted, returns base64 inline"},
1024 "inline": {"type": "boolean", "description": "Return bytes as base64 in the response instead of writing to disk"},
1025 "max_bytes": {"type": "integer", "description": "Abort if body exceeds this many bytes (default 500 MB)"},
1026 "timeout": {"type": "integer", "description": "Request timeout in seconds (default 120)"},
1027 "headers": {"type": "object", "description": "Extra request headers, e.g. {\"Authorization\": \"Bearer abc\"}"},
1028 "follow_redirects": {"type": "boolean", "description": "Follow 3xx redirects (default true)"}
1029 }
1030 });
1031
1032 Tool {
1033 name: "file_manager:download".to_string(),
1034 description: "Download bytes from a URL. Writes to --out <path> or returns base64 inline."
1035 .to_string(),
1036 endpoint: String::new(),
1037 method: HttpMethod::Post,
1038 scope: Some("tool:file_manager:download".to_string()),
1039 input_schema: Some(schema),
1040 response: None,
1041 tags: vec![
1042 "file".to_string(),
1043 "download".to_string(),
1044 "binary".to_string(),
1045 ],
1046 hint: Some(
1047 "Use for 'I have a URL, give me the bytes' — images, video, audio, PDFs, CSVs, ZIPs."
1048 .to_string(),
1049 ),
1050 examples: vec![
1051 "ati run file_manager:download --url https://example.com/file.mp4 --out /tmp/clip.mp4"
1052 .to_string(),
1053 "ati run file_manager:download --url https://example.com/data.csv --inline true"
1054 .to_string(),
1055 ],
1056 }
1057}
1058
1059fn build_file_manager_upload_tool() -> Tool {
1060 let schema = serde_json::json!({
1061 "type": "object",
1062 "required": ["path"],
1063 "properties": {
1064 "path": {"type": "string", "description": "Local file path to upload"},
1065 "content_type": {"type": "string", "description": "Override MIME type (default: inferred from extension)"},
1066 "object_name": {"type": "string", "description": "Object key (when destination is GCS-style); default: auto-generated"},
1067 "destination": {"type": "string", "description": "Allowlist key declared in the operator's file_manager.toml manifest (e.g. \"fal\", \"gcs\"). Omit to use the operator default."}
1068 }
1069 });
1070
1071 Tool {
1072 name: "file_manager:upload".to_string(),
1073 description: "Upload a local file to a manifest-declared destination, return a public URL.".to_string(),
1074 endpoint: String::new(),
1075 method: HttpMethod::Post,
1076 scope: Some("tool:file_manager:upload".to_string()),
1077 input_schema: Some(schema),
1078 response: None,
1079 tags: vec!["file".to_string(), "upload".to_string(), "binary".to_string()],
1080 hint: Some("Upload a local file to a manifest-declared destination (GCS, fal_storage, etc.) and get a public URL.".to_string()),
1081 examples: vec![
1082 "ati run file_manager:upload --path /tmp/narration.mp3".to_string(),
1083 "ati run file_manager:upload --path /tmp/report.pdf --destination gcs".to_string(),
1084 ],
1085 }
1086}