use std::io::{BufRead, Write};
use serde_json::{Value, json};
use crate::verbs::{ParameterInfo, ParameterKind, VerbInfo};
use crate::{Error, Session};
pub const PROTOCOL_VERSION: &str = "2024-11-05";
pub const SERVER_NAME: &str = "xtalforge";
fn schema_type(kind: ParameterKind) -> &'static str {
match kind {
ParameterKind::String => "string",
ParameterKind::Integer => "integer",
ParameterKind::Number => "number",
ParameterKind::Boolean => "boolean",
ParameterKind::Array => "array",
ParameterKind::Object => "object",
}
}
fn parameter_schema(parameter: &ParameterInfo) -> Value {
let mut schema = json!({
"type": schema_type(parameter.kind),
"description": parameter.summary,
});
if parameter.kind == ParameterKind::Array
&& let Some(object) = schema.as_object_mut()
{
object.insert("items".to_owned(), json!({"type": "integer"}));
object.insert("minItems".to_owned(), json!(3));
object.insert("maxItems".to_owned(), json!(3));
}
if let Some(default) = parameter.default
&& let Ok(value) = serde_json::from_str::<Value>(default)
&& let Some(object) = schema.as_object_mut()
{
object.insert("default".to_owned(), value);
}
schema
}
#[must_use]
pub fn input_schema(verb: &VerbInfo) -> Value {
let mut properties = serde_json::Map::new();
let mut required = Vec::new();
for parameter in verb.parameters {
properties.insert(parameter.name.to_owned(), parameter_schema(parameter));
if parameter.required {
required.push(Value::String(parameter.name.to_owned()));
}
}
let mut schema = json!({
"type": "object",
"properties": properties,
"additionalProperties": false,
});
if !required.is_empty()
&& let Some(object) = schema.as_object_mut()
{
object.insert("required".to_owned(), Value::Array(required));
}
schema
}
fn tool(verb: &VerbInfo) -> Value {
json!({
"name": verb.name,
"description": verb.summary,
"inputSchema": input_schema(verb),
})
}
fn success(id: Value, result: Value) -> Value {
json!({"jsonrpc": "2.0", "id": id, "result": result})
}
fn failure(id: Value, code: i64, message: &str) -> Value {
json!({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}})
}
#[derive(Debug)]
pub struct McpServer {
session: Session,
}
impl Default for McpServer {
fn default() -> Self {
Self::new()
}
}
impl McpServer {
#[must_use]
pub fn new() -> Self {
Self {
session: Session::new(),
}
}
#[must_use]
pub fn session(&self) -> &Session {
&self.session
}
#[must_use]
pub fn handle(&mut self, request: &Value) -> Option<Value> {
let id = request.get("id").cloned();
let method = request.get("method").and_then(Value::as_str).unwrap_or("");
if method.starts_with("notifications/") || id.is_none() {
return None;
}
let id = id.unwrap_or(Value::Null);
let result = match method {
"initialize" => Ok(json!({
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {"tools": {"listChanged": false}},
"serverInfo": {
"name": SERVER_NAME,
"version": xtalforge_core::version(),
},
"instructions": crate::context::instructions(),
})),
"ping" => Ok(json!({})),
"tools/list" => Ok(json!({
"tools": crate::verbs::catalogue().iter().map(tool).collect::<Vec<_>>(),
})),
"tools/call" => {
let params = request.get("params").cloned().unwrap_or_else(|| json!({}));
let Some(name) = params.get("name").and_then(Value::as_str) else {
return Some(failure(id, -32602, "tools/call needs a tool name"));
};
let arguments = params
.get("arguments")
.cloned()
.unwrap_or_else(|| json!({}));
match self.session.run(name, &arguments) {
Ok(answer) => Ok(json!({
"content": [{
"type": "text",
"text": serde_json::to_string(&answer).unwrap_or_default(),
}],
"isError": false,
})),
Err(error) => Ok(json!({
"content": [{
"type": "text",
"text": serde_json::to_string(&json!({
"code": error.code(),
"message": error.to_string(),
}))
.unwrap_or_default(),
}],
"isError": true,
})),
}
}
other => Err((-32601_i64, format!("method not found: {other}"))),
};
Some(match result {
Ok(value) => success(id, value),
Err((code, message)) => failure(id, code, &message),
})
}
pub fn serve<R: BufRead, W: Write>(&mut self, reader: R, mut writer: W) -> Result<(), Error> {
for line in reader.lines() {
let line = line.map_err(|error| Error::Io(error.to_string()))?;
if line.trim().is_empty() {
continue;
}
let request: Value = match serde_json::from_str(&line) {
Ok(value) => value,
Err(error) => {
let response = failure(Value::Null, -32700, &format!("parse error: {error}"));
writeln!(writer, "{response}").map_err(|error| Error::Io(error.to_string()))?;
continue;
}
};
if let Some(response) = self.handle(&request) {
writeln!(writer, "{response}").map_err(|error| Error::Io(error.to_string()))?;
writer
.flush()
.map_err(|error| Error::Io(error.to_string()))?;
}
}
Ok(())
}
}
pub fn serve_stdio() -> Result<(), Error> {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
McpServer::new().serve(stdin.lock(), stdout.lock())
}