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;