Skip to main content
Glama

tfmcp

by nwiizo
client.rs30.9 kB
use reqwest::Client; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use thiserror::Error; use tracing::{debug, error, info, warn}; #[derive(Error, Debug, Clone)] pub enum RegistryError { #[error("HTTP request failed: {0}")] HttpError(String), #[error("JSON parsing failed: {0}")] JsonError(String), #[error("Provider '{provider}' not found in namespace '{namespace}'. Try using a different namespace or let the system auto-fallback to common namespaces (hashicorp, terraform-providers, community).")] ProviderNotFound { provider: String, namespace: String }, #[error("Service '{service}' not found for provider '{provider}' in namespace '{namespace}'. Check the service name spelling or browse available services first.")] #[allow(dead_code)] ServiceNotFound { service: String, provider: String, namespace: String, }, #[error("Documentation not found for '{doc_id}'. The documentation may have been moved or the ID may be incorrect.")] DocumentationNotFound { doc_id: String }, #[error("Invalid response format from Terraform Registry API. This may indicate a temporary service issue or API changes.")] InvalidResponse, #[error("Rate limit exceeded. Please wait before making additional requests. The Terraform Registry has usage limits to ensure fair access.")] RateLimited, #[error("Search returned no results for query '{query}'. Try using broader search terms or check spelling.")] NoSearchResults { query: String }, #[error("Provider '{provider}' exists but has no available versions in namespace '{namespace}'. This may indicate a deprecated or invalid provider.")] NoVersionsAvailable { provider: String, namespace: String }, } impl From<reqwest::Error> for RegistryError { fn from(error: reqwest::Error) -> Self { RegistryError::HttpError(error.to_string()) } } impl From<serde_json::Error> for RegistryError { fn from(error: serde_json::Error) -> Self { RegistryError::JsonError(error.to_string()) } } // Flexible provider info structure that can handle multiple API versions #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ProviderInfo { pub name: String, pub namespace: String, #[serde(default)] pub version: String, #[serde(default)] pub description: String, #[serde(default)] pub downloads: u64, #[serde(default)] pub published_at: String, #[serde(default)] pub id: String, // Additional fields for API compatibility #[serde(default)] pub source: Option<String>, #[serde(default)] pub tag: Option<String>, #[serde(default)] pub logo_url: Option<String>, #[serde(default)] pub owner: Option<String>, #[serde(default)] pub tier: Option<String>, #[serde(default)] pub verified: Option<bool>, #[serde(default)] pub trusted: Option<bool>, // Catch unknown fields to avoid parsing failures #[serde(flatten)] pub extra: HashMap<String, Value>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DocIdResult { pub id: String, #[serde(default)] pub title: String, #[serde(default)] pub description: String, #[serde(default)] pub category: String, #[serde(default)] pub slug: Option<String>, #[serde(default)] pub path: Option<String>, #[serde(default)] pub subcategory: Option<String>, // Catch unknown fields #[serde(flatten)] pub extra: HashMap<String, Value>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProviderVersions { #[serde(default)] pub versions: Vec<String>, // Handle alternative response formats #[serde(default)] pub data: Option<Vec<VersionInfo>>, #[serde(flatten)] pub extra: HashMap<String, Value>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VersionInfo { #[serde(default)] pub version: String, #[serde(default)] pub published_at: Option<String>, #[serde(default)] pub protocols: Option<Vec<String>>, #[serde(flatten)] pub extra: HashMap<String, Value>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegistrySearchResponse { #[serde(default)] pub providers: Vec<ProviderInfo>, #[serde(default)] pub meta: HashMap<String, Value>, // Handle alternative response formats #[serde(default)] pub data: Option<Vec<ProviderInfo>>, #[serde(flatten)] pub extra: HashMap<String, Value>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProviderDocsResponse { #[serde(default)] pub data: Vec<DocIdResult>, // Handle alternative response formats #[serde(default)] pub docs: Option<Vec<DocIdResult>>, #[serde(default)] pub documentation: Option<Vec<DocIdResult>>, #[serde(flatten)] pub extra: HashMap<String, Value>, } pub struct RegistryClient { client: Client, base_url: String, } impl Default for RegistryClient { fn default() -> Self { Self::new() } } impl RegistryClient { pub fn new() -> Self { Self { client: Client::builder() .user_agent("tfmcp/0.1.3") .timeout(std::time::Duration::from_secs(30)) .build() .unwrap_or_else(|_| Client::new()), // Fallback to default client base_url: "https://registry.terraform.io".to_string(), } } /// Search for providers in the Terraform Registry with improved error handling pub async fn search_providers(&self, query: &str) -> Result<Vec<ProviderInfo>, RegistryError> { let url = format!("{}/v1/providers", self.base_url); debug!("Searching providers with query '{}' at URL: {}", query, url); let response = self.client.get(&url).query(&[("q", query)]).send().await?; let status = response.status(); debug!("Search response status: {}", status); if status == 429 { warn!("Rate limit exceeded for provider search"); return Err(RegistryError::RateLimited); } if !status.is_success() { error!("HTTP error {} for search request", status); return Err(RegistryError::HttpError(format!("HTTP {}", status))); } let response_text = response.text().await?; debug!( "Search response (first 1000 chars): {}", &response_text.chars().take(1000).collect::<String>() ); match serde_json::from_str::<Value>(&response_text) { Ok(json_value) => { debug!("Parsed search JSON structure: {:#?}", json_value); match serde_json::from_value::<RegistrySearchResponse>(json_value.clone()) { Ok(mut search_response) => { // Handle alternative response formats if search_response.providers.is_empty() { if let Some(data) = search_response.data.take() { search_response.providers = data; } } if search_response.providers.is_empty() { info!("No search results found for query: {}", query); return Err(RegistryError::NoSearchResults { query: query.to_string(), }); } info!( "Found {} providers for query: {}", search_response.providers.len(), query ); Ok(search_response.providers) } Err(e) => { error!("Failed to deserialize search response: {}", e); // Try manual extraction if let Some(providers_array) = json_value.get("providers").and_then(|v| v.as_array()) { let providers = self.extract_providers_from_array(providers_array); if !providers.is_empty() { warn!("Using fallback provider search parsing"); return Ok(providers); } } Err(RegistryError::JsonError(format!( "Failed to parse search response: {}", e ))) } } } Err(e) => { error!("Failed to parse search JSON: {}", e); error!("Response text was: {}", response_text); Err(RegistryError::JsonError(format!( "Invalid JSON response: {}", e ))) } } } /// Helper function to extract providers from JSON array with fallback parsing fn extract_providers_from_array(&self, providers_array: &[Value]) -> Vec<ProviderInfo> { providers_array .iter() .filter_map(|provider| { let mut provider_info = ProviderInfo::default(); if let Some(name) = provider.get("name").and_then(|v| v.as_str()) { provider_info.name = name.to_string(); } else { return None; // Name is required } if let Some(namespace) = provider.get("namespace").and_then(|v| v.as_str()) { provider_info.namespace = namespace.to_string(); } if let Some(desc) = provider.get("description").and_then(|v| v.as_str()) { provider_info.description = desc.to_string(); } if let Some(downloads) = provider.get("downloads").and_then(|v| v.as_u64()) { provider_info.downloads = downloads; } if let Some(version) = provider.get("version").and_then(|v| v.as_str()) { provider_info.version = version.to_string(); } Some(provider_info) }) .collect() } /// Get provider information by namespace and name with detailed error logging pub async fn get_provider_info( &self, provider_name: &str, namespace: &str, ) -> Result<ProviderInfo, RegistryError> { let url = format!( "{}/v1/providers/{}/{}", self.base_url, namespace, provider_name ); debug!("Fetching provider info from URL: {}", url); let response = self.client.get(&url).send().await?; let status = response.status(); debug!("Response status: {}", status); debug!("Response headers: {:?}", response.headers()); if status == 404 { warn!("Provider not found: {}/{}", namespace, provider_name); return Err(RegistryError::ProviderNotFound { provider: provider_name.to_string(), namespace: namespace.to_string(), }); } if status == 429 { warn!("Rate limit exceeded for provider info request"); return Err(RegistryError::RateLimited); } if !status.is_success() { error!( "HTTP error {}: {}", status, status.canonical_reason().unwrap_or("Unknown") ); return Err(RegistryError::HttpError(format!("HTTP {}", status))); } // Get response text for detailed debugging let response_text = response.text().await?; debug!( "Response body (first 1000 chars): {}", &response_text.chars().take(1000).collect::<String>() ); // First try to parse as generic JSON to debug structure match serde_json::from_str::<Value>(&response_text) { Ok(json_value) => { debug!("Successfully parsed JSON. Structure: {:#?}", json_value); // Now try to deserialize into ProviderInfo match serde_json::from_value::<ProviderInfo>(json_value.clone()) { Ok(provider_info) => { info!( "Successfully retrieved provider info for {}/{}", namespace, provider_name ); Ok(provider_info) } Err(e) => { error!("Failed to deserialize ProviderInfo: {}", e); error!( "Parsed JSON was: {}", serde_json::to_string_pretty(&json_value) .unwrap_or_else(|_| "Invalid JSON".to_string()) ); // Try to extract essential fields manually let provider_info = ProviderInfo { name: json_value .get("name") .and_then(|v| v.as_str()) .unwrap_or(provider_name) .to_string(), namespace: json_value .get("namespace") .and_then(|v| v.as_str()) .unwrap_or(namespace) .to_string(), description: json_value .get("description") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), downloads: json_value .get("downloads") .and_then(|v| v.as_u64()) .unwrap_or(0), ..Default::default() }; warn!("Using fallback provider info parsing due to deserialization error"); Ok(provider_info) } } } Err(e) => { error!("Failed to parse JSON: {}", e); error!("Response text was: {}", response_text); Err(RegistryError::JsonError(format!( "Invalid JSON response: {}", e ))) } } } /// Get latest version of a provider with improved error handling pub async fn get_latest_version( &self, provider_name: &str, namespace: &str, ) -> Result<String, RegistryError> { let url = format!( "{}/v1/providers/{}/{}/versions", self.base_url, namespace, provider_name ); debug!("Fetching provider versions from URL: {}", url); let response = self.client.get(&url).send().await?; let status = response.status(); debug!("Response status: {}", status); if status == 404 { warn!( "Provider versions not found: {}/{}", namespace, provider_name ); return Err(RegistryError::ProviderNotFound { provider: provider_name.to_string(), namespace: namespace.to_string(), }); } if status == 429 { warn!("Rate limit exceeded for provider versions request"); return Err(RegistryError::RateLimited); } if !status.is_success() { error!( "HTTP error {}: {}", status, status.canonical_reason().unwrap_or("Unknown") ); return Err(RegistryError::HttpError(format!("HTTP {}", status))); } let response_text = response.text().await?; debug!( "Versions response (first 500 chars): {}", &response_text.chars().take(500).collect::<String>() ); // Parse JSON and handle multiple response formats match serde_json::from_str::<Value>(&response_text) { Ok(json_value) => { debug!("Parsed versions JSON structure: {:#?}", json_value); // Try to deserialize into ProviderVersions match serde_json::from_value::<ProviderVersions>(json_value.clone()) { Ok(mut versions) => { // Handle alternative response formats if versions.versions.is_empty() { if let Some(data) = versions.data.as_ref() { versions.versions = data .iter() .map(|v| v.version.clone()) .filter(|v| !v.is_empty()) .collect(); } } if versions.versions.is_empty() { warn!( "No versions available for provider {}/{}", namespace, provider_name ); return Err(RegistryError::NoVersionsAvailable { provider: provider_name.to_string(), namespace: namespace.to_string(), }); } let latest_version = versions .versions .first() .cloned() .ok_or(RegistryError::InvalidResponse)?; info!( "Found latest version {} for provider {}/{}", latest_version, namespace, provider_name ); Ok(latest_version) } Err(e) => { error!("Failed to deserialize ProviderVersions: {}", e); // Try manual extraction if let Some(versions_array) = json_value.get("versions").and_then(|v| v.as_array()) { if let Some(first_version) = versions_array.first().and_then(|v| v.as_str()) { warn!("Using fallback version parsing"); return Ok(first_version.to_string()); } } Err(RegistryError::JsonError(format!( "Failed to parse versions: {}", e ))) } } } Err(e) => { error!("Failed to parse versions JSON: {}", e); error!("Response text was: {}", response_text); Err(RegistryError::JsonError(format!( "Invalid JSON response: {}", e ))) } } } /// Search for provider documentation IDs with multiple endpoint patterns pub async fn search_docs( &self, provider_name: &str, namespace: &str, service_slug: &str, data_type: &str, ) -> Result<Vec<DocIdResult>, RegistryError> { debug!( "Searching docs for provider: {}/{}, service: {}, type: {}", namespace, provider_name, service_slug, data_type ); // Try multiple URL patterns as the API endpoint may vary let url_patterns = [ format!( "{}/v1/providers/{}/{}/docs", self.base_url, namespace, provider_name ), format!( "{}/v2/providers/{}/{}/docs", self.base_url, namespace, provider_name ), format!( "{}/providers/{}/{}/docs", self.base_url, namespace, provider_name ), format!( "{}/docs/providers/{}/{}", self.base_url, namespace, provider_name ), ]; let query_params = [ vec![("category", data_type), ("slug", service_slug)], vec![("type", data_type), ("slug", service_slug)], vec![ ("filter[category]", data_type), ("filter[slug]", service_slug), ], vec![("q", service_slug), ("category", data_type)], ]; for (url_idx, url) in url_patterns.iter().enumerate() { for params in query_params.iter() { debug!( "Trying URL pattern {}/{}: {} with params: {:?}", url_idx + 1, url_patterns.len(), url, params ); let response = self.client.get(url).query(params).send().await?; let status = response.status(); debug!("Response status: {} for URL: {}", status, url); if status == 429 { warn!("Rate limit exceeded for docs search"); return Err(RegistryError::RateLimited); } if status == 404 { debug!( "404 for pattern {}/{}, trying next pattern", url_idx + 1, url_patterns.len() ); continue; } if !status.is_success() { warn!("HTTP error {} for docs URL: {}", status, url); continue; } let response_text = response.text().await?; debug!( "Docs response (first 500 chars): {}", &response_text.chars().take(500).collect::<String>() ); match serde_json::from_str::<Value>(&response_text) { Ok(json_value) => { debug!("Parsed docs JSON structure: {:#?}", json_value); // Try to deserialize into ProviderDocsResponse match serde_json::from_value::<ProviderDocsResponse>(json_value.clone()) { Ok(mut docs_response) => { // Handle multiple response format possibilities if docs_response.data.is_empty() { if let Some(docs) = docs_response.docs.take() { docs_response.data = docs; } else if let Some(documentation) = docs_response.documentation.take() { docs_response.data = documentation; } } if !docs_response.data.is_empty() { info!( "Found {} docs for {}/{} service: {}", docs_response.data.len(), namespace, provider_name, service_slug ); return Ok(docs_response.data); } } Err(e) => { warn!("Failed to deserialize docs response: {}", e); // Try manual extraction from various JSON structures if let Some(docs_array) = json_value.get("data").and_then(|v| v.as_array()) { let docs = self.extract_docs_from_array(docs_array); if !docs.is_empty() { info!( "Extracted {} docs using fallback parsing", docs.len() ); return Ok(docs); } } if let Some(docs_array) = json_value.get("docs").and_then(|v| v.as_array()) { let docs = self.extract_docs_from_array(docs_array); if !docs.is_empty() { info!( "Extracted {} docs using fallback parsing (docs field)", docs.len() ); return Ok(docs); } } // Try direct array if let Some(docs_array) = json_value.as_array() { let docs = self.extract_docs_from_array(docs_array); if !docs.is_empty() { info!("Extracted {} docs using fallback parsing (direct array)", docs.len()); return Ok(docs); } } } } } Err(e) => { warn!("Failed to parse docs JSON: {}", e); continue; } } } } warn!( "No documentation found for {}/{} service: {} after trying all patterns", namespace, provider_name, service_slug ); Ok(vec![]) } /// Helper function to extract docs from JSON array with fallback parsing fn extract_docs_from_array(&self, docs_array: &[Value]) -> Vec<DocIdResult> { docs_array .iter() .filter_map(|doc| { let doc_result = DocIdResult { id: doc .get("id") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), title: doc .get("title") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), description: doc .get("description") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), category: doc .get("category") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), slug: doc .get("slug") .and_then(|v| v.as_str()) .map(|s| s.to_string()), path: doc .get("path") .and_then(|v| v.as_str()) .map(|s| s.to_string()), subcategory: doc .get("subcategory") .and_then(|v| v.as_str()) .map(|s| s.to_string()), extra: HashMap::new(), }; // Only include if we have essential fields if !doc_result.id.is_empty() || !doc_result.title.is_empty() { Some(doc_result) } else { None } }) .collect() } /// Get provider documentation content by ID with multiple endpoint patterns pub async fn get_doc_content(&self, doc_id: &str) -> Result<String, RegistryError> { debug!("Fetching documentation content for ID: {}", doc_id); // Try multiple URL patterns for documentation content let url_patterns = [ format!("{}/v1/docs/{}", self.base_url, doc_id), format!("{}/v2/docs/{}", self.base_url, doc_id), format!("{}/docs/{}", self.base_url, doc_id), format!("{}/documentation/{}", self.base_url, doc_id), ]; for (idx, url) in url_patterns.iter().enumerate() { debug!( "Trying documentation URL pattern {}/{}: {}", idx + 1, url_patterns.len(), url ); let response = self.client.get(url).send().await?; let status = response.status(); debug!("Response status: {} for docs URL: {}", status, url); if status == 429 { warn!("Rate limit exceeded for documentation content"); return Err(RegistryError::RateLimited); } if status == 404 { debug!( "404 for docs pattern {}/{}, trying next pattern", idx + 1, url_patterns.len() ); continue; } if !status.is_success() { warn!("HTTP error {} for docs content URL: {}", status, url); continue; } let content = response.text().await?; debug!( "Retrieved documentation content ({} chars) for ID: {}", content.len(), doc_id ); if !content.trim().is_empty() { info!( "Successfully retrieved documentation content for ID: {}", doc_id ); return Ok(content); } } error!( "Documentation not found for ID: {} after trying all patterns", doc_id ); Err(RegistryError::DocumentationNotFound { doc_id: doc_id.to_string(), }) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nwiizo/tfmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server