use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::io::{BufRead, Write};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct RpcRequest {
jsonrpc: String,
id: Option<serde_json::Value>,
method: String,
params: Option<serde_json::Value>,
}
#[derive(Debug, Serialize)]
struct RpcResponse {
jsonrpc: String,
id: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<serde_json::Value>,
}
pub fn run_stdio_server() -> Result<()> {
let stdin = std::io::stdin();
let mut stdout = std::io::stdout();
for line in stdin.lock().lines() {
let text = match line {
Ok(t) => t,
Err(_) => break,
};
if text.trim().is_empty() {
continue;
}
let req: RpcRequest = match serde_json::from_str(&text) {
Ok(r) => r,
Err(_) => continue,
};
let resp = handle_request(&req);
let resp_str = serde_json::to_string(&resp)?;
writeln!(stdout, "{}", resp_str)?;
stdout.flush()?;
}
Ok(())
}
fn handle_request(req: &RpcRequest) -> RpcResponse {
match req.method.as_str() {
"tools/list" => RpcResponse {
jsonrpc: "2.0".into(),
id: req.id.clone(),
result: Some(json!({
"tools": [
{
"name": "seo_keywords",
"description": "Fetch autocomplete keyword suggestions for zero cost",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"]
}
},
{
"name": "seo_serp_inspect",
"description": "Scrape live DuckDuckGo SERP competitors and analyze gaps",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"limit": { "type": "integer" }
},
"required": ["query"]
}
},
{
"name": "seo_audit",
"description": "Audit local markdown or HTML file for on-page SEO issues",
"inputSchema": {
"type": "object",
"properties": {
"path": { "type": "string" }
},
"required": ["path"]
}
}
]
})),
error: None,
},
"tools/call" => {
let params = req.params.as_ref();
let tool_name = params.and_then(|p| p.get("name")).and_then(|n| n.as_str()).unwrap_or("");
let args = params.and_then(|p| p.get("arguments")).cloned().unwrap_or(json!({}));
let result_content = execute_tool(tool_name, &args);
RpcResponse {
jsonrpc: "2.0".into(),
id: req.id.clone(),
result: Some(json!({
"content": [
{
"type": "text",
"text": result_content
}
]
})),
error: None,
}
}
_ => RpcResponse {
jsonrpc: "2.0".into(),
id: req.id.clone(),
result: None,
error: Some(json!({ "code": -32601, "message": "Method not found" })),
},
}
}
fn execute_tool(name: &str, args: &serde_json::Value) -> String {
match name {
"seo_keywords" => {
let q = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
match crate::serp::get_autocomplete(q) {
Ok(items) => serde_json::to_string_pretty(&items).unwrap_or_default(),
Err(e) => format!("Error: {}", e),
}
}
"seo_serp_inspect" => {
let q = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
match crate::serp::scrape_serp(q, limit) {
Ok(items) => serde_json::to_string_pretty(&items).unwrap_or_default(),
Err(e) => format!("Error: {}", e),
}
}
"seo_audit" => {
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or("");
match crate::audit::audit_file(path) {
Ok(rep) => serde_json::to_string_pretty(&rep).unwrap_or_default(),
Err(e) => format!("Error: {}", e),
}
}
_ => format!("Unknown tool: {}", name),
}
}