Skip to main content
Glama
nizovtsevnv

Outline knowledge base MCP Server

by nizovtsevnv
outline.rs5.95 kB
//! Outline API client //! //! Simple HTTP client for Outline Knowledge Base API. use reqwest::{header, Client as HttpClient}; use serde_json::Value; use tracing::debug; use url::Url; use crate::config::ApiKey; use crate::error::{Error, Result}; /// Outline API client #[derive(Debug, Clone)] pub struct Client { /// HTTP client instance http: HttpClient, /// API authentication key api_key: ApiKey, /// Base API URL base_url: Url, } impl Client { /// Create new Outline API client pub fn new(api_key: ApiKey, base_url: Url) -> Result<Self> { let http_client = HttpClient::builder() .user_agent(format!( "{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION") )) .timeout(std::time::Duration::from_secs(30)) .build()?; Ok(Self { http: http_client, api_key, base_url, }) } /// Execute POST request to Outline API pub async fn post(&self, endpoint: &str, body: Value) -> Result<Value> { // Ensure base_url ends with a slash for proper joining let mut url_string = self.base_url.to_string(); if !url_string.ends_with('/') { url_string.push('/'); } url_string.push_str(endpoint); let url = url_string.parse::<url::Url>().map_err(|e| Error::Config { message: format!("Invalid URL: {url_string}"), source: Some(Box::new(e)), })?; debug!("📤 POST request: {} | Body: {}", url, body); let response = self .http .post(url.clone()) .header( header::AUTHORIZATION, format!("Bearer {}", self.api_key.as_str()), ) .header(header::CONTENT_TYPE, "application/json") .json(&body) .send() .await?; self.handle_response(response).await } /// Execute GET request to Outline API #[allow(dead_code)] pub async fn get(&self, endpoint: &str) -> Result<Value> { // Ensure base_url ends with a slash for proper joining let mut url_string = self.base_url.to_string(); if !url_string.ends_with('/') { url_string.push('/'); } url_string.push_str(endpoint); let url = url_string.parse::<url::Url>().map_err(|e| Error::Config { message: format!("Invalid URL: {url_string}"), source: Some(Box::new(e)), })?; debug!("📥 GET request: {}", url); let response = self .http .get(url.clone()) .header( header::AUTHORIZATION, format!("Bearer {}", self.api_key.as_str()), ) .send() .await?; self.handle_response(response).await } /// Handle HTTP response and parse JSON async fn handle_response(&self, response: reqwest::Response) -> Result<Value> { let status = response.status(); let status_code = status.as_u16(); if status.is_success() { let text = response.text().await?; debug!("✅ Response: {}", text); serde_json::from_str(&text).map_err(|e| Error::Json { context: format!("Failed to parse API response: {text}"), source: e, }) } else { let error_text = response.text().await.unwrap_or_default(); debug!("❌ Error response: {} - {}", status_code, error_text); Err(Error::Api { status: status_code, message: format!("API request failed with status {status_code}"), body: Some(error_text), }) } } } // Helper functions for API operations /// Create document request body pub fn create_document_request(title: &str, text: &str, collection_id: Option<&str>) -> Value { let mut request = serde_json::json!({ "title": title, "text": text }); if let Some(cid) = collection_id { request["collectionId"] = serde_json::json!(cid); } request } /// Update document request body pub fn update_document_request(id: &str, title: Option<&str>, text: Option<&str>) -> Value { let mut request = serde_json::json!({ "id": id }); if let Some(t) = title { request["title"] = serde_json::json!(t); } if let Some(txt) = text { request["text"] = serde_json::json!(txt); } request } /// Search documents request body pub fn search_documents_request(query: &str, limit: Option<u32>) -> Value { let mut request = serde_json::json!({ "query": query }); if let Some(l) = limit { request["limit"] = serde_json::json!(l); } request } /// Create collection request body pub fn create_collection_request(name: &str, description: Option<&str>) -> Value { let mut request = serde_json::json!({ "name": name }); if let Some(desc) = description { request["description"] = serde_json::json!(desc); } request } /// Create comment request body pub fn create_comment_request(document_id: &str, data: &str) -> Value { serde_json::json!({ "documentId": document_id, "data": data }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_create_document_request() { let request = create_document_request("Test Title", "Test content", Some("collection-id")); assert_eq!(request["title"], "Test Title"); assert_eq!(request["text"], "Test content"); assert_eq!(request["collectionId"], "collection-id"); } #[test] fn test_search_documents_request() { let request = search_documents_request("test query", Some(10)); assert_eq!(request["query"], "test query"); assert_eq!(request["limit"], 10); } }

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/nizovtsevnv/outline-mcp-rs'

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