use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sqlrite::Connection;
use crate::error::ProtocolError;
use crate::tools;
pub const MCP_PROTOCOL_VERSION: &str = "2025-11-25";
pub struct ServerState {
pub conn: Connection,
pub read_only: bool,
pub initialized: bool,
}
impl ServerState {
pub fn new(conn: Connection, read_only: bool) -> Self {
Self {
conn,
read_only,
initialized: false,
}
}
}
#[derive(Debug, Deserialize)]
struct Request {
#[allow(dead_code)]
jsonrpc: String,
id: Option<Value>,
method: String,
#[serde(default)]
params: Option<Value>,
}
#[derive(Debug, Serialize)]
struct Response<'a> {
jsonrpc: &'static str,
id: &'a Value,
result: Value,
}
pub fn handle(message: &str, state: &mut ServerState) -> Option<Value> {
let req: Request = match serde_json::from_str(message) {
Ok(r) => r,
Err(err) => {
return Some(error_response(
&Value::Null,
&ProtocolError::parse_error(format!("invalid JSON: {err}")),
));
}
};
let id = req.id.clone();
let is_notification = id.is_none();
let needs_init = !matches!(
req.method.as_str(),
"initialize" | "notifications/initialized" | "notifications/cancelled"
);
if needs_init && !state.initialized {
if is_notification {
return None;
}
return Some(error_response(
&id.unwrap_or(Value::Null),
&ProtocolError::server_not_initialized(format!(
"received `{}` before `initialize`",
req.method
)),
));
}
let result = match req.method.as_str() {
"initialize" => handle_initialize(state, req.params.as_ref()),
"notifications/initialized" => {
state.initialized = true;
return None; }
"notifications/cancelled" => {
return None;
}
"shutdown" => Ok(Value::Null),
"tools/list" => handle_tools_list(state),
"tools/call" => handle_tools_call(state, req.params.as_ref()),
"ping" => Ok(json!({})),
other => {
if is_notification {
return None;
}
return Some(error_response(
&id.unwrap_or(Value::Null),
&ProtocolError::method_not_found(other),
));
}
};
if is_notification {
return None;
}
let id = id.unwrap_or(Value::Null);
match result {
Ok(value) => Some(
serde_json::to_value(Response {
jsonrpc: "2.0",
id: &id,
result: value,
})
.unwrap(),
),
Err(err) => Some(error_response(&id, &err)),
}
}
fn error_response(id: &Value, err: &ProtocolError) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": err.code,
"message": err.message,
}
})
}
fn handle_initialize(
state: &mut ServerState,
params: Option<&Value>,
) -> Result<Value, ProtocolError> {
let _ = params;
state.initialized = true;
Ok(json!({
"protocolVersion": MCP_PROTOCOL_VERSION,
"capabilities": {
"tools": { "listChanged": false }
},
"serverInfo": {
"name": "sqlrite-mcp",
"version": env!("CARGO_PKG_VERSION"),
}
}))
}
fn handle_tools_list(state: &ServerState) -> Result<Value, ProtocolError> {
Ok(json!({
"tools": tools::list(state.read_only),
}))
}
#[derive(Debug, Deserialize)]
struct ToolsCallParams {
name: String,
#[serde(default)]
arguments: Value,
}
fn handle_tools_call(
state: &mut ServerState,
params: Option<&Value>,
) -> Result<Value, ProtocolError> {
let params = params.ok_or_else(|| {
ProtocolError::invalid_params("tools/call requires `name` + `arguments` params")
})?;
let parsed: ToolsCallParams = serde_json::from_value(params.clone()).map_err(|err| {
ProtocolError::invalid_params(format!("invalid tools/call params: {err}"))
})?;
if parsed.name == "execute" && state.read_only {
return Ok(tool_error_result(
"the `execute` tool is disabled in read-only mode (--read-only). \
Use `query` for SELECT statements, or restart the server without --read-only.",
));
}
match tools::dispatch(&parsed.name, parsed.arguments, state) {
Ok(text) => Ok(tool_text_result(text, false)),
Err(crate::error::ToolError(msg)) => Ok(tool_text_result(msg, true)),
}
}
pub(crate) fn tool_text_result(text: String, is_error: bool) -> Value {
json!({
"content": [{ "type": "text", "text": text }],
"isError": is_error,
})
}
pub(crate) fn tool_error_result(msg: impl Into<String>) -> Value {
tool_text_result(msg.into(), true)
}