haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use serde::{Deserialize, Serialize, de::DeserializeOwned};

use super::{Sender, ServiceRefusal, ToolResult, result};
use crate::mcp::types;
use crate::serve::protocol;

#[derive(Deserialize, Serialize)]
pub(super) struct Snapshot {
    pub(super) branch: String,
    pub(super) version: types::Version,
}

impl Snapshot {
    pub(super) fn selection(&self) -> protocol::Selection {
        protocol::Selection {
            branch: self.branch.clone(),
            version: Some(self.version.0),
        }
    }

    pub(super) fn output(self) -> types::Snapshot {
        types::Snapshot {
            branch: self.branch,
            version: self.version,
        }
    }

    pub(super) fn require(&self, other: &Self) -> Result<(), ServiceRefusal> {
        if self.branch == other.branch && self.version.0 == other.version.0 {
            Ok(())
        } else {
            Err(failure(
                "mcp_service_snapshot_mismatch",
                "The served pages do not identify the same captured version",
            ))
        }
    }

    pub(super) fn requested(&self, asked: &types::Selection) -> Result<(), ServiceRefusal> {
        if self.branch == asked.branch
            && asked
                .version
                .is_none_or(|version| version.0 == self.version.0)
        {
            Ok(())
        } else {
            Err(failure(
                "mcp_service_snapshot_mismatch",
                "The service did not answer the requested captured version",
            ))
        }
    }
}

pub(super) fn failure(code: &str, message: impl Into<String>) -> ServiceRefusal {
    ServiceRefusal {
        code: code.to_owned(),
        message: message.into(),
        receipt: None,
    }
}

pub(super) fn call<T: DeserializeOwned>(
    sender: &mut impl Sender,
    operation: protocol::Operation,
) -> Result<T, ServiceRefusal> {
    let raw = sender.send(operation)?;
    serde_json::from_str(raw.get())
        .map_err(|error| failure("mcp_service_answer_invalid", error.to_string()))
}

pub(super) fn bounded(body: &impl Serialize, bytes: u64) -> Result<ToolResult, ServiceRefusal> {
    let output = result(body, false)
        .map_err(|error| failure("mcp_result_encoding_failed", error.to_string()))?;
    let size = u64::try_from(output.structured_content.get().len()).map_err(|_| {
        failure(
            "mcp_page_bytes_unrepresentable",
            "The answer length is not representable",
        )
    })?;
    if size > bytes {
        return Err(failure(
            "mcp_page_bytes_exceeded",
            "The complete answer exceeds the requested byte limit; increase it or reduce rows",
        ));
    }
    Ok(output)
}