mod context;
mod explain_file;
mod jsonrpc;
mod review_change;
mod scan;
#[cfg(test)]
mod tests;
use crate::cli::McpOptions;
use jsonrpc::{METHOD_NOT_FOUND, Request, Response};
use serde_json::{Value, json};
use std::io::{BufRead, Write};
const SERVER_NAME: &str = "repopilot";
const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_PROTOCOL_VERSION: &str = "2024-11-05";
pub fn run(_options: McpOptions) -> Result<(), Box<dyn std::error::Error>> {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
serve(stdin.lock(), stdout.lock())?;
Ok(())
}
pub fn serve<R: BufRead, W: Write>(reader: R, mut writer: W) -> std::io::Result<()> {
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let Ok(request) = serde_json::from_str::<Request>(&line) else {
continue;
};
if let Some(response) = handle(&request) {
let encoded = serde_json::to_string(&response)?;
writer.write_all(encoded.as_bytes())?;
writer.write_all(b"\n")?;
writer.flush()?;
}
}
Ok(())
}
fn handle(request: &Request) -> Option<Response> {
let id = request.id.clone()?;
let response = match request.method.as_str() {
"initialize" => Response::success(id, initialize_result(&request.params)),
"ping" => Response::success(id, json!({})),
"tools/list" => Response::success(id, tools_list_result()),
"tools/call" => handle_tools_call(id, &request.params),
other => Response::error(id, METHOD_NOT_FOUND, format!("method not found: {other}")),
};
Some(response)
}
fn initialize_result(params: &Value) -> Value {
let protocol_version = params
.get("protocolVersion")
.and_then(Value::as_str)
.unwrap_or(DEFAULT_PROTOCOL_VERSION);
json!({
"protocolVersion": protocol_version,
"capabilities": { "tools": {} },
"serverInfo": { "name": SERVER_NAME, "version": SERVER_VERSION }
})
}
fn tools_list_result() -> Value {
json!({
"tools": [
review_change::definition(),
scan::definition(),
context::definition(),
explain_file::definition(),
]
})
}
fn handle_tools_call(id: Value, params: &Value) -> Response {
let name = params
.get("name")
.and_then(Value::as_str)
.unwrap_or_default();
let arguments = params
.get("arguments")
.cloned()
.unwrap_or_else(|| json!({}));
let outcome = match name {
review_change::TOOL_NAME => review_change::call(&arguments),
scan::TOOL_NAME => scan::call(&arguments),
context::TOOL_NAME => context::call(&arguments),
explain_file::TOOL_NAME => explain_file::call(&arguments),
other => Err(format!("unknown tool: {other}")),
};
Response::success(id, tool_result(outcome))
}
fn tool_result(outcome: Result<String, String>) -> Value {
match outcome {
Ok(text) => json!({ "content": [{ "type": "text", "text": text }], "isError": false }),
Err(message) => {
json!({ "content": [{ "type": "text", "text": message }], "isError": true })
}
}
}