Skip to main content
Glama
lib.rs13.6 kB
// Re-export all module index types so that client users do not have to import two crates. pub use module_index_types::*; use reqwest::{ StatusCode, header::{ self, HeaderMap, HeaderValue, }, }; use si_pkg::WorkspaceExport; use thiserror::Error; use ulid::Ulid; use url::Url; #[remain::sorted] #[derive(Debug, Error)] pub enum ModuleIndexClientError { #[error("Deserialization error: {0}")] Deserialization(serde_json::Error), #[error("Request error: {0}")] InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue), #[error("module not found (module id: {0})")] ModuleNotFound(String), #[error("Request error: {0}")] Request(#[from] reqwest::Error), #[error("Serialization error: {0}")] Serialization(serde_json::Error), #[error("Url parse error: {0}")] UrlParse(#[from] url::ParseError), } pub type ModuleIndexClientResult<T> = Result<T, ModuleIndexClientError>; #[derive(Debug, Clone)] pub struct ModuleIndexClient { inner: reqwest::Client, base_url: Url, } impl ModuleIndexClient { pub fn new(base_url: Url, auth_token: &str) -> ModuleIndexClientResult<Self> { let headers = { let mut headers = HeaderMap::new(); let mut auth_header_value = HeaderValue::from_str(&format!("Bearer {auth_token}"))?; auth_header_value.set_sensitive(true); headers.insert(header::AUTHORIZATION, auth_header_value); headers }; let inner = reqwest::Client::builder() .default_headers(headers) .build()?; Ok(Self { inner, base_url }) } pub fn unauthenticated_client(base_url: Url) -> ModuleIndexClientResult<Self> { let inner = reqwest::Client::builder().build()?; Ok(Self { inner, base_url }) } pub async fn reject_module( &self, module_id: Ulid, rejected_by_display_name: String, ) -> ModuleIndexClientResult<ModuleRejectionResponse> { let reject_url = self .base_url .join("modules/")? .join(&format!("{}/", module_id.to_string()))? .join("reject")?; let upload_response = self .inner .post(reject_url) .multipart( reqwest::multipart::Form::new().text("rejected by user", rejected_by_display_name), ) .send() .await?; if upload_response.status() == reqwest::StatusCode::NOT_FOUND { return Err(ModuleIndexClientError::ModuleNotFound( module_id.to_string(), )); } let upload_response = upload_response.error_for_status()?; Ok(upload_response.json::<ModuleRejectionResponse>().await?) } pub async fn promote_to_builtin( &self, module_id: Ulid, promoted_to_builtin_by_display_name: String, ) -> ModuleIndexClientResult<ModulePromotedResponse> { let reject_url = self .base_url .join("builtins/")? .join(&format!("{}/", module_id.to_string()))? .join("promote")?; let promote_response = self .inner .post(reject_url) .multipart( reqwest::multipart::Form::new() .text("promoted by user", promoted_to_builtin_by_display_name), ) .send() .await?; if promote_response.status() == reqwest::StatusCode::NOT_FOUND { return Err(ModuleIndexClientError::ModuleNotFound( module_id.to_string(), )); } let promote_response = promote_response.error_for_status()?; Ok(promote_response.json::<ModulePromotedResponse>().await?) } #[allow(clippy::too_many_arguments)] pub async fn upload_module( &self, module_name: &str, module_version: &str, module_based_on_hash: Option<String>, module_schema_id: Option<String>, module_bytes: Vec<u8>, module_schema_variant_id: Option<String>, module_schema_variant_version: Option<String>, module_is_private_scoped: Option<bool>, ) -> ModuleIndexClientResult<ModuleDetailsResponse> { let module_upload_part = reqwest::multipart::Part::bytes(module_bytes) .file_name(format!("{module_name}_{module_version}.tar")); let mut multipart_form = reqwest::multipart::Form::new().part(MODULE_BUNDLE_FIELD_NAME, module_upload_part); if let Some(module_based_on_hash) = module_based_on_hash { multipart_form = multipart_form.part( MODULE_BASED_ON_HASH_FIELD_NAME, reqwest::multipart::Part::text(module_based_on_hash), ); } if let Some(schema_id) = module_schema_id { multipart_form = multipart_form.part( MODULE_SCHEMA_ID_FIELD_NAME, reqwest::multipart::Part::text(schema_id), ); } if let Some(schema_variant_id) = module_schema_variant_id { multipart_form = multipart_form.part( MODULE_SCHEMA_VARIANT_ID_FIELD_NAME, reqwest::multipart::Part::text(schema_variant_id), ); } if let Some(schema_variant_version) = module_schema_variant_version { multipart_form = multipart_form.part( MODULE_SCHEMA_VARIANT_VERSION_FIELD_NAME, reqwest::multipart::Part::text(schema_variant_version), ); } if let Some(is_private_scoped) = module_is_private_scoped { multipart_form = multipart_form.part( MODULE_IS_PRIVATE_SCOPED_FIELD_NAME, reqwest::multipart::Part::text(is_private_scoped.to_string()), ); } let upload_url = self.base_url.join("modules")?; let upload_response = self .inner .post(upload_url) .multipart(multipart_form) .send() .await? .error_for_status()?; Ok(upload_response.json::<ModuleDetailsResponse>().await?) } #[allow(clippy::too_many_arguments)] pub async fn upsert_builtin( &self, module_name: &str, module_version: &str, module_based_on_hash: Option<String>, module_schema_id: Option<String>, module_bytes: Vec<u8>, module_schema_variant_id: Option<String>, module_schema_variant_version: Option<String>, ) -> ModuleIndexClientResult<bool> { let module_upload_part = reqwest::multipart::Part::bytes(module_bytes) .file_name(format!("{module_name}_{module_version}.tar")); let mut multipart_form = reqwest::multipart::Form::new().part(MODULE_BUNDLE_FIELD_NAME, module_upload_part); if let Some(module_based_on_hash) = module_based_on_hash { multipart_form = multipart_form.part( MODULE_BASED_ON_HASH_FIELD_NAME, reqwest::multipart::Part::text(module_based_on_hash), ); } if let Some(schema_id) = module_schema_id { multipart_form = multipart_form.part( MODULE_SCHEMA_ID_FIELD_NAME, reqwest::multipart::Part::text(schema_id), ); } if let Some(schema_variant_id) = module_schema_variant_id { multipart_form = multipart_form.part( MODULE_SCHEMA_VARIANT_ID_FIELD_NAME, reqwest::multipart::Part::text(schema_variant_id), ); } if let Some(schema_variant_version) = module_schema_variant_version { multipart_form = multipart_form.part( MODULE_SCHEMA_VARIANT_VERSION_FIELD_NAME, reqwest::multipart::Part::text(schema_variant_version), ); } let upsert_url = self.base_url.join("builtins/upsert")?; let upsert_response = self .inner .post(upsert_url) .multipart(multipart_form) .send() .await? .error_for_status()?; Ok(upsert_response.json::<bool>().await?) } pub async fn download_module(&self, module_id: Ulid) -> ModuleIndexClientResult<Vec<u8>> { let download_url = self .base_url .join("modules/")? .join(&format!("{}/", module_id.to_string()))? .join("download")?; let response = self.inner.get(download_url).send().await?; if response.status() == reqwest::StatusCode::NOT_FOUND { return Err(ModuleIndexClientError::ModuleNotFound( module_id.to_string(), )); } let response = response.error_for_status()?; let bytes = response.bytes().await?; Ok(bytes.to_vec()) } pub async fn list_builtins(&self) -> ModuleIndexClientResult<BuiltinsDetailsResponse> { let url = self.base_url.join("builtins")?; let resp = self.inner.get(url).send().await?.error_for_status()?; let mut builtins = resp.json::<BuiltinsDetailsResponse>().await?; if builtins.modules.is_empty() && self.base_url.clone().as_str().contains("http://localhost") { // We want to fall back to the production module index to pull builtins from there instead let url = Url::parse("https://module-index.systeminit.com")?.join("builtins")?; let resp = self.inner.get(url).send().await?.error_for_status()?; builtins = resp.json::<BuiltinsDetailsResponse>().await? }; Ok(builtins) } pub async fn module_details( &self, module_id: Ulid, ) -> ModuleIndexClientResult<ModuleDetailsResponse> { let details_url = self .base_url .join("modules/")? .join(&format!("{module_id}"))?; let response = self.inner.get(details_url).send().await?; if response.status() == reqwest::StatusCode::NOT_FOUND { return Err(ModuleIndexClientError::ModuleNotFound( module_id.to_string(), )); } let response = response.error_for_status()?; let response = response.json().await?; Ok(response) } pub async fn get_builtin(&self, module_id: Ulid) -> ModuleIndexClientResult<Vec<u8>> { let download_url = self .base_url .join("modules/")? .join(&format!("{}/", module_id.to_string()))? .join("download_builtin")?; let mut response = self.inner.get(download_url).send().await?; if response.status() == StatusCode::NOT_FOUND && self.base_url.clone().as_str().contains("http://localhost") { // We want to fall back to the production module index to pull builtins from there instead let url = Url::parse("https://module-index.systeminit.com")? .join("modules/")? .join(&format!("{}/", module_id.to_string()))? .join("download_builtin")?; response = self.inner.get(url).send().await?; }; let bytes = response.error_for_status()?.bytes().await?; Ok(bytes.to_vec()) } pub async fn upload_workspace( &self, workspace_name: &str, workspace_version: &str, content: WorkspaceExport, ) -> ModuleIndexClientResult<()> { let bytes = serde_json::to_vec(&content).map_err(ModuleIndexClientError::Serialization)?; let upload_part = reqwest::multipart::Part::bytes(bytes) .file_name(format!("{workspace_name}_{workspace_version}.tar")); let upload_url = self.base_url.join("workspace")?; self.inner .post(upload_url) .multipart(reqwest::multipart::Form::new().part("workspace bundle", upload_part)) .send() .await? .error_for_status()?; Ok(()) } pub async fn download_workspace( &self, module_id: Ulid, ) -> ModuleIndexClientResult<WorkspaceExport> { let download_url = self .base_url .join("workspace/")? .join(&format!("{}/", module_id.to_string()))? .join("download")?; let response = self .inner .get(download_url) .send() .await? .error_for_status()?; let bytes = response.bytes().await?; let export_data: WorkspaceExport = serde_json::from_slice(&bytes).map_err(ModuleIndexClientError::Deserialization)?; // Deserialize back into export object Ok(export_data) } /// Lists all of the latest, _promoted_ [`Modules`](Model) (route: GET /modules/latest). pub async fn list_latest_modules(&self) -> ModuleIndexClientResult<ListLatestModulesResponse> { let url = self.base_url.join("modules/")?.join("latest")?; Ok(self .inner .get(url) .send() .await? .error_for_status()? .json() .await?) } // Will skip builtins pub async fn list_module_details(&self) -> ModuleIndexClientResult<ListModulesResponse> { let url = self.base_url.join("modules")?; Ok(self .inner .get(url) .send() .await? .error_for_status()? .json() .await?) } }

Latest Blog Posts

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/systeminit/si'

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