use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::io::{BufRead, BufReader, Write};
use std::sync::Arc;
use crate::handlers::State;
#[derive(Debug, Deserialize)]
struct RpcRequest {
#[serde(default)]
id: Option<Value>,
method: String,
#[serde(default)]
params: Value,
}
#[derive(Debug, Serialize)]
struct RpcResponse {
jsonrpc: &'static str,
id: Value,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<RpcError>,
}
#[derive(Debug, Serialize)]
struct RpcError {
code: i32,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Value>,
}
pub fn serve_mcp(state: Arc<State>) -> std::io::Result<()> {
eprintln!("lex MCP server ready (stdio); v1 tools: lex_check, lex_publish, lex_run, lex_stage_get, lex_stage_attestations");
let stdin = std::io::stdin();
let reader = BufReader::new(stdin.lock());
let stdout = std::io::stdout();
let mut out = stdout.lock();
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() { continue; }
match serde_json::from_str::<RpcRequest>(&line) {
Ok(req) => {
if let Some(resp) = dispatch(&state, req) {
let body = serde_json::to_string(&resp).unwrap_or_else(|_| "{}".into());
writeln!(out, "{body}")?;
out.flush()?;
}
}
Err(e) => {
let resp = RpcResponse {
jsonrpc: "2.0",
id: Value::Null,
result: None,
error: Some(RpcError {
code: -32700,
message: format!("parse error: {e}"),
data: None,
}),
};
writeln!(out, "{}", serde_json::to_string(&resp).unwrap())?;
out.flush()?;
}
}
}
Ok(())
}
fn dispatch(state: &State, req: RpcRequest) -> Option<RpcResponse> {
let id = req.id?; let method = req.method.as_str();
let result = match method {
"initialize" => Ok(json!({
"protocolVersion": "2024-11-05",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": {
"name": "lex",
"version": env!("CARGO_PKG_VERSION"),
}
})),
"tools/list" => Ok(json!({ "tools": tool_definitions() })),
"tools/call" => call_tool(state, &req.params),
"ping" => Ok(json!({})),
other => Err(RpcError {
code: -32601,
message: format!("method not found: {other}"),
data: None,
}),
};
Some(match result {
Ok(v) => RpcResponse { jsonrpc: "2.0", id, result: Some(v), error: None },
Err(e) => RpcResponse { jsonrpc: "2.0", id, result: None, error: Some(e) },
})
}
fn tool_definitions() -> Value {
json!([
{
"name": "lex_check",
"description": "Type-check a Lex source string. Returns ok or a list of TypeErrors with structured detail.",
"inputSchema": {
"type": "object",
"properties": { "source": { "type": "string" } },
"required": ["source"]
}
},
{
"name": "lex_publish",
"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.",
"inputSchema": {
"type": "object",
"properties": {
"source": { "type": "string" },
"activate": { "type": "boolean", "default": false }
},
"required": ["source"]
}
},
{
"name": "lex_run",
"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.",
"inputSchema": {
"type": "object",
"properties": {
"source": { "type": "string" },
"fn": { "type": "string" },
"args": { "type": "array", "items": {} },
"policy": {
"type": "object",
"properties": {
"allow_effects": { "type": "array", "items": { "type": "string" } },
"allow_fs_read": { "type": "array", "items": { "type": "string" } },
"allow_fs_write": { "type": "array", "items": { "type": "string" } },
"budget": { "type": "integer" }
}
}
},
"required": ["source", "fn"]
}
},
{
"name": "lex_stage_get",
"description": "Fetch a stage's metadata + canonical AST + status by stage_id (lowercase-hex SHA-256).",
"inputSchema": {
"type": "object",
"properties": { "stage_id": { "type": "string" } },
"required": ["stage_id"]
}
},
{
"name": "lex_stage_attestations",
"description": "List every attestation persisted against a stage (TypeCheck / Spec / Examples / DiffBody / EffectAudit / SandboxRun). Newest-first.",
"inputSchema": {
"type": "object",
"properties": { "stage_id": { "type": "string" } },
"required": ["stage_id"]
}
}
])
}
fn call_tool(state: &State, params: &Value) -> Result<Value, RpcError> {
let name = params.get("name").and_then(|v| v.as_str()).ok_or_else(|| RpcError {
code: -32602, message: "tools/call: missing `name`".into(), data: None,
})?;
let args = params.get("arguments").cloned().unwrap_or_else(|| json!({}));
let body = serde_json::to_string(&args).unwrap_or_else(|_| "{}".into());
let (status, response_body): (u16, String) = match name {
"lex_check" => http_to_string(crate::handlers::check_handler(&body)),
"lex_publish" => http_to_string(crate::handlers::publish_handler(state, &body)),
"lex_run" => http_to_string(crate::handlers::run_handler(state, &body, false)),
"lex_stage_get" => {
let id = args.get("stage_id").and_then(|v| v.as_str()).ok_or_else(|| RpcError {
code: -32602, message: "lex_stage_get: missing stage_id".into(), data: None,
})?;
http_to_string(crate::handlers::stage_handler(state, id))
}
"lex_stage_attestations" => {
let id = args.get("stage_id").and_then(|v| v.as_str()).ok_or_else(|| RpcError {
code: -32602, message: "lex_stage_attestations: missing stage_id".into(), data: None,
})?;
http_to_string(crate::handlers::stage_attestations_handler(state, id))
}
other => return Err(RpcError {
code: -32602, message: format!("unknown tool: {other}"), data: None,
}),
};
let is_error = !(200..300).contains(&status);
Ok(json!({
"content": [{ "type": "text", "text": response_body }],
"isError": is_error,
}))
}
fn http_to_string(
resp: tiny_http::Response<std::io::Cursor<Vec<u8>>>,
) -> (u16, String) {
let status = resp.status_code().0;
let mut buf = Vec::new();
let mut reader = resp.into_reader();
let _ = std::io::copy(&mut reader, &mut buf);
let body = String::from_utf8(buf).unwrap_or_default();
(status, body)
}