1mod base64;
21mod doc_ref;
22mod exec;
23#[cfg(feature = "http")]
24mod http;
25mod protocol;
26mod resources;
27mod serialize;
28mod tools;
29
30use std::io::{self, BufRead, Write};
31
32use serde_json::{Value, json};
33
34use protocol::{error, success};
35
36const DEFAULT_PROTOCOL: &str = "2025-06-18";
39
40const INSTRUCTIONS: &str = "Zenith authors, validates, and renders deterministic .zen design \
42documents. If your environment can run the local `zenith` CLI, prefer it (install the binary and \
43the skill, then call commands directly) — this MCP server is for environments where a local binary \
44is not suitable (remote, CI, sandboxed, hosted agents). It is a first-class surface: results are \
45trimmed, schema detail is on demand, and large artifacts come back as resource links. Address a \
46document by its path or its doc-id (returned once identity is attached). Typical loop: zenith_schema \
47(learn node/op shapes on demand) → zenith_tx to edit → zenith_validate (hard Error diagnostics block \
48rendering) → zenith_render (returns a resource link; read it via resources/read). Keep design \
49iterations as scratch candidates and promote the chosen one into the export page rather than editing \
50the deliverable directly. Results are trimmed by default; opt into detail with the documented params.";
51
52pub fn run() -> u8 {
54 let stdin = io::stdin();
55 let mut out = io::stdout();
56 let reader = stdin.lock();
57
58 for line in reader.lines() {
59 let line = match line {
60 Ok(l) => l,
61 Err(e) => {
62 eprintln!("zenith mcp: stdin read error: {e}");
63 break;
64 }
65 };
66 if line.trim().is_empty() {
67 continue;
68 }
69 if let Some(response) = handle_message(&line) {
70 if writeln!(out, "{response}").is_err() {
71 break;
72 }
73 let _ = out.flush();
74 }
75 }
76 0
77}
78
79pub fn handle_message(line: &str) -> Option<Value> {
84 let msg: Value = match serde_json::from_str(line) {
85 Ok(v) => v,
86 Err(e) => return Some(error(Value::Null, -32700, &format!("parse error: {e}"))),
87 };
88
89 let id = msg.get("id").cloned();
90 let method = msg.get("method").and_then(Value::as_str).unwrap_or("");
91 let params = msg.get("params").cloned().unwrap_or(Value::Null);
92
93 let id = id?;
95
96 match method {
97 "initialize" => Some(success(id, initialize_result(¶ms))),
98 "ping" => Some(success(id, json!({}))),
99 "tools/list" => Some(success(id, tools::list_payload())),
100 "tools/call" => Some(tools_call(id, ¶ms)),
101 "resources/list" => Some(success(id, resources::list_payload())),
102 "resources/read" => Some(resources_read(id, ¶ms)),
103 other => Some(error(id, -32601, &format!("method not found: {other}"))),
104 }
105}
106
107fn initialize_result(params: &Value) -> Value {
109 let protocol = params
110 .get("protocolVersion")
111 .and_then(Value::as_str)
112 .unwrap_or(DEFAULT_PROTOCOL);
113 json!({
114 "protocolVersion": protocol,
115 "capabilities": {
116 "tools": {},
117 "resources": { "listChanged": false },
118 },
119 "serverInfo": { "name": "zenith", "version": env!("CARGO_PKG_VERSION") },
120 "instructions": INSTRUCTIONS,
121 })
122}
123
124fn tools_call(id: Value, params: &Value) -> Value {
128 let Some(name) = params.get("name").and_then(Value::as_str) else {
129 return error(id, -32602, "missing tool name");
130 };
131 let args = params.get("arguments").cloned().unwrap_or(json!({}));
132 success(id, exec::call(name, &args).into_payload())
133}
134
135#[cfg(feature = "http")]
138pub fn run_http(addr: &str) -> u8 {
139 http::serve(addr)
140}
141
142#[cfg(not(feature = "http"))]
144pub fn run_http(_addr: &str) -> u8 {
145 eprintln!(
146 "zenith mcp: this binary was built without the `http` feature; rebuild with \
147 `--features http` to use --http, or use the default stdio transport."
148 );
149 1
150}
151
152fn resources_read(id: Value, params: &Value) -> Value {
154 let Some(uri) = params.get("uri").and_then(Value::as_str) else {
155 return error(id, -32602, "missing resource uri");
156 };
157 match resources::read_payload(uri) {
158 Ok(result) => success(id, result),
159 Err(message) => error(id, -32002, &message),
160 }
161}