tftio-kb 2.5.4

Personal knowledge base — typed AST with org-mode as projection, SQLite-backed
Documentation
//! JSON-RPC 2.0 envelope types for the MCP transport.
//!
//! Mirrors the Haskell `KB.MCP.Protocol`. One JSON message per line on
//! the wire; every message is either a `Request` (with `id`) or a
//! `Notification` (no `id`). Both shapes are modelled as a single
//! [`Request`] whose `id` is `Option<RpcId>`.
//!
//! Dispatch lives in [`super::server`]; this module is purely the wire
//! surface.

use serde::{Deserialize, Serialize, de, ser::SerializeStruct};
use serde_json::{Value, json};

/// JSON-RPC request identifier: a number, a string, or null.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RpcId {
    /// Numeric id (JSON number constrained to a 64-bit signed integer).
    Number(i64),
    /// String id.
    String(String),
    /// Null id, used in error responses where the request's id couldn't
    /// be parsed.
    Null,
}

impl Serialize for RpcId {
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        match self {
            Self::Number(n) => s.serialize_i64(*n),
            Self::String(t) => s.serialize_str(t),
            Self::Null => s.serialize_none(),
        }
    }
}

impl<'de> Deserialize<'de> for RpcId {
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        let v = Value::deserialize(d)?;
        match v {
            Value::Number(n) => n
                .as_i64()
                .map(RpcId::Number)
                .ok_or_else(|| de::Error::custom("id is not a bounded integer")),
            Value::String(s) => Ok(Self::String(s)),
            Value::Null => Ok(Self::Null),
            _ => Err(de::Error::custom("id must be number, string, or null")),
        }
    }
}

/// A JSON-RPC 2.0 request or notification. `id` is `None` for
/// notifications.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Request {
    /// Always `"2.0"` for valid messages; the dispatcher rejects others
    /// with `invalid_request`.
    pub jsonrpc: String,
    /// Present on requests, absent on notifications.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<RpcId>,
    /// Method name (e.g. `"initialize"`, `"tools/list"`).
    pub method: String,
    /// Method-specific parameters; absent if the field was missing.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub params: Option<Value>,
}

/// A JSON-RPC 2.0 response. Exactly one of `result` or `error` is
/// populated; build via [`success_response`] / [`error_response`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Response {
    /// Always `"2.0"`.
    pub jsonrpc: String,
    /// Echoes the request's `id` (or `RpcId::Null` for parse errors
    /// where we couldn't recover an id).
    pub id: Option<RpcId>,
    /// Success payload (mutually exclusive with `error`).
    pub result: Option<Value>,
    /// Failure payload (mutually exclusive with `result`).
    pub error: Option<RpcError>,
}

impl Serialize for Response {
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        // jsonrpc + id always emitted; one of result/error is emitted
        // based on which is populated.
        let mut field_count = 2;
        if self.result.is_some() {
            field_count += 1;
        }
        if self.error.is_some() {
            field_count += 1;
        }
        let mut st = s.serialize_struct("Response", field_count)?;
        st.serialize_field("jsonrpc", &self.jsonrpc)?;
        // Always include id, even when None (serializes as null).
        match &self.id {
            Some(rid) => st.serialize_field("id", rid)?,
            None => st.serialize_field("id", &Value::Null)?,
        }
        if let Some(r) = &self.result {
            st.serialize_field("result", r)?;
        }
        if let Some(e) = &self.error {
            st.serialize_field("error", e)?;
        }
        st.end()
    }
}

/// A JSON-RPC error object.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RpcError {
    /// JSON-RPC error code (see `parse_error`, `invalid_request`, …).
    pub code: i64,
    /// Human-readable explanation.
    pub message: String,
    /// Optional structured detail.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub data: Option<Value>,
}

/// Build a success response.
#[must_use]
pub fn success_response(id: Option<RpcId>, value: Value) -> Response {
    Response {
        jsonrpc: "2.0".into(),
        id,
        result: Some(value),
        error: None,
    }
}

/// Build an error response.
#[must_use]
pub fn error_response(id: Option<RpcId>, err: RpcError) -> Response {
    Response {
        jsonrpc: "2.0".into(),
        id,
        result: None,
        error: Some(err),
    }
}

// ── Standard error codes (JSON-RPC 2.0 §5.1) ──────────────────────────

