use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use mnem_backend_redb::open_or_init;
use mnem_core::repo::ReadonlyRepo;
use mnem_core::store::{Blockstore, OpHeadsStore};
use serde_json::{Value, json};
use crate::protocol::{MCP_PROTOCOL_VERSION, Request, Response, error_code};
use crate::tools;
pub struct Server {
repo_path: PathBuf,
bs: Option<Arc<dyn Blockstore>>,
ohs: Option<Arc<dyn OpHeadsStore>>,
pub allow_labels: bool,
}
impl Server {
pub fn new(repo_path: PathBuf) -> Self {
Self {
repo_path,
bs: None,
ohs: None,
allow_labels: Self::resolve_allow_labels_from_env(),
}
}
#[must_use]
pub(crate) fn resolve_allow_labels_from_env() -> bool {
Self::resolve_allow_labels_with(|k| std::env::var(k).ok())
}
#[must_use]
pub(crate) fn resolve_allow_labels_with<F>(get: F) -> bool
where
F: Fn(&str) -> Option<String>,
{
if let Some(v) = get("MNEM_LABELS") {
return Self::parse_truthy_env(Some(&v));
}
if let Some(v) = get("MNEM_BENCH") {
return Self::parse_truthy_env(Some(&v));
}
true
}
#[must_use]
pub(crate) fn parse_truthy_env(val: Option<&str>) -> bool {
match val {
None => false,
Some(s) => {
let t = s.trim();
if t.is_empty() {
return false;
}
let l = t.to_ascii_lowercase();
!matches!(l.as_str(), "0" | "false" | "no" | "off")
}
}
}
#[cfg(feature = "summarize")]
pub(crate) fn repo_path(&self) -> &std::path::Path {
&self.repo_path
}
fn ensure_stores(&mut self) -> anyhow::Result<()> {
if self.bs.is_some() {
return Ok(());
}
std::fs::create_dir_all(&self.repo_path)?;
let redb_path = self.repo_path.join("repo.redb");
let (bs, ohs, _file) = open_or_init(&redb_path)?;
self.bs = Some(bs);
self.ohs = Some(ohs);
Ok(())
}
pub(crate) fn load_repo(&mut self) -> anyhow::Result<ReadonlyRepo> {
self.ensure_stores()?;
let bs = self.bs.as_ref().unwrap().clone();
let ohs = self.ohs.as_ref().unwrap().clone();
match ReadonlyRepo::open(bs.clone(), ohs.clone()) {
Ok(r) => Ok(r),
Err(e) if e.is_uninitialized() => ReadonlyRepo::init(bs, ohs).map_err(Into::into),
Err(e) => Err(e.into()),
}
}
pub(crate) fn stores(
&mut self,
) -> anyhow::Result<(Arc<dyn Blockstore>, Arc<dyn OpHeadsStore>)> {
self.ensure_stores()?;
Ok((
self.bs.as_ref().unwrap().clone(),
self.ohs.as_ref().unwrap().clone(),
))
}
pub fn handle_line(&mut self, line: &str) -> Option<String> {
let req: Request = match serde_json::from_str(line) {
Ok(r) => r,
Err(e) => {
let resp =
Response::err(None, error_code::PARSE_ERROR, format!("parse error: {e}"));
return Some(serde_json::to_string(&resp).unwrap());
}
};
if req.jsonrpc != "2.0" {
let resp = Response::err(
req.id.clone(),
error_code::INVALID_REQUEST,
format!(
"invalid jsonrpc field: expected '2.0', got {:?}",
req.jsonrpc
),
);
return Some(serde_json::to_string(&resp).unwrap());
}
let is_notification = req.id.is_none();
let id = req.id.clone();
let resp = match req.method.as_str() {
"initialize" => Self::handle_initialize(id),
"tools/list" => self.handle_tools_list(id),
"tools/call" => self.handle_tools_call(id, req.params),
"ping" => Response::ok(id, json!({})),
"notifications/initialized" | "notifications/cancelled" => return None,
other => Response::err(
id,
error_code::METHOD_NOT_FOUND,
format!("method not found: {other}"),
),
};
if is_notification {
None
} else {
Some(serde_json::to_string(&resp).unwrap())
}
}
fn handle_initialize(id: Option<Value>) -> Response {
Response::ok(
id,
json!({
"protocolVersion": MCP_PROTOCOL_VERSION,
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "mnem-mcp",
"version": env!("CARGO_PKG_VERSION"),
}
}),
)
}
fn handle_tools_list(&self, id: Option<Value>) -> Response {
Response::ok(id, json!({ "tools": tools::all_tools(self.allow_labels) }))
}
fn handle_tools_call(&mut self, id: Option<Value>, params: Value) -> Response {
let Some(name) = params.get("name").and_then(Value::as_str) else {
return Response::err(
id,
error_code::INVALID_PARAMS,
"tools/call: `name` field is missing or not a string",
);
};
let name = name.to_string();
let args = params.get("arguments").cloned().unwrap_or(Value::Null);
let start = Instant::now();
let outcome = tools::dispatch(self, &name, args);
let latency_micros = start.elapsed().as_micros() as u64;
let text = match outcome {
Ok(v) => v,
Err(e) => {
let err_text = format!("mnem_mcp tool error: {e}");
let bytes = err_text.len();
let tokens_estimate = bytes / 4;
return Response::ok(
id,
json!({
"content": [{ "type": "text", "text": err_text }],
"isError": true,
"_meta": {
"bytes": bytes,
"latency_micros": latency_micros,
"tokens_estimate": tokens_estimate
}
}),
);
}
};
let bytes = text.len();
let tokens_estimate = bytes / 4;
Response::ok(
id,
json!({
"content": [{ "type": "text", "text": text }],
"_meta": {
"bytes": bytes,
"latency_micros": latency_micros,
"tokens_estimate": tokens_estimate
}
}),
)
}
}
#[cfg(test)]
mod env_parse_tests {
use super::Server;
#[test]
fn unset_value_parses_false() {
assert!(!Server::parse_truthy_env(None));
}
#[test]
fn falsy_strings_parse_false() {
for v in [
"", "0", "false", "FALSE", "False", "no", "No", "NO", "off", "Off", "OFF", " ", " 0 ",
] {
assert!(
!Server::parse_truthy_env(Some(v)),
"expected `{v:?}` to parse false"
);
}
}
#[test]
fn truthy_strings_parse_true() {
for v in ["1", "true", "yes", "on", "YES", "benchmark", "anything"] {
assert!(
Server::parse_truthy_env(Some(v)),
"expected `{v:?}` to parse true"
);
}
}
#[test]
fn resolve_allow_labels_default_is_true_when_both_unset() {
assert!(Server::resolve_allow_labels_with(|_| None));
}
#[test]
fn resolve_allow_labels_honours_explicit_off_via_mnem_labels() {
let off = Server::resolve_allow_labels_with(|k| match k {
"MNEM_LABELS" => Some("0".into()),
_ => None,
});
assert!(!off);
}
#[test]
fn resolve_allow_labels_honours_legacy_mnem_bench_off() {
let off = Server::resolve_allow_labels_with(|k| match k {
"MNEM_BENCH" => Some("0".into()),
_ => None,
});
assert!(!off);
}
#[test]
fn mnem_labels_takes_precedence_over_mnem_bench() {
let on = Server::resolve_allow_labels_with(|k| match k {
"MNEM_LABELS" => Some("1".into()),
"MNEM_BENCH" => Some("0".into()),
_ => None,
});
assert!(on);
let off = Server::resolve_allow_labels_with(|k| match k {
"MNEM_LABELS" => Some("0".into()),
"MNEM_BENCH" => Some("1".into()),
_ => None,
});
assert!(!off);
}
#[test]
fn legacy_mnem_bench_alone_still_enables() {
let on = Server::resolve_allow_labels_with(|k| match k {
"MNEM_BENCH" => Some("1".into()),
_ => None,
});
assert!(on);
}
}