use anyhow::Result;
use serde_json::Value;
use std::collections::HashMap;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tracing::{debug, error, info, warn};
mod handlers;
mod protocol;
mod tools;
mod validation;
use handlers::RequestHandler;
use protocol::*;
use tools::ToolManager;
#[tokio::main]
async fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let mut tools_file_override: Option<String> = None;
let mut injected_values: HashMap<String, String> = HashMap::new();
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--help" | "-h" => {
print_help();
return Ok(());
}
"--version" | "-V" => {
println!("gamecode-mcp2 {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
"--tools-file" | "-t" => {
if i + 1 < args.len() {
tools_file_override = Some(args[i + 1].clone());
i += 2;
} else {
eprintln!("Error: --tools-file requires an argument");
std::process::exit(1);
}
}
"--inject" | "-i" => {
if i + 1 < args.len() {
let inject_arg = &args[i + 1];
if let Some((key, value)) = inject_arg.split_once('=') {
injected_values.insert(key.to_string(), value.to_string());
i += 2;
} else {
eprintln!("Error: --inject requires KEY=VALUE format");
std::process::exit(1);
}
} else {
eprintln!("Error: --inject requires an argument");
std::process::exit(1);
}
}
_ => {
eprintln!("Unknown argument: {}", args[i]);
eprintln!("Try 'gamecode-mcp2 --help' for more information.");
std::process::exit(1);
}
}
}
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("mcp_server=info".parse().unwrap()),
)
.with_writer(std::io::stderr)
.init();
info!("Starting GameCode MCP Server v2...");
let mut tool_manager = ToolManager::new();
if let Err(e) = tool_manager.load_with_precedence(tools_file_override).await {
warn!("Failed to load tools: {}", e);
warn!("The server will start but no tools will be available.");
}
let handler = RequestHandler::new(tool_manager, injected_values);
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let mut reader = BufReader::new(stdin);
let mut stdout = stdout;
info!("MCP server ready, waiting for requests...");
loop {
let mut line = String::new();
match reader.read_line(&mut line).await {
Ok(0) => {
info!("Client disconnected");
break;
}
Ok(_) => {
let line = line.trim();
if line.is_empty() {
continue;
}
debug!("Received: {}", line);
match serde_json::from_str::<Value>(line) {
Ok(value) => {
if value.get("id").is_some() {
match serde_json::from_value::<JsonRpcRequest>(value) {
Ok(request) => {
let response = handler.handle_request(request).await;
let response_str = serde_json::to_string(&response)?;
debug!("Sending: {}", response_str);
stdout.write_all(response_str.as_bytes()).await?;
stdout.write_all(b"\n").await?;
stdout.flush().await?;
}
Err(e) => {
error!("Invalid request: {}", e);
let error_response = JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: serde_json::Value::Null,
result: None,
error: Some(JsonRpcError {
code: INVALID_REQUEST,
message: "Invalid request".to_string(),
data: None,
}),
};
let response_str = serde_json::to_string(&error_response)?;
stdout.write_all(response_str.as_bytes()).await?;
stdout.write_all(b"\n").await?;
stdout.flush().await?;
}
}
} else {
match serde_json::from_value::<JsonRpcNotification>(value) {
Ok(notification) => {
handler.handle_notification(notification).await;
}
Err(e) => {
error!("Invalid notification: {}", e);
}
}
}
}
Err(e) => {
error!("Parse error: {}", e);
let error_response = JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: serde_json::Value::Null,
result: None,
error: Some(JsonRpcError {
code: PARSE_ERROR,
message: "Parse error".to_string(),
data: None,
}),
};
let response_str = serde_json::to_string(&error_response)?;
stdout.write_all(response_str.as_bytes()).await?;
stdout.write_all(b"\n").await?;
stdout.flush().await?;
}
}
}
Err(e) => {
error!("Read error: {}", e);
break;
}
}
}
info!("MCP server shutting down");
Ok(())
}
fn print_help() {
println!("gamecode-mcp2 {}", env!("CARGO_PKG_VERSION"));
println!("{}", env!("CARGO_PKG_DESCRIPTION"));
println!();
println!("USAGE:");
println!(" gamecode-mcp2 [OPTIONS]");
println!();
println!("OPTIONS:");
println!(" -h, --help Print help information");
println!(" -V, --version Print version information");
println!(" -t, --tools-file <FILE> Specify tools configuration file");
println!(" -i, --inject KEY=VALUE Inject server-side values (can be used multiple times)");
println!();
println!("DESCRIPTION:");
println!(" An MCP server that communicates via stdio (stdin/stdout).");
println!(" Configure tools in tools.yaml or via GAMECODE_TOOLS_FILE.");
println!(" ");
println!(" This server is designed to be spawned by MCP clients like");
println!(" Claude Desktop. It does not accept network connections.");
println!();
println!("ENVIRONMENT:");
println!(" GAMECODE_TOOLS_FILE Path to tools YAML configuration");
println!(" GAMECODE_MODE Load a specific mode/profile");
println!(" RUST_LOG Set logging level (default: info)");
println!();
println!("EXAMPLES:");
println!(" # Run with default tool detection");
println!(" gamecode-mcp2");
println!();
println!(" # Run with specific tools file");
println!(" GAMECODE_TOOLS_FILE=~/my-tools.yaml gamecode-mcp2");
println!();
println!(" # Run in Python development mode");
println!(" GAMECODE_MODE=python-dev gamecode-mcp2");
println!();
println!(" # Run with injected values for multi-tenant scenarios");
println!(" gamecode-mcp2 --inject tenant=customer123 --inject environment=production");
println!();
println!("MORE INFO:");
println!(" Repository: {}", env!("CARGO_PKG_REPOSITORY"));
}