use std::collections::HashMap;
use std::process::Stdio;
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::process::{Child, Command};
use tokio::sync::RwLock;
use tracing::{info, debug, error, warn};
mod handler;
use handler::TestingHandler;
#[derive(Debug, Deserialize)]
struct McpRequest {
jsonrpc: String,
id: Option<Value>,
method: String,
params: Option<Value>,
}
#[derive(Debug, Serialize)]
struct McpResponse {
jsonrpc: &'static str,
id: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<McpError>,
}
#[derive(Debug, Serialize)]
struct McpError {
code: i32,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Value>,
}
#[derive(Debug, Serialize)]
struct ServerCapabilities {
tools: ToolCapabilities,
}
#[derive(Debug, Serialize)]
struct ToolCapabilities {
list_changed: bool,
}
#[derive(Debug, Serialize)]
struct Tool {
name: String,
description: String,
input_schema: Value,
}
#[derive(Debug, Serialize)]
struct ServerInfo {
name: &'static str,
version: &'static str,
}
#[derive(Debug, Serialize)]
struct InitializeResult {
protocol_version: &'static str,
capabilities: ServerCapabilities,
server_info: ServerInfo,
}
#[derive(Debug, Serialize)]
struct ToolsListResult {
tools: Vec<Tool>,
}
#[derive(Debug, Serialize)]
struct ToolCallResult {
content: Vec<ToolContent>,
is_error: bool,
}
#[derive(Debug, Serialize)]
struct ToolContent {
#[serde(rename = "type")]
content_type: &'static str,
text: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
enum TestingOperation {
LaunchGame { executable: String, args: Option<Vec<String>>, working_dir: Option<String> },
StopGame { force: Option<bool> },
GetGameState,
SaveGameState { slot: Option<u32> },
LoadGameState { slot: Option<u32> },
TakeScreenshot { path: Option<String> },
RunTest { test_name: String, args: Option<Value> },
}
struct GameProcessManager {
process: RwLock<Option<Child>>,
state: RwLock<GameState>,
}
#[derive(Debug, Clone, Default)]
struct GameState {
running: bool,
pid: Option<u32>,
executable: Option<String>,
start_time: Option<u64>,
memory_mb: Option<u64>,
}
impl GameProcessManager {
fn new() -> Self {
Self {
process: RwLock::new(None),
state: RwLock::new(GameState::default()),
}
}
async fn launch(&self, executable: &str, args: &[String], working_dir: Option<&str>) -> Result<(), String> {
let mut process_lock = self.process.write().await;
let mut state_lock = self.state.write().await;
if process_lock.is_some() {
return Err("Game already running".to_string());
}
info!("Launching game: {} {:?}", executable, args);
let mut cmd = Command::new(executable);
cmd.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(dir) = working_dir {
cmd.current_dir(dir);
}
let child = cmd.spawn()
.map_err(|e| format!("Failed to launch game: {}", e))?;
let pid = child.id();
let start_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
*process_lock = Some(child);
*state_lock = GameState {
running: true,
pid,
executable: Some(executable.to_string()),
start_time: Some(start_time),
memory_mb: None,
};
info!("Game launched with PID {:?}", pid);
Ok(())
}
async fn stop(&self, force: bool) -> Result<(), String> {
let mut process_lock = self.process.write().await;
let mut state_lock = self.state.write().await;
if let Some(mut process) = process_lock.take() {
info!("Stopping game (force={})", force);
if force {
let _ = process.kill().await;
} else {
let _ = process.kill().await;
}
*state_lock = GameState::default();
info!("Game stopped");
}
Ok(())
}
async fn get_state(&self) -> GameState {
self.state.read().await.clone()
}
async fn save_state(&self, slot: Option<u32>) -> Result<String, String> {
let slot = slot.unwrap_or(0);
info!("Saving game state to slot {}", slot);
Ok(format!("State saved to slot {}", slot))
}
async fn load_state(&self, slot: Option<u32>) -> Result<String, String> {
let slot = slot.unwrap_or(0);
info!("Loading game state from slot {}", slot);
Ok(format!("State loaded from slot {}", slot))
}
async fn take_screenshot(&self, path: Option<String>) -> Result<String, String> {
let path = path.unwrap_or_else(|| "screenshot.png".to_string());
info!("Taking screenshot: {}", path);
Ok(format!("Screenshot saved to {}", path))
}
}
struct McpServer {
handler: Arc<Mutex<TestingHandler>>,
game_manager: Arc<GameProcessManager>,
}
impl McpServer {
fn new() -> Self {
Self {
handler: Arc::new(Mutex::new(TestingHandler::new())),
game_manager: Arc::new(GameProcessManager::new()),
}
}
fn handle_request(&self, request: McpRequest) -> McpResponse {
debug!("Handling method: {}", request.method);
match request.method.as_str() {
"initialize" => self.handle_initialize(request.id),
"tools/list" => self.handle_tools_list(request.id),
"tools/call" => self.handle_tool_call(request.id, request.params),
_ => McpResponse {
jsonrpc: "2.0",
id: request.id,
result: None,
error: Some(McpError {
code: -32601,
message: format!("Method not found: {}", request.method),
data: None,
}),
}
}
}
fn handle_initialize(&self, id: Option<Value>) -> McpResponse {
McpResponse {
jsonrpc: "2.0",
id,
result: Some(serde_json::to_value(InitializeResult {
protocol_version: "2024-11-05",
capabilities: ServerCapabilities {
tools: ToolCapabilities { list_changed: false },
},
server_info: ServerInfo {
name: "phenotype-mcp-testing",
version: "0.1.0",
},
}).unwrap()),
error: None,
}
}
fn handle_tools_list(&self, id: Option<Value>) -> McpResponse {
let tools = vec![
Tool {
name: "game_launch".to_string(),
description: "Launch a game process".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"executable": { "type": "string", "description": "Path to game executable" },
"args": { "type": "array", "items": { "type": "string" }, "description": "Command line arguments" },
"working_dir": { "type": "string", "description": "Working directory" }
},
"required": ["executable"]
}),
},
Tool {
name: "game_stop".to_string(),
description: "Stop the running game process".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"force": { "type": "boolean", "description": "Force kill", "default": false }
}
}),
},
Tool {
name: "game_get_state".to_string(),
description: "Get current game state".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {}
}),
},
Tool {
name: "game_save_state".to_string(),
description: "Save game state to a slot".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"slot": { "type": "integer", "description": "Save slot number", "default": 0 }
}
}),
},
Tool {
name: "game_load_state".to_string(),
description: "Load game state from a slot".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"slot": { "type": "integer", "description": "Save slot number", "default": 0 }
}
}),
},
Tool {
name: "game_screenshot".to_string(),
description: "Take a screenshot of the game".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"path": { "type": "string", "description": "Screenshot save path", "default": "screenshot.png" }
}
}),
},
Tool {
name: "game_run_test".to_string(),
description: "Run an automated test".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"test_name": { "type": "string", "description": "Name of the test to run" },
"args": { "type": "object", "description": "Test arguments" }
},
"required": ["test_name"]
}),
},
];
McpResponse {
jsonrpc: "2.0",
id,
result: Some(serde_json::to_value(ToolsListResult { tools }).unwrap()),
error: None,
}
}
fn handle_tool_call(&self, id: Option<Value>, params: Option<Value>) -> McpResponse {
let params = match params {
Some(p) => p,
None => {
return McpResponse {
jsonrpc: "2.0",
id,
result: None,
error: Some(McpError {
code: -32602,
message: "Missing params".to_string(),
data: None,
}),
}
}
};
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
let arguments = params.get("arguments").cloned().unwrap_or(serde_json::json!({}));
let rt = tokio::runtime::Handle::current();
let game_manager = self.game_manager.clone();
let result: Result<String, String> = rt.block_on(async move {
match name {
"game_launch" => {
let executable = arguments.get("executable").and_then(|v| v.as_str()).ok_or("Missing executable")?;
let args: Vec<String> = arguments.get("args")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default();
let working_dir = arguments.get("working_dir").and_then(|v| v.as_str());
game_manager.launch(executable, &args, working_dir).await
.map(|_| format!("Game launched: {}", executable))
}
"game_stop" => {
let force = arguments.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
game_manager.stop(force).await
.map(|_| "Game stopped".to_string())
}
"game_get_state" => {
let state = game_manager.get_state().await;
Ok(format!("Running: {}, PID: {:?}, Executable: {:?}",
state.running, state.pid, state.executable))
}
"game_save_state" => {
let slot = arguments.get("slot").and_then(|v| v.as_u64()).map(|v| v as u32);
game_manager.save_state(slot).await
}
"game_load_state" => {
let slot = arguments.get("slot").and_then(|v| v.as_u64()).map(|v| v as u32);
game_manager.load_state(slot).await
}
"game_screenshot" => {
let path = arguments.get("path").and_then(|v| v.as_str()).map(String::from);
game_manager.take_screenshot(path).await
}
"game_run_test" => {
let test_name = arguments.get("test_name").and_then(|v| v.as_str()).ok_or("Missing test_name")?;
let _args = arguments.get("args").cloned();
Ok(format!("Test '{}' completed (mock)", test_name))
}
_ => Err(format!("Unknown tool: {}", name)),
}
});
match result {
Ok(content) => McpResponse {
jsonrpc: "2.0",
id,
result: Some(serde_json::to_value(ToolCallResult {
content: vec![ToolContent {
content_type: "text",
text: content,
}],
is_error: false,
}).unwrap()),
error: None,
},
Err(e) => McpResponse {
jsonrpc: "2.0",
id,
result: Some(serde_json::to_value(ToolCallResult {
content: vec![ToolContent {
content_type: "text",
text: e,
}],
is_error: true,
}).unwrap()),
error: None,
}
}
}
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env()
.add_directive(tracing::Level::INFO.into()))
.init();
info!("Starting phenotype-mcp-testing server");
let server = McpServer::new();
use tokio::io::{AsyncBufReadExt, BufReader};
use std::io::Write;
let stdin = tokio::io::stdin();
let mut reader = BufReader::new(stdin);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => {
info!("EOF received, shutting down");
break;
}
Ok(_) => {
let line = line.trim();
if line.is_empty() {
continue;
}
debug!("Received: {}", line);
match serde_json::from_str::<McpRequest>(line) {
Ok(request) => {
let response = server.handle_request(request);
let response_json = serde_json::to_string(&response).unwrap();
let mut stdout = std::io::stdout();
if let Err(e) = writeln!(stdout, "{}", response_json) {
error!("Failed to write response: {}", e);
break;
}
}
Err(e) => {
error!("Failed to parse request: {}", e);
let error_response = serde_json::json!({
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32700,
"message": format!("Parse error: {}", e)
}
});
let mut stdout = std::io::stdout();
let _ = writeln!(stdout, "{}", error_response);
}
}
}
Err(e) => {
error!("Failed to read line: {}", e);
break;
}
}
}
}