ocr 0.1.0

A minimalist OCR library for Rust — from scratch, no external engine
Documentation
use anyhow::Result;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use rmcp::{
    handler::server::wrapper::Parameters,
    schemars,
    serde,
    service::ServiceExt,
    tool,
    tool_router,
    transport::stdio,
};

#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
struct OcrImageParams {
    #[schemars(description = "Absolute path to the image file")]
    image_path: String,
}

#[derive(Debug, Clone, serde::Deserialize, schemars::JsonSchema)]
struct OcrBase64Params {
    #[schemars(description = "Base64-encoded image data (raw base64 or data URI)")]
    image_base64: String,
}

#[derive(Clone)]
struct OcrServer;

#[tool_router(server_handler)]
impl OcrServer {
    #[tool(description = "Perform OCR on an image file and return the extracted text with confidence. English text only.")]
    fn ocr_image(
        &self,
        Parameters(params): Parameters<OcrImageParams>,
    ) -> String {
        let engine = ocr::OcrEngine::new();
        match engine.recognize_file(std::path::Path::new(&params.image_path)) {
            Ok(result) => format_result(&result),
            Err(e) => format!("Error: {}", e),
        }
    }

    #[tool(description = "Perform OCR on a base64-encoded image and return the extracted text. English text only.")]
    fn ocr_base64(
        &self,
        Parameters(params): Parameters<OcrBase64Params>,
    ) -> String {
        let image_data = match decode_base64(&params.image_base64) {
            Ok(data) => data,
            Err(e) => return format!("Error decoding base64: {}", e),
        };

        let engine = ocr::OcrEngine::new();
        match engine.recognize_bytes(&image_data) {
            Ok(result) => format_result(&result),
            Err(e) => format!("Error: {}", e),
        }
    }
}

fn format_result(result: &ocr::OcrResult) -> String {
    let mut output = result.text.clone();
    if !result.words.is_empty() {
        output.push_str(&format!(
            "\n\n---\nConfidence: {:.1}% | Words: {}",
            result.confidence,
            result.words.len(),
        ));
    }
    output
}

fn decode_base64(input: &str) -> Result<Vec<u8>, String> {
    let cleaned = input
        .trim()
        .strip_prefix("data:image/")
        .and_then(|s| s.split_once(";base64,"))
        .map(|(_, data)| data)
        .unwrap_or(input.trim());
    BASE64.decode(cleaned).map_err(|e| format!("{}", e))
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt()
        .with_writer(std::io::stderr)
        .with_target(false)
        .init();
    tracing::info!("Starting ocr-rs MCP server");
    let service = OcrServer.serve(stdio()).await?;
    service.waiting().await?;
    Ok(())
}