use crate::verbs::VERBS;
use rmcp::model::{
CallToolRequestParams, CallToolResponse, CallToolResult, ContentBlock, Implementation,
ListToolsResult, PaginatedRequestParams, ServerCapabilities, ServerConfig, Tool,
ToolAnnotations,
};
use rmcp::service::RequestContext;
use rmcp::{ErrorData, RoleServer, ServerHandler, ServiceExt};
use std::sync::Arc;
const INSTRUCTIONS: &str =
"Questions about DWG and DXF drawings, one edit, and a picture of it. Every tool \
reads the files it is given, by path on this machine, and answers with a JSON document; \
the same files and arguments give the same answer. A value the drawing does not establish \
is reported as such (absent, ambiguous with every candidate, or not searched with a \
reason), never guessed. `set` changes one field and writes the result as a new model JSON \
file -- never over an existing one, never into its input -- which every tool reads as a \
drawing, so edits chain; its answer is the difference it made. `redline` draws that \
difference on top of the original as an SVG or PNG file (a new one), for a person to see. \
`balloon` draws labels you give, each pointing at an entity or a point, on top of a drawing \
as an SVG file (a new one).";
struct Server;
impl ServerHandler for Server {
fn get_info(&self) -> ServerConfig {
ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
.with_server_info(Implementation::new("uncad", env!("CARGO_PKG_VERSION")))
.with_instructions(INSTRUCTIONS)
}
async fn list_tools(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, ErrorData> {
let tools = VERBS
.iter()
.map(|verb| {
Tool::new(verb.name, verb.description, Arc::new(verb.input_schema()))
.with_annotations(ToolAnnotations::from_raw(
None,
Some(!verb.writes),
Some(false),
Some(!verb.writes),
Some(false),
))
})
.collect();
Ok(ListToolsResult::with_all_items(tools))
}
async fn call_tool(
&self,
request: CallToolRequestParams,
_context: RequestContext<RoleServer>,
) -> Result<CallToolResponse, ErrorData> {
let verb = VERBS
.iter()
.find(|v| v.name == request.name)
.ok_or_else(|| {
ErrorData::invalid_params(format!("no tool '{}'", request.name), None)
})?;
let args = request.arguments.unwrap_or_default();
let result = match verb.call(&args) {
Ok(answer) => {
let mut content = vec![ContentBlock::text(answer.json)];
content.extend(
answer
.warnings
.into_iter()
.map(|w| ContentBlock::text(format!("warning: {w}"))),
);
CallToolResult::success(content)
}
Err(message) => CallToolResult::error(vec![ContentBlock::text(message)]),
};
Ok(result.into())
}
}
const HELD_DRAWINGS: usize = 4;
pub fn serve() -> Result<(), String> {
crate::verbs::hold_reads(HELD_DRAWINGS);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("cannot start the MCP server: {e}"))?;
runtime.block_on(async {
let service = Server
.serve(rmcp::transport::stdio())
.await
.map_err(|e| format!("MCP initialization failed: {e}"))?;
service
.waiting()
.await
.map_err(|e| format!("MCP server stopped: {e}"))?;
Ok(())
})
}