/// `-32700` Parse error.
#[must_use]
pub fn parse_error(msg: &str) -> RpcError {
    RpcError {
        code: -32700,
        message: format!("Parse error: {msg}"),
        data: None,
    }
}

/// `-32600` Invalid Request.
#[must_use]
pub fn invalid_request(msg: &str) -> RpcError {
    RpcError {
        code: -32600,
        message: format!("Invalid Request: {msg}"),
        data: None,
    }
}

/// `-32601` Method not found.
#[must_use]
pub fn method_not_found(name: &str) -> RpcError {
    RpcError {
        code: -32601,
        message: format!("Method not found: {name}"),
        data: None,
    }
}

/// `-32602` Invalid params.
#[must_use]
pub fn invalid_params(msg: &str) -> RpcError {
    RpcError {
        code: -32602,
        message: format!("Invalid params: {msg}"),
        data: None,
    }
}

/// `-32603` Internal error.
#[must_use]
pub fn internal_error(msg: &str) -> RpcError {
    RpcError {
        code: -32603,
        message: format!("Internal error: {msg}"),
        data: None,
    }
}

/// Encode a JSON-RPC message to a single-line `String` (no trailing
/// newline; the caller appends it for the stdio transport).
///
/// # Errors
///
/// Returns `serde_json::Error` if the value cannot be encoded.
pub fn encode_message<T: Serialize>(v: &T) -> Result<String, serde_json::Error> {
    serde_json::to_string(v)
}

/// Parse a single JSON-RPC request line. The error is the underlying
/// `serde_json` parse error, which the caller merely displays inside a
/// [`parse_error`] or [`invalid_request`] response.
///
/// # Errors
///
/// Returns `serde_json::Error` on malformed input.
pub fn decode_request(line: &str) -> Result<Request, serde_json::Error> {
    serde_json::from_str::<Request>(line)
}

/// Convenience: build a `Value` from any serializable. Used by the
/// builtins module.
#[must_use]
pub fn to_value<T: Serialize>(t: &T) -> Value {
    serde_json::to_value(t).unwrap_or(json!(null))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn mcp_protocol_rpc_id_roundtrips() {
        for id in [RpcId::Number(42), RpcId::String("abc".into()), RpcId::Null] {
            let s = serde_json::to_string(&id).unwrap();
            let parsed: RpcId = serde_json::from_str(&s).unwrap();
            assert_eq!(parsed, id);
        }
    }

    #[test]
    fn mcp_protocol_request_decodes_with_and_without_id() {
        let r: Request = serde_json::from_str(r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#).unwrap();
        assert_eq!(r.id, Some(RpcId::Number(1)));
        let n: Request = serde_json::from_str(r#"{"jsonrpc":"2.0","method":"y"}"#).unwrap();
        assert!(n.id.is_none());
    }

    #[test]
    fn mcp_protocol_error_codes_match_spec() {
        assert_eq!(parse_error("x").code, -32700);
        assert_eq!(invalid_request("x").code, -32600);
        assert_eq!(method_not_found("x").code, -32601);
        assert_eq!(invalid_params("x").code, -32602);
        assert_eq!(internal_error("x").code, -32603);
    }

    #[test]
    fn mcp_protocol_response_emits_id_and_result_only_on_success() {
        let r = success_response(Some(RpcId::Number(7)), json!({"ok": true}));
        let s = serde_json::to_string(&r).unwrap();
        assert!(s.contains(r#""jsonrpc":"2.0""#));
        assert!(s.contains(r#""id":7"#));
        assert!(s.contains(r#""result":{"ok":true}"#));
        assert!(!s.contains(r#""error""#));
    }

    #[test]
    fn mcp_protocol_response_emits_id_and_error_only_on_failure() {
        let r = error_response(Some(RpcId::String("a".into())), invalid_params("nope"));
        let s = serde_json::to_string(&r).unwrap();
        assert!(s.contains(r#""error""#));
        assert!(s.contains(r#""code":-32602"#));
        assert!(!s.contains(r#""result""#));
    }

    #[test]
    fn mcp_protocol_null_id_serializes_as_json_null() {
        let r = error_response(Some(RpcId::Null), parse_error("bad"));
        let s = serde_json::to_string(&r).unwrap();
        assert!(s.contains(r#""id":null"#));
    }
}