gy 0.1.0

good,yes — Decision, question and need graph ledger CLI with MCP
use crate::{Cli, Commands, Error, Result, execute};
use clap::Parser;
use serde_json::{Value, json};
use std::io::{self, BufRead, Write};

const TOOL_NAMES: &[(&str, &str, &[&str])] = &[
    (
        "gy_find",
        "Search sections and arbitrary attributes across all scopes",
        &["find"],
    ),
    (
        "gy_show",
        "Show a node and its surrounding relationships",
        &["show"],
    ),
    ("gy_lint", "Check internal ledger consistency", &["lint"]),
    (
        "gy_next",
        "List needs whose prerequisites are resolved",
        &["next"],
    ),
    (
        "gy_stats",
        "Report acceptance criteria and question arrival rates",
        &["stats"],
    ),
    (
        "gy_handover",
        "List records needed for a handover",
        &["handover"],
    ),
    ("gy_need", "Create needs and record their filing", &["need"]),
    ("gy_question", "Create and close questions", &["question"]),
    (
        "gy_decide",
        "Record a decision with applicability conditions",
        &["decide"],
    ),
    ("gy_link", "Update relationships on both sides", &["link"]),
    (
        "gy_req",
        "Register requirements, advance states, and compress records",
        &["req"],
    ),
    (
        "gy_criterion",
        "Register and satisfy acceptance criteria",
        &["criterion"],
    ),
    ("gy_gate", "Register continuation gates", &["gate"]),
    (
        "gy_node",
        "Edit arbitrary attributes and body text",
        &["node"],
    ),
    ("gy_render", "Generate Markdown or DOT", &["render"]),
    (
        "gy_import",
        "Import existing ADRs while preserving IDs",
        &["import"],
    ),
    ("gy_init", "Create a ledger and scope", &["init"]),
    (
        "gy_cheatsheet",
        "Usage instructions for the first session",
        &["cheatsheet"],
    ),
];
pub fn serve(parent: &Cli) -> Result<()> {
    let input = io::stdin();
    let mut output = io::stdout().lock();
    for line in input.lock().lines() {
        let line = line?;
        if line.trim().is_empty() {
            continue;
        }
        let request: Value = match serde_json::from_str(&line) {
            Ok(v) => v,
            Err(_) => {
                writeln!(
                    output,
                    "{}",
                    json!({"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}})
                )?;
                output.flush()?;
                continue;
            }
        };
        let Some(id) = request.get("id") else {
            continue;
        };
        let response = handle(&request, parent);
        let response = match response {
            Ok(result) => json!({"jsonrpc":"2.0","id":id,"result":result}),
            Err((code, message)) => {
                json!({"jsonrpc":"2.0","id":id,"error":{"code":code,"message":message}})
            }
        };
        writeln!(output, "{response}")?;
        output.flush()?;
    }
    Ok(())
}
fn handle(request: &Value, parent: &Cli) -> std::result::Result<Value, (i32, String)> {
    if request["jsonrpc"] != "2.0" {
        return Err((-32600, "JSON-RPC 2.0 required".into()));
    }
    match request["method"].as_str().unwrap_or("") {
        "initialize" => {
            let requested = request["params"]["protocolVersion"].as_str().unwrap_or("");
            let version =
                if ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"].contains(&requested) {
                    requested
                } else {
                    "2025-11-25"
                };
            Ok(
                json!({"protocolVersion":version,"capabilities":{"tools":{"listChanged":false}},"serverInfo":{"name":"gy","version":env!("CARGO_PKG_VERSION")},"instructions":"Call gy_cheatsheet first. args is an array of arguments after the corresponding CLI subcommand, not a shell string. Verify separately that the ledger matches reality."}),
            )
        }
        "ping" => Ok(json!({})),
        "tools/list" => Ok(
            json!({"tools":TOOL_NAMES.iter().map(|(name,description,_)|json!({"name":name,"description":description,"inputSchema":{"type":"object","properties":{"args":{"type":"array","items":{"type":"string"},"description":"CLI arguments. Example for question: [\"add\",\"Question\",\"--decider\",\"master\",\"--options\",\"A\",\"--options\",\"B\",\"--scope\",\"demo\"]"}},"additionalProperties":false}})).collect::<Vec<_>>()}),
        ),
        "tools/call" => {
            let name = request["params"]["name"].as_str().unwrap_or("");
            let (_, _, prefix) = TOOL_NAMES
                .iter()
                .find(|t| t.0 == name)
                .ok_or_else(|| (-32602, format!("Unknown tool {name}")))?;
            let arguments = &request["params"]["arguments"];
            if !arguments.is_null() && !arguments.is_object() {
                return Err((-32602, "arguments must be an object".into()));
            }
            if arguments
                .as_object()
                .is_some_and(|m| m.keys().any(|k| k != "args"))
            {
                return Err((-32602, "Only args is accepted".into()));
            }
            let mut args = vec!["gy".to_string(), "--json".to_string()];
            if let Some(cwd) = &parent.cwd {
                args.extend(["--cwd".into(), cwd.to_string_lossy().into()]);
            }
            if let Some(scope) = &parent.scope {
                args.extend(["--scope".into(), scope.clone()]);
            }
            args.extend(prefix.iter().map(|s| s.to_string()));
            if let Some(a) = arguments.get("args") {
                let a = a
                    .as_array()
                    .ok_or_else(|| (-32602, "args must be a string array".into()))?;
                for arg in a {
                    args.push(
                        arg.as_str()
                            .ok_or_else(|| (-32602, "args must be a string array".into()))?
                            .into(),
                    );
                }
            }
            let result = match Cli::try_parse_from(args) {
                Ok(cli) if !matches!(cli.command, Commands::Mcp { .. } | Commands::Q { .. }) => {
                    execute(&cli)
                }
                Ok(_) => Err(Error::input("This command is not exposed through MCP")),
                Err(e) => {
                    let error = e.use_stderr();
                    let text = e.to_string();
                    return Ok(json!({"content":[{"type":"text","text":text}],"isError":error}));
                }
            };
            Ok(match result {
                Ok(out) => {
                    let structured =
                        json!({"result":out.value,"warnings":out.warnings,"code":out.code});
                    json!({"content":[{"type":"text","text":structured.to_string()}],"structuredContent":structured,"isError":out.code!=0})
                }
                Err(e) => {
                    json!({"content":[{"type":"text","text":serde_json::to_string(&e).unwrap()}],"isError":true})
                }
            })
        }
        _ => Err((-32601, "Method not found".into())),
    }
}