41c8b36826
- Add handle_notification() for notification messages (no id field) - Use CARGO_PKG_VERSION for dynamic version in initialize response - Skip serializing None fields in McpResponse and McpError - Remove verified field from setup info
216 lines
6.2 KiB
Rust
216 lines
6.2 KiB
Rust
mod analytics;
|
|
mod compile;
|
|
mod models;
|
|
mod optimization;
|
|
mod pipeline;
|
|
mod storage;
|
|
mod tools;
|
|
|
|
mod mcp_server;
|
|
|
|
use anyhow::Result;
|
|
use clap::Parser;
|
|
use serde_json::{json, Value};
|
|
use std::io::{stdout, Write};
|
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
|
use tokio::net::TcpListener;
|
|
use tracing::{info, error};
|
|
|
|
#[derive(Parser)]
|
|
#[command(name = "mt5-quant")]
|
|
#[command(about = "MT5-Quant MCP Server - Exposes MT5 backtest and optimization tools via MCP")]
|
|
struct Cli {
|
|
/// Run as stdio MCP server (default)
|
|
#[arg(short, long, default_value = "false")]
|
|
stdio: bool,
|
|
|
|
/// Run on TCP port for debugging
|
|
#[arg(short, long)]
|
|
port: Option<u16>,
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
|
pub struct McpRequest {
|
|
jsonrpc: String,
|
|
id: Option<Value>,
|
|
method: String,
|
|
params: Option<Value>,
|
|
}
|
|
|
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
|
pub struct McpResponse {
|
|
jsonrpc: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
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, serde::Deserialize, serde::Serialize)]
|
|
pub struct McpError {
|
|
code: i32,
|
|
message: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
data: Option<Value>,
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize)]
|
|
pub struct ServerInfo {
|
|
name: String,
|
|
version: String,
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize)]
|
|
pub struct InitializeResult {
|
|
protocol_version: String,
|
|
capabilities: ServerCapabilities,
|
|
server_info: ServerInfo,
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize)]
|
|
pub struct ServerCapabilities {
|
|
experimental: Value,
|
|
tools: ToolCapabilities,
|
|
}
|
|
|
|
#[derive(Debug, serde::Serialize)]
|
|
pub struct ToolCapabilities {
|
|
list_changed: bool,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
tracing_subscriber::fmt()
|
|
.with_writer(std::io::stderr)
|
|
.init();
|
|
|
|
let cli = Cli::parse();
|
|
|
|
if let Some(port) = cli.port {
|
|
run_tcp_server(port).await?;
|
|
} else {
|
|
run_stdio_server().await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn run_stdio_server() -> Result<()> {
|
|
info!("Starting MT5-Quant MCP server on stdio");
|
|
|
|
let server = std::sync::Arc::new(mcp_server::McpServer::new());
|
|
let mut reader = BufReader::new(tokio::io::stdin());
|
|
let mut line = String::new();
|
|
|
|
loop {
|
|
line.clear();
|
|
match reader.read_line(&mut line).await {
|
|
Ok(0) => break, // EOF
|
|
Ok(_) => {
|
|
let line = line.trim();
|
|
if line.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let server_clone = server.clone();
|
|
match serde_json::from_str::<McpRequest>(line) {
|
|
Ok(request) => {
|
|
// Notifications have no id — don't send a response
|
|
if request.id.is_none() {
|
|
server_clone.handle_notification(request).await;
|
|
continue;
|
|
}
|
|
let response = server_clone.handle_request(request).await;
|
|
let response_json = serde_json::to_string(&response)?;
|
|
println!("{}", response_json);
|
|
stdout().flush()?;
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to parse JSON: {}", e);
|
|
let error_response = McpResponse {
|
|
jsonrpc: "2.0".to_string(),
|
|
id: None,
|
|
result: None,
|
|
error: Some(McpError {
|
|
code: -32700,
|
|
message: "Parse error".to_string(),
|
|
data: Some(json!(e.to_string())),
|
|
}),
|
|
};
|
|
let response_json = serde_json::to_string(&error_response)?;
|
|
println!("{}", response_json);
|
|
stdout().flush()?;
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to read from stdin: {}", e);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn run_tcp_server(port: u16) -> Result<()> {
|
|
info!("Starting MT5-Quant MCP server on TCP port {}", port);
|
|
|
|
let listener = TcpListener::bind(format!("127.0.0.1:{}", port)).await?;
|
|
info!("Listening on 127.0.0.1:{}", port);
|
|
|
|
loop {
|
|
let (socket, addr) = listener.accept().await?;
|
|
info!("New connection from {}", addr);
|
|
|
|
tokio::spawn(async move {
|
|
if let Err(e) = handle_connection(socket).await {
|
|
error!("Connection error: {}", e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async fn handle_connection(socket: tokio::net::TcpStream) -> Result<()> {
|
|
let (reader, mut writer) = socket.into_split();
|
|
let mut reader = BufReader::new(reader);
|
|
let mut line = String::new();
|
|
let server = mcp_server::McpServer::new();
|
|
|
|
loop {
|
|
line.clear();
|
|
match reader.read_line(&mut line).await {
|
|
Ok(0) => break,
|
|
Ok(_) => {
|
|
let line = line.trim();
|
|
if line.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
match serde_json::from_str::<McpRequest>(line) {
|
|
Ok(request) => {
|
|
if request.id.is_none() {
|
|
server.handle_notification(request).await;
|
|
continue;
|
|
}
|
|
let response = server.handle_request(request).await;
|
|
let response_json = serde_json::to_string(&response)? + "\n";
|
|
writer.write_all(response_json.as_bytes()).await?;
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to parse JSON: {}", e);
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to read: {}", e);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|