use crate::application::runtime::InvocationContext;
use crate::error::CarryCtxError;
use serde_json::Value;
use std::io::{self, BufRead, Write};
pub fn run_stdio_server(_ctx: &InvocationContext) -> Result<(), CarryCtxError> {
let stdin = io::stdin();
let mut stdout = io::stdout();
for line_res in stdin.lock().lines() {
let line = line_res
.map_err(|e| CarryCtxError::database_error(format!("Failed to read stdin: {e}")))?;
let line = line.trim();
if line.is_empty() {
continue;
}
let req: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(e) => {
let err_res = serde_json::json!({
"jsonrpc": "2.0",
"id": Value::Null,
"error": { "code": -32700, "message": format!("Parse error: {e}") }
});
writeln!(stdout, "{}", serde_json::to_string(&err_res).unwrap()).unwrap();
stdout.flush().unwrap();
continue;
}
};
let id = req.get("id").cloned().unwrap_or(Value::Null);
let method = req.get("method").and_then(|m| m.as_str()).unwrap_or("");
if method == "initialize" {
let res = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"protocolVersion": "2024-11-05", "capabilities": {
"tools": { "listChanged": true }
},
"serverInfo": {
"name": "carryctx",
"version": env!("CARGO_PKG_VERSION")
}
}
});
writeln!(stdout, "{}", serde_json::to_string(&res).unwrap()).unwrap();
stdout.flush().unwrap();
continue;
}
if method == "notifications/initialized" || method == "initialized" {
continue;
}
if method == "tools/list" {
let res = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"tools": [
{
"name": "carryctx_graph_explorer",
"description": "Query, scan, and export the project Context Graph (nodes, edges, dependencies, file-to-file links). Actions: scan, edges, link, add-node, export.",
"inputSchema": {
"type": "object",
"properties": {
"action": { "type": "string", "description": "The graph subcommand: scan, edges, link, add-node, export" },
"args": { "type": "array", "items": { "type": "string" }, "description": "CLI flags and arguments (e.g. ['--format', 'mermaid', '--compact'])" }
},
"required": ["action"]
}
},
{
"name": "carryctx_context_manager",
"description": "Manage persistent context, checkpoints, and state snapshots. Actions: status, context, checkpoint, resume, doctor.",
"inputSchema": {
"type": "object",
"properties": {
"action": { "type": "string", "description": "The context command: status, context, checkpoint, resume, doctor" },
"args": { "type": "array", "items": { "type": "string" }, "description": "CLI flags and arguments" }
},
"required": ["action"]
}
},
{
"name": "carryctx_task_manager",
"description": "Manage project tasks, dependencies, and priorities. Actions: list, create, update, claim, complete, block, unblock.",
"inputSchema": {
"type": "object",
"properties": {
"action": { "type": "string", "description": "The task subcommand: list, create, update, claim, complete, block, unblock" },
"args": { "type": "array", "items": { "type": "string" }, "description": "CLI flags and arguments" }
},
"required": ["action"]
}
},
{
"name": "carryctx_progress_tracker",
"description": "Manage task progress, notes, and blockers. Actions: list, create, update, resolve.",
"inputSchema": {
"type": "object",
"properties": {
"action": { "type": "string", "description": "The progress subcommand: list, create, update, resolve" },
"args": { "type": "array", "items": { "type": "string" }, "description": "CLI flags and arguments" }
},
"required": ["action"]
}
},
{
"name": "carryctx_decision_logger",
"description": "Log and search architectural decision records (ADRs). Actions: list, record, resolve.",
"inputSchema": {
"type": "object",
"properties": {
"action": { "type": "string", "description": "The decision subcommand: list, record, resolve" },
"args": { "type": "array", "items": { "type": "string" }, "description": "CLI flags and arguments" }
},
"required": ["action"]
}
},
{
"name": "carryctx_project_admin",
"description": "Manage project database, stats, cold storage archiving, and config. Actions: stats, prune, config, project.",
"inputSchema": {
"type": "object",
"properties": {
"action": { "type": "string", "description": "The administrative command: stats, prune, config, project" },
"args": { "type": "array", "items": { "type": "string" }, "description": "CLI flags and arguments" }
},
"required": ["action"]
}
}
]
}
});
writeln!(stdout, "{}", serde_json::to_string(&res).unwrap()).unwrap();
stdout.flush().unwrap();
continue;
}
if method == "tools/call" {
let params = req.get("params").and_then(|p| p.as_object());
let name = params
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.unwrap_or("");
let args_obj = params
.and_then(|p| p.get("arguments"))
.and_then(|a| a.as_object());
let action = args_obj
.and_then(|a| a.get("action"))
.and_then(|a| a.as_str())
.unwrap_or("");
let cli_args = args_obj
.and_then(|a| a.get("args"))
.and_then(|a| a.as_array());
let mut cmd = std::process::Command::new(
std::env::current_exe().unwrap_or_else(|_| "carryctx".into()),
);
cmd.arg("--json");
let valid = match name {
"carryctx_graph_explorer" => {
cmd.arg("graph");
true
}
"carryctx_task_manager" => {
cmd.arg("task");
true
}
"carryctx_progress_tracker" => {
cmd.arg("progress");
true
}
"carryctx_decision_logger" => {
cmd.arg("decision");
true
}
"carryctx_project_admin" => {
if action == "prune" {
cmd.arg("project");
}
true
}
"carryctx_context_manager" | "carryctx_session_controller" => true, _ => false,
};
if !valid {
let err_res = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": { "code": -32601, "message": format!("Tool not found: {}", name) }
});
writeln!(stdout, "{}", serde_json::to_string(&err_res).unwrap()).unwrap();
stdout.flush().unwrap();
continue;
}
if !action.is_empty() {
cmd.arg(action);
}
if let Some(arr) = cli_args {
for a in arr {
if let Some(s) = a.as_str() {
cmd.arg(s);
}
}
}
match cmd.output() {
Ok(output) => {
let stdout_str = String::from_utf8_lossy(&output.stdout).to_string();
let stderr_str = String::from_utf8_lossy(&output.stderr).to_string();
let mut text = stdout_str.clone();
if !stderr_str.is_empty() {
if !text.is_empty() {
text.push_str("\n--- STDERR ---\n");
}
text.push_str(&stderr_str);
}
let res = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"content": [
{ "type": "text", "text": text }
],
"isError": !output.status.success()
}
});
writeln!(stdout, "{}", serde_json::to_string(&res).unwrap()).unwrap();
stdout.flush().unwrap();
}
Err(e) => {
let err_res = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": { "code": -32000, "message": format!("Failed to execute carryctx subprocess: {}", e) }
});
writeln!(stdout, "{}", serde_json::to_string(&err_res).unwrap()).unwrap();
stdout.flush().unwrap();
}
}
continue;
}
let err_res = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": -32601,
"message": format!("Method not found: {}", method)
}
});
writeln!(stdout, "{}", serde_json::to_string(&err_res).unwrap()).unwrap();
stdout.flush().unwrap();
}
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn test_mcp_tools_list_contains_graph_explorer() {
let tools_list_response = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{ "name": "carryctx_graph_explorer" },
{ "name": "carryctx_context_manager" },
{ "name": "carryctx_task_manager" },
{ "name": "carryctx_progress_tracker" },
{ "name": "carryctx_decision_logger" },
{ "name": "carryctx_project_admin" }
]
}
});
let tools = tools_list_response["result"]["tools"].as_array().unwrap();
assert_eq!(tools.len(), 6);
assert!(tools.iter().any(|t| t["name"] == "carryctx_graph_explorer"));
}
}