1use serde::{Deserialize, Serialize};
25use serde_json::{json, Value};
26use std::io::{BufRead, BufReader, Write};
27use std::sync::Arc;
28
29use crate::handlers::State;
30
31#[derive(Debug, Deserialize)]
35struct RpcRequest {
36 #[serde(default)]
37 id: Option<Value>,
38 method: String,
39 #[serde(default)]
40 params: Value,
41}
42
43#[derive(Debug, Serialize)]
44struct RpcResponse {
45 jsonrpc: &'static str,
46 id: Value,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 result: Option<Value>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 error: Option<RpcError>,
51}
52
53#[derive(Debug, Serialize)]
54struct RpcError {
55 code: i32,
56 message: String,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 data: Option<Value>,
59}
60
61pub fn serve_mcp(state: Arc<State>) -> std::io::Result<()> {
67 eprintln!("lex MCP server ready (stdio); v1 tools: lex_check, lex_publish, lex_run, lex_stage_get, lex_stage_attestations");
68 let stdin = std::io::stdin();
69 let reader = BufReader::new(stdin.lock());
70 let stdout = std::io::stdout();
71 let mut out = stdout.lock();
72
73 for line in reader.lines() {
74 let line = line?;
75 if line.trim().is_empty() { continue; }
76 match serde_json::from_str::<RpcRequest>(&line) {
77 Ok(req) => {
78 if let Some(resp) = dispatch(&state, req) {
79 let body = serde_json::to_string(&resp).unwrap_or_else(|_| "{}".into());
80 writeln!(out, "{body}")?;
81 out.flush()?;
82 }
83 }
85 Err(e) => {
86 let resp = RpcResponse {
88 jsonrpc: "2.0",
89 id: Value::Null,
90 result: None,
91 error: Some(RpcError {
92 code: -32700,
93 message: format!("parse error: {e}"),
94 data: None,
95 }),
96 };
97 writeln!(out, "{}", serde_json::to_string(&resp).unwrap())?;
98 out.flush()?;
99 }
100 }
101 }
102 Ok(())
103}
104
105fn dispatch(state: &State, req: RpcRequest) -> Option<RpcResponse> {
108 let id = req.id?; let method = req.method.as_str();
110 let result = match method {
111 "initialize" => Ok(json!({
112 "protocolVersion": "2024-11-05",
113 "capabilities": { "tools": { "listChanged": false } },
114 "serverInfo": {
115 "name": "lex",
116 "version": env!("CARGO_PKG_VERSION"),
117 }
118 })),
119 "tools/list" => Ok(json!({ "tools": tool_definitions() })),
120 "tools/call" => call_tool(state, &req.params),
121 "ping" => Ok(json!({})),
123 other => Err(RpcError {
124 code: -32601,
125 message: format!("method not found: {other}"),
126 data: None,
127 }),
128 };
129 Some(match result {
130 Ok(v) => RpcResponse { jsonrpc: "2.0", id, result: Some(v), error: None },
131 Err(e) => RpcResponse { jsonrpc: "2.0", id, result: None, error: Some(e) },
132 })
133}
134
135fn tool_definitions() -> Value {
139 json!([
140 {
141 "name": "lex_check",
142 "description": "Type-check a Lex source string. Returns ok or a list of TypeErrors with structured detail.",
143 "inputSchema": {
144 "type": "object",
145 "properties": { "source": { "type": "string" } },
146 "required": ["source"]
147 }
148 },
149 {
150 "name": "lex_publish",
151 "description": "Publish a Lex source to the store. Type-check gate runs first; rejected sources don't advance the branch head. Returns the typed ops produced.",
152 "inputSchema": {
153 "type": "object",
154 "properties": {
155 "source": { "type": "string" },
156 "activate": { "type": "boolean", "default": false }
157 },
158 "required": ["source"]
159 }
160 },
161 {
162 "name": "lex_run",
163 "description": "Execute a Lex function under an effect policy. Pure programs run with no policy; effectful ones need allow_effects / allow_fs_read / allow_fs_write / allow_net_host grants.",
164 "inputSchema": {
165 "type": "object",
166 "properties": {
167 "source": { "type": "string" },
168 "fn": { "type": "string" },
169 "args": { "type": "array", "items": {} },
170 "policy": {
171 "type": "object",
172 "properties": {
173 "allow_effects": { "type": "array", "items": { "type": "string" } },
174 "allow_fs_read": { "type": "array", "items": { "type": "string" } },
175 "allow_fs_write": { "type": "array", "items": { "type": "string" } },
176 "budget": { "type": "integer" }
177 }
178 }
179 },
180 "required": ["source", "fn"]
181 }
182 },
183 {
184 "name": "lex_stage_get",
185 "description": "Fetch a stage's metadata + canonical AST + status by stage_id (lowercase-hex SHA-256).",
186 "inputSchema": {
187 "type": "object",
188 "properties": { "stage_id": { "type": "string" } },
189 "required": ["stage_id"]
190 }
191 },
192 {
193 "name": "lex_stage_attestations",
194 "description": "List every attestation persisted against a stage (TypeCheck / Spec / Examples / DiffBody / EffectAudit / SandboxRun). Newest-first.",
195 "inputSchema": {
196 "type": "object",
197 "properties": { "stage_id": { "type": "string" } },
198 "required": ["stage_id"]
199 }
200 }
201 ])
202}
203
204fn call_tool(state: &State, params: &Value) -> Result<Value, RpcError> {
210 let name = params.get("name").and_then(|v| v.as_str()).ok_or_else(|| RpcError {
211 code: -32602, message: "tools/call: missing `name`".into(), data: None,
212 })?;
213 let args = params.get("arguments").cloned().unwrap_or_else(|| json!({}));
214
215 let body = serde_json::to_string(&args).unwrap_or_else(|_| "{}".into());
222 let (status, response_body): (u16, String) = match name {
223 "lex_check" => http_to_string(crate::handlers::check_handler(&body)),
224 "lex_publish" => http_to_string(crate::handlers::publish_handler(state, &body)),
225 "lex_run" => http_to_string(crate::handlers::run_handler(state, &body, false)),
226 "lex_stage_get" => {
227 let id = args.get("stage_id").and_then(|v| v.as_str()).ok_or_else(|| RpcError {
228 code: -32602, message: "lex_stage_get: missing stage_id".into(), data: None,
229 })?;
230 http_to_string(crate::handlers::stage_handler(state, id))
231 }
232 "lex_stage_attestations" => {
233 let id = args.get("stage_id").and_then(|v| v.as_str()).ok_or_else(|| RpcError {
234 code: -32602, message: "lex_stage_attestations: missing stage_id".into(), data: None,
235 })?;
236 http_to_string(crate::handlers::stage_attestations_handler(state, id))
237 }
238 other => return Err(RpcError {
239 code: -32602, message: format!("unknown tool: {other}"), data: None,
240 }),
241 };
242
243 let is_error = !(200..300).contains(&status);
244 Ok(json!({
245 "content": [{ "type": "text", "text": response_body }],
246 "isError": is_error,
247 }))
248}
249
250fn http_to_string(
253 resp: tiny_http::Response<std::io::Cursor<Vec<u8>>>,
254) -> (u16, String) {
255 let status = resp.status_code().0;
261 let mut buf = Vec::new();
262 let mut reader = resp.into_reader();
263 let _ = std::io::copy(&mut reader, &mut buf);
264 let body = String::from_utf8(buf).unwrap_or_default();
265 (status, body)
266}