mod tools;
use std::sync::Arc;
use anyhow::{Context as _, Result};
use serde_json::{json, Value};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Stdout};
use tokio::sync::Mutex;
use crate::commands::Cli;
const MCP_VERSION: &str = "2024-11-05";
const SERVER_NAME: &str = "vibesurfer";
const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn run() -> Result<()> {
init_tracing();
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.context("build tokio runtime for vs mcp")?;
rt.block_on(serve())
}
async fn serve() -> Result<()> {
let stdin = tokio::io::stdin();
let stdout = Arc::new(Mutex::new(tokio::io::stdout()));
let mut reader = BufReader::new(stdin).lines();
while let Some(line) = reader.next_line().await? {
let line = line.trim();
if line.is_empty() {
continue;
}
let req: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(e) => {
tracing::warn!("invalid json on stdin: {e}; raw: {line}");
continue;
}
};
let resp = handle_request(&req).await;
if let Some(r) = resp {
write_message(&stdout, r).await;
}
}
Ok(())
}
fn init_tracing() {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("vs_cli=info,info")),
)
.with_writer(std::io::stderr) .try_init();
}
async fn write_message(stdout: &Mutex<Stdout>, msg: Value) {
let bytes = match serde_json::to_vec(&msg) {
Ok(b) => b,
Err(e) => {
tracing::error!("serialize response: {e}");
return;
}
};
let mut out = stdout.lock().await;
let _ = out.write_all(&bytes).await;
let _ = out.write_all(b"\n").await;
let _ = out.flush().await;
}
async fn handle_request(req: &Value) -> Option<Value> {
let id = req.get("id").cloned();
let method = req.get("method").and_then(Value::as_str).unwrap_or("");
let params = req.get("params").cloned().unwrap_or(Value::Null);
let is_notification = id.is_none();
let result: Result<Value, McpError> = match method {
"initialize" => Ok(initialize_result()),
"initialized" | "notifications/initialized" => return None,
"ping" => Ok(json!({})),
"tools/list" => Ok(json!({ "tools": tools::list() })),
"tools/call" => call_tool(¶ms).await,
other => Err(McpError {
code: -32601,
message: format!("method not found: {other}"),
}),
};
if is_notification {
return None;
}
let id = id.unwrap_or(Value::Null);
Some(match result {
Ok(value) => json!({
"jsonrpc": "2.0",
"id": id,
"result": value,
}),
Err(err) => json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": err.code,
"message": err.message,
},
}),
})
}
fn initialize_result() -> Value {
json!({
"protocolVersion": MCP_VERSION,
"capabilities": {
"tools": { "listChanged": false },
},
"serverInfo": {
"name": SERVER_NAME,
"version": SERVER_VERSION,
},
})
}
#[derive(Debug)]
struct McpError {
code: i64,
message: String,
}
async fn call_tool(params: &Value) -> Result<Value, McpError> {
let name = params
.get("name")
.and_then(Value::as_str)
.ok_or_else(|| McpError {
code: -32602,
message: "missing tool name".into(),
})?;
let arguments = params
.get("arguments")
.cloned()
.unwrap_or_else(|| json!({}));
let cli = tools::build_cli(name, &arguments).map_err(|e| McpError {
code: -32602,
message: e.to_string(),
})?;
let resp = tokio::task::spawn_blocking(move || run_cli(&cli))
.await
.map_err(|e| McpError {
code: -32603,
message: format!("blocking task: {e}"),
})?
.map_err(|e| McpError {
code: -32603,
message: format!("vs dispatch: {e:#}"),
})?;
Ok(json!({
"content": [{
"type": "text",
"text": resp,
}],
"isError": false,
}))
}
fn run_cli(cli: &Cli) -> Result<String> {
let resp = crate::commands::run(cli).context("vs_cli::commands::run")?;
Ok(crate::commands::render(&resp, false))
}