use std::io::{BufRead, Write};
use std::path::{Path, PathBuf};
use anyhow::Result;
use serde_json::{Value, json};
enum Scope {
Repo {
repo: PathBuf,
freshness: crate::freshness::RepoFreshness,
},
Workspace(PathBuf),
}
pub fn run(repo: &Path) -> Result<()> {
let repo = repo.canonicalize()?;
let freshness = crate::freshness::RepoFreshness::new(&repo)?;
serve(Scope::Repo { repo, freshness })
}
pub fn run_workspace(manifest: &Path) -> Result<()> {
serve(Scope::Workspace(manifest.canonicalize()?))
}
fn serve(scope: Scope) -> Result<()> {
let stdin = std::io::stdin();
let mut stdout = std::io::stdout().lock();
for line in stdin.lock().lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let Ok(request): Result<Value, _> = serde_json::from_str(&line) else {
writeln!(
stdout,
"{}",
json!({"jsonrpc": "2.0", "id": Value::Null,
"error": {"code": -32700, "message": "parse error"}})
)?;
stdout.flush()?;
continue;
};
let id = request.get("id").cloned();
let method = request.get("method").and_then(Value::as_str).unwrap_or("");
let params = request.get("params").cloned().unwrap_or(Value::Null);
let Some(id) = id else {
continue;
};
let response = match handle(&scope, method, ¶ms) {
Ok(result) => json!({"jsonrpc": "2.0", "id": id, "result": result}),
Err(error) => error_response(id, method, ¶ms, &error),
};
writeln!(stdout, "{response}")?;
stdout.flush()?;
}
Ok(())
}
fn error_response(id: Value, method: &str, params: &Value, error: &anyhow::Error) -> Value {
let mut message = format!("{error:#}");
if message.contains("no symbol matches") {
if let Some(position) = message.find(" — try `sinter ask") {
message.truncate(position);
}
message.push_str(" — try the ask tool for concept search");
}
let operation = if method == "tools/call" {
params
.get("name")
.and_then(Value::as_str)
.unwrap_or("unknown")
} else {
method
};
let mut data = crate::agent_protocol::failure(operation, error);
data["error"]["message"] = json!(message);
json!({
"jsonrpc": "2.0",
"id": id,
"error": {"code": -32000, "message": message, "data": data}
})
}
fn handle(scope: &Scope, method: &str, params: &Value) -> Result<Value> {
match method {
"initialize" => Ok(json!({
"protocolVersion": params
.get("protocolVersion")
.and_then(Value::as_str)
.unwrap_or("2024-11-05"),
"capabilities": {"tools": {}, "resources": {}},
"serverInfo": {"name": "sinter", "version": env!("CARGO_PKG_VERSION")},
"instructions": format!(
"sinter answers code-structure questions from a dependency graph. \
Read `structuredContent` (content text is a summary only) and check \
`outcome.status` before acting. Key legend, coverage semantics, \
batching, and budget paging: see resource {}",
crate::tool_catalog::GUIDE_URI
),
})),
"ping" => Ok(json!({})),
"resources/list" => Ok(json!({"resources": [{
"uri": crate::tool_catalog::GUIDE_URI,
"name": "guide",
"description": "How to read sinter results: key legend, coverage, batching, paging",
"mimeType": "text/markdown",
}]})),
"resources/templates/list" => Ok(json!({"resourceTemplates": []})),
"resources/read" => {
let uri = params.get("uri").and_then(Value::as_str).unwrap_or("");
if uri != crate::tool_catalog::GUIDE_URI {
anyhow::bail!("unknown resource {uri}");
}
Ok(json!({"contents": [{
"uri": uri,
"mimeType": "text/markdown",
"text": crate::tool_catalog::GUIDE,
}]}))
}
"tools/list" => Ok(match scope {
Scope::Repo { .. } => crate::tool_catalog::repository(),
Scope::Workspace(_) => crate::tool_catalog::workspace(),
}),
"tools/call" => call_tool(scope, params),
other => anyhow::bail!("unknown method {other}"),
}
}
fn call_tool(scope: &Scope, params: &Value) -> Result<Value> {
let name = params.get("name").and_then(Value::as_str).unwrap_or("");
let mut args = params
.get("arguments")
.cloned()
.unwrap_or_else(|| json!({}));
let budget = crate::agent_protocol::take_budget(&mut args)?;
crate::agent_protocol::validate_arguments(name, &args, matches!(scope, Scope::Workspace(_)))?;
let result = match scope {
Scope::Repo { repo, freshness } => {
freshness.sync()?;
crate::repository_tools::call(repo, name, &args)?
}
Scope::Workspace(manifest) => crate::workspace_tools::call(manifest, name, &args)?,
};
crate::agent_protocol::mcp_success(name, &result, budget)
}