use hyper::StatusCode;
use serde_json::{Value, json};
use super::super::AppState;
use super::{MANUAL, tools};
const PROTOCOL_VERSIONS: &[&str] = &["2025-06-18", "2025-03-26", "2024-11-05"];
const MANUAL_URI: &str = "rustploy://manual";
pub async fn handle(state: &AppState, body: &[u8], server: &str) -> Option<(StatusCode, Value)> {
let msg: Value = match serde_json::from_slice(body) {
Ok(v) => v,
Err(e) => {
return Some((
StatusCode::BAD_REQUEST,
error(Value::Null, -32700, format!("JSON inválido: {e}")),
));
}
};
if msg.is_array() {
return Some((
StatusCode::BAD_REQUEST,
error(
Value::Null,
-32600,
"lotes JSON-RPC não são aceitos — uma mensagem por POST",
),
));
}
let id = msg.get("id").cloned()?;
let Some(method) = msg.get("method").and_then(Value::as_str) else {
return None;
};
let params = msg.get("params").cloned().unwrap_or(Value::Null);
let resultado = match method {
"initialize" => Ok(initialize(¶ms, server)),
"ping" => Ok(json!({})),
"tools/list" => Ok(tools_list()),
"tools/call" => tools_call(state, ¶ms).await,
"resources/list" => Ok(json!({
"resources": [{
"uri": MANUAL_URI,
"name": "manual",
"title": "Manual do agente rustploy",
"mimeType": "text/markdown",
}]
})),
"resources/read" => resources_read(¶ms),
"prompts/list" => Ok(json!({ "prompts": [] })),
outro => Err((-32601, format!("método {outro:?} não existe"))),
};
Some(match resultado {
Ok(result) => (
StatusCode::OK,
json!({ "jsonrpc": "2.0", "id": id, "result": result }),
),
Err((code, message)) => (StatusCode::OK, error(id, code, message)),
})
}
fn error(id: Value, code: i64, message: impl Into<String>) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"error": { "code": code, "message": message.into() }
})
}
fn initialize(params: &Value, server: &str) -> Value {
let pedida = params.get("protocolVersion").and_then(Value::as_str);
let versao = pedida
.filter(|v| PROTOCOL_VERSIONS.contains(v))
.unwrap_or(PROTOCOL_VERSIONS[0]);
json!({
"protocolVersion": versao,
"capabilities": {
"tools": { "listChanged": false },
"resources": { "listChanged": false, "subscribe": false },
},
"serverInfo": {
"name": "rustploy",
"title": format!("Rustploy daemon em {server}"),
"version": env!("CARGO_PKG_VERSION"),
},
"instructions": format!(
"Este servidor MCP é o daemon rustploy em **{server}**. Se houver mais de \
um rustploy registrado, confirme que é este o que o usuário pediu antes \
de qualquer ação que altere algo.\n\n{MANUAL}"
),
})
}
fn tools_list() -> Value {
let lista: Vec<Value> = tools::TOOLS
.iter()
.map(|t| {
json!({
"name": t.name,
"description": t.description,
"inputSchema": (t.input_schema)(),
})
})
.collect();
json!({ "tools": lista })
}
async fn tools_call(state: &AppState, params: &Value) -> Result<Value, (i64, String)> {
let nome = params
.get("name")
.and_then(Value::as_str)
.ok_or((-32602, "tools/call sem \"name\"".to_owned()))?;
if !tools::TOOLS.iter().any(|t| t.name == nome) {
return Err((
-32602,
format!("ferramenta {nome:?} não existe — veja tools/list"),
));
}
let args = params
.get("arguments")
.cloned()
.unwrap_or_else(|| json!({}));
Ok(match tools::call(state, nome, &args).await {
Ok(valor) => {
let texto = match valor.get("manual").and_then(Value::as_str) {
Some(md) if nome == "manual" => md.to_owned(),
_ => serde_json::to_string_pretty(&valor).unwrap_or_default(),
};
let mut out = json!({
"content": [{ "type": "text", "text": texto }],
"isError": false,
});
if valor.is_object() {
out["structuredContent"] = valor;
}
out
}
Err(falha) => json!({
"content": [{ "type": "text", "text": format!("{}: {}", falha.code, falha.message) }],
"isError": true,
}),
})
}
fn resources_read(params: &Value) -> Result<Value, (i64, String)> {
match params.get("uri").and_then(Value::as_str) {
Some(MANUAL_URI) => Ok(json!({
"contents": [{ "uri": MANUAL_URI, "mimeType": "text/markdown", "text": MANUAL }]
})),
Some(outro) => Err((-32002, format!("recurso {outro:?} não existe"))),
None => Err((-32602, "resources/read sem \"uri\"".to_owned())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initialize_ecoa_versao_conhecida_e_manda_o_manual() {
let r = initialize(&json!({ "protocolVersion": "2025-03-26" }), "rp.exemplo.com:9797");
assert_eq!(r["protocolVersion"], "2025-03-26");
let instructions = r["instructions"].as_str().unwrap();
assert!(instructions.ends_with(MANUAL));
assert!(instructions.starts_with("Este servidor MCP é o daemon rustploy em **rp.exemplo.com:9797**"));
assert_eq!(r["serverInfo"]["name"], "rustploy");
assert_eq!(r["serverInfo"]["title"], "Rustploy daemon em rp.exemplo.com:9797");
let r = initialize(&json!({ "protocolVersion": "1999-01-01" }), "x:1");
assert_eq!(r["protocolVersion"], PROTOCOL_VERSIONS[0]);
}
#[test]
fn tools_list_traz_todas_com_schema() {
let r = tools_list();
let nomes: Vec<&str> = r["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap())
.collect();
assert!(nomes.contains(&"deploy") && nomes.contains(&"rpc"));
assert!(r["tools"][0]["inputSchema"].is_object());
}
#[test]
fn manual_como_recurso() {
let r = resources_read(&json!({ "uri": MANUAL_URI })).unwrap();
assert_eq!(r["contents"][0]["text"], MANUAL);
assert!(resources_read(&json!({ "uri": "x://y" })).is_err());
}
}