Skip to main content

chromaframe_mcp/
lib.rs

1#![forbid(unsafe_code)]
2
3mod error;
4mod runtime;
5mod schema;
6
7use rmcp::{
8    ServerHandler, ServiceExt,
9    handler::server::{router::tool::ToolRouter, wrapper::Json, wrapper::Parameters},
10    model::{ServerCapabilities, ServerInfo},
11    tool, tool_handler, tool_router,
12};
13pub use schema::{AnalyzeImageOutput, CandidateRankingSummary, ManualRankOutput, ReadinessOutput};
14
15#[derive(Debug, Clone)]
16pub struct ChromaFrameMcpServer {
17    runtime: runtime::ChromaFrameRuntime,
18    tool_router: ToolRouter<Self>,
19}
20
21impl ChromaFrameMcpServer {
22    #[must_use]
23    pub fn from_env() -> Self {
24        Self::from_runtime(runtime::ChromaFrameRuntime::from_env())
25    }
26
27    #[must_use]
28    fn from_runtime(runtime: runtime::ChromaFrameRuntime) -> Self {
29        Self {
30            runtime,
31            tool_router: Self::tool_router(),
32        }
33    }
34
35    pub async fn serve_stdio(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
36        let running = self.serve(rmcp::transport::stdio()).await?;
37        let _ = running.waiting().await?;
38        Ok(())
39    }
40}
41
42#[tool_router]
43impl ChromaFrameMcpServer {
44    #[tool(
45        name = "chromaframe_readiness",
46        description = "Report sanitized ChromaFrame SDK and local vision-helper readiness.",
47        annotations(read_only_hint = true, idempotent_hint = true)
48    )]
49    async fn chromaframe_readiness(
50        &self,
51        Parameters(input): Parameters<schema::ReadinessInput>,
52    ) -> Result<Json<schema::ReadinessOutput>, String> {
53        self.runtime
54            .readiness(&input)
55            .map(Json)
56            .map_err(|error| error.message().to_string())
57    }
58
59    #[tool(
60        name = "chromaframe_rank_candidates",
61        description = "Rank candidate colors from manual Lab subject measurements with deterministic SDK scoring.",
62        annotations(read_only_hint = true, idempotent_hint = true)
63    )]
64    async fn chromaframe_rank_candidates(
65        &self,
66        Parameters(input): Parameters<schema::ManualRankInput>,
67    ) -> Result<Json<schema::ManualRankOutput>, String> {
68        self.runtime
69            .rank_candidates(&input)
70            .map(Json)
71            .map_err(|error| error.message().to_string())
72    }
73
74    #[tool(
75        name = "chromaframe_analyze_image",
76        description = "Analyze a local image with the vision helper and return sanitized deterministic rankings.",
77        annotations(read_only_hint = true, idempotent_hint = true)
78    )]
79    async fn chromaframe_analyze_image(
80        &self,
81        Parameters(input): Parameters<schema::AnalyzeImageInput>,
82    ) -> Result<Json<schema::AnalyzeImageOutput>, String> {
83        self.runtime
84            .analyze_image(&input)
85            .await
86            .map(Json)
87            .map_err(|error| error.message().to_string())
88    }
89}
90
91#[tool_handler(router = self.tool_router)]
92impl ServerHandler for ChromaFrameMcpServer {
93    fn get_info(&self) -> ServerInfo {
94        ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
95    }
96}
97
98#[cfg(test)]
99mod tests;