chromaframe-mcp 0.1.1

MCP stdio server for chromaframe-sdk
Documentation
#![forbid(unsafe_code)]

mod error;
mod runtime;
mod schema;

use rmcp::{
    ServerHandler, ServiceExt,
    handler::server::{router::tool::ToolRouter, wrapper::Json, wrapper::Parameters},
    model::{ServerCapabilities, ServerInfo},
    tool, tool_handler, tool_router,
};
pub use schema::{AnalyzeImageOutput, CandidateRankingSummary, ManualRankOutput, ReadinessOutput};

#[derive(Debug, Clone)]
pub struct ChromaFrameMcpServer {
    runtime: runtime::ChromaFrameRuntime,
    tool_router: ToolRouter<Self>,
}

impl ChromaFrameMcpServer {
    #[must_use]
    pub fn from_env() -> Self {
        Self::from_runtime(runtime::ChromaFrameRuntime::from_env())
    }

    #[must_use]
    fn from_runtime(runtime: runtime::ChromaFrameRuntime) -> Self {
        Self {
            runtime,
            tool_router: Self::tool_router(),
        }
    }

    pub async fn serve_stdio(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let running = self.serve(rmcp::transport::stdio()).await?;
        let _ = running.waiting().await?;
        Ok(())
    }
}

#[tool_router]
impl ChromaFrameMcpServer {
    #[tool(
        name = "chromaframe_readiness",
        description = "Report sanitized ChromaFrame SDK and local vision-helper readiness.",
        annotations(read_only_hint = true, idempotent_hint = true)
    )]
    async fn chromaframe_readiness(
        &self,
        Parameters(input): Parameters<schema::ReadinessInput>,
    ) -> Result<Json<schema::ReadinessOutput>, String> {
        self.runtime
            .readiness(&input)
            .map(Json)
            .map_err(|error| error.message().to_string())
    }

    #[tool(
        name = "chromaframe_rank_candidates",
        description = "Rank candidate colors from manual Lab subject measurements with deterministic SDK scoring.",
        annotations(read_only_hint = true, idempotent_hint = true)
    )]
    async fn chromaframe_rank_candidates(
        &self,
        Parameters(input): Parameters<schema::ManualRankInput>,
    ) -> Result<Json<schema::ManualRankOutput>, String> {
        self.runtime
            .rank_candidates(&input)
            .map(Json)
            .map_err(|error| error.message().to_string())
    }

    #[tool(
        name = "chromaframe_analyze_image",
        description = "Analyze a local image with the vision helper and return sanitized deterministic rankings.",
        annotations(read_only_hint = true, idempotent_hint = true)
    )]
    async fn chromaframe_analyze_image(
        &self,
        Parameters(input): Parameters<schema::AnalyzeImageInput>,
    ) -> Result<Json<schema::AnalyzeImageOutput>, String> {
        self.runtime
            .analyze_image(&input)
            .await
            .map(Json)
            .map_err(|error| error.message().to_string())
    }
}

#[tool_handler(router = self.tool_router)]
impl ServerHandler for ChromaFrameMcpServer {
    fn get_info(&self) -> ServerInfo {
        ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
    }
}

#[cfg(test)]
mod tests;