use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use car_ir::ActionProposal;
use car_memgine::note_store::Note;
use car_memgine::MemgineEngine;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tokio::sync::Mutex;
use crate::error_codes::{
INTERNAL as E_INTERNAL, INVALID_PARAMS as E_INVALID_PARAMS,
INVALID_REQUEST as E_INVALID_REQUEST, METHOD_NOT_FOUND as E_METHOD_NOT_FOUND,
};
use crate::schemas::{cached_prompt_schemas, cached_tool_schemas};
use crate::{PROTOCOL_VERSION, SERVER_NAME, SUPPORTED_VERSIONS};
#[derive(Debug, Deserialize)]
pub struct Request {
pub jsonrpc: String,
#[serde(default)]
pub id: Option<Value>,
pub method: String,
#[serde(default)]
pub params: Value,
}
#[derive(Debug, Serialize)]
pub struct Response {
pub jsonrpc: &'static str,
pub id: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorObj>,
}
#[derive(Debug, Serialize)]
pub struct ErrorObj {
pub code: i32,
pub message: String,
}
pub fn ok(id: Value, result: Value) -> Response {
Response {
jsonrpc: "2.0",
id,
result: Some(result),
error: None,
}
}
pub fn err(id: Value, code: i32, message: impl Into<String>) -> Response {
Response {
jsonrpc: "2.0",
id,
result: None,
error: Some(ErrorObj {
code,
message: message.into(),
}),
}
}
#[derive(Debug)]
pub enum ToolError {
InvalidParams(String),
Internal(String),
UnknownTool(String),
}
impl ToolError {
pub fn code(&self) -> i32 {
match self {
ToolError::InvalidParams(_) => E_INVALID_PARAMS,
ToolError::Internal(_) => E_INTERNAL,
ToolError::UnknownTool(_) => E_METHOD_NOT_FOUND,
}
}
pub fn message(&self) -> &str {
match self {
ToolError::InvalidParams(m) | ToolError::Internal(m) | ToolError::UnknownTool(m) => m,
}
}
pub fn is_execution_error(&self) -> bool {
match self {
ToolError::Internal(_) => true,
ToolError::InvalidParams(_) | ToolError::UnknownTool(_) => false,
}
}
}
fn tool_execution_error(message: &str) -> Value {
json!({
"content": [{ "type": "text", "text": message }],
"isError": true,
})
}
fn missing(field: &str) -> ToolError {
ToolError::InvalidParams(format!("missing {}", field))
}
fn from_tool_value<T: serde::de::DeserializeOwned>(
value: &Value,
label: &str,
) -> Result<T, ToolError> {
serde_json::from_value(value.clone())
.map_err(|e| ToolError::InvalidParams(format!("{label}: {e}")))
}
fn to_json_text<T: Serialize>(value: &T) -> Result<String, ToolError> {
serde_json::to_string(value).map_err(|e| ToolError::Internal(e.to_string()))
}
pub(crate) const MAX_TEST_STATES: usize = 256;
const RESOURCE_PAGE_SIZE: usize = 100;
const MAX_COMPLETION_VALUES: usize = 100;
const CONTEXT_MODES: &[&str] = &["full", "fast"];
fn encode_cursor(uri: &str) -> String {
let mut out = String::with_capacity((uri.len() + 3) * 2);
for b in format!("v1:{}", uri).bytes() {
out.push_str(&format!("{:02x}", b));
}
out
}
fn decode_cursor(cursor: &str) -> Result<String, ToolError> {
let bad = || ToolError::InvalidParams(format!("invalid cursor: {}", cursor));
if !cursor.len().is_multiple_of(2) {
return Err(bad());
}
let mut bytes = Vec::with_capacity(cursor.len() / 2);
for pair in cursor.as_bytes().chunks(2) {
let hex = std::str::from_utf8(pair).map_err(|_| bad())?;
bytes.push(u8::from_str_radix(hex, 16).map_err(|_| bad())?);
}
let decoded = String::from_utf8(bytes).map_err(|_| bad())?;
decoded
.strip_prefix("v1:")
.map(str::to_string)
.ok_or_else(bad)
}
#[derive(Debug, PartialEq)]
enum Notification {
Initialized,
Cancelled {
request_id: Option<Value>,
reason: Option<String>,
},
Progress { token: Option<Value> },
RootsListChanged,
Unknown,
}
fn classify_notification(method: &str, params: &Value) -> Notification {
match method {
"notifications/initialized" | "initialized" => Notification::Initialized,
"notifications/cancelled" => Notification::Cancelled {
request_id: params.get("requestId").cloned(),
reason: params
.get("reason")
.and_then(|v| v.as_str())
.map(str::to_string),
},
"notifications/progress" => Notification::Progress {
token: params.get("progressToken").cloned(),
},
"notifications/roots/list_changed" => Notification::RootsListChanged,
_ => Notification::Unknown,
}
}
fn negotiate_version_in(supported: &[&'static str], params: &Value) -> &'static str {
params
.get("protocolVersion")
.and_then(Value::as_str)
.and_then(|want| supported.iter().copied().find(|v| *v == want))
.unwrap_or(PROTOCOL_VERSION)
}
fn negotiate_version(params: &Value) -> &'static str {
negotiate_version_in(SUPPORTED_VERSIONS, params)
}
#[async_trait::async_trait]
pub trait ToolHandler: Send + Sync + 'static {
async fn call(&self, args: Value) -> Result<String, ToolError>;
}
#[derive(Debug)]
pub enum RegisterError {
MissingName,
MissingAnnotations(String),
CollidesWithBuiltIn(String),
AlreadyRegistered(String),
}
impl std::fmt::Display for RegisterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RegisterError::MissingName => write!(f, "tool schema has no \"name\""),
RegisterError::MissingAnnotations(name) => write!(
f,
"tool schema for {name} must carry all four annotation hints \
(readOnlyHint, destructiveHint, idempotentHint, openWorldHint)"
),
RegisterError::CollidesWithBuiltIn(name) => {
write!(f, "{name} is a built-in tool and cannot be replaced")
}
RegisterError::AlreadyRegistered(name) => {
write!(f, "{name} is already registered on this server")
}
}
}
}
impl std::error::Error for RegisterError {}
const REQUIRED_ANNOTATIONS: [&str; 4] = [
"readOnlyHint",
"destructiveHint",
"idempotentHint",
"openWorldHint",
];
pub struct Server {
memgine: Arc<Mutex<MemgineEngine>>,
notes: Mutex<Vec<Note>>,
store: Option<PathBuf>,
handlers: HashMap<String, Arc<dyn ToolHandler>>,
advertised: Option<Vec<Value>>,
}
impl Default for Server {
fn default() -> Self {
Self::new()
}
}
impl Server {
pub fn new() -> Self {
Self::with_memgine(Arc::new(Mutex::new(MemgineEngine::new(None))))
}
pub fn with_store(path: PathBuf) -> Result<Self, String> {
let notes = car_memgine::note_store::load_checked(&path)?;
let mut engine = MemgineEngine::new(None);
car_memgine::note_store::ingest_all(&mut engine, ¬es);
tracing::info!(
path = %path.display(),
notes = notes.len(),
"MCP server opened the durable note store"
);
Ok(Self {
memgine: Arc::new(Mutex::new(engine)),
notes: Mutex::new(notes),
store: Some(path),
handlers: HashMap::new(),
advertised: None,
})
}
pub fn with_memgine(memgine: Arc<Mutex<MemgineEngine>>) -> Self {
Self {
memgine,
notes: Mutex::new(Vec::new()),
store: None,
handlers: HashMap::new(),
advertised: None,
}
}
pub fn register_tool(
&mut self,
schema: Value,
handler: Arc<dyn ToolHandler>,
) -> Result<(), RegisterError> {
let name = schema
.get("name")
.and_then(|v| v.as_str())
.ok_or(RegisterError::MissingName)?
.to_string();
if cached_tool_schemas()
.iter()
.any(|t| t["name"].as_str() == Some(name.as_str()))
{
return Err(RegisterError::CollidesWithBuiltIn(name));
}
if self.handlers.contains_key(&name) {
return Err(RegisterError::AlreadyRegistered(name));
}
let classified = schema
.get("annotations")
.and_then(|a| a.as_object())
.is_some_and(|a| {
REQUIRED_ANNOTATIONS
.iter()
.all(|hint| a.get(*hint).is_some_and(Value::is_boolean))
});
if !classified {
return Err(RegisterError::MissingAnnotations(name));
}
self.advertised
.get_or_insert_with(|| cached_tool_schemas().clone())
.push(schema);
self.handlers.insert(name, handler);
Ok(())
}
fn advertised_tools(&self) -> &[Value] {
self.advertised
.as_deref()
.unwrap_or_else(|| cached_tool_schemas().as_slice())
}
pub async fn handle(&self, req: Request) -> Option<Response> {
let id = match req.id.clone() {
Some(id) => id,
None => {
match classify_notification(&req.method, &req.params) {
Notification::Initialized => {
tracing::debug!("client completed the initialize handshake");
}
Notification::Cancelled { request_id, reason } => {
tracing::debug!(
request_id = ?request_id,
reason = ?reason,
"cancellation for an already-completed request; nothing to stop"
);
}
Notification::Progress { token } => {
tracing::debug!(token = ?token, "client progress notification");
}
Notification::RootsListChanged => {
tracing::debug!("client roots list changed");
}
Notification::Unknown => {
tracing::debug!(method = %req.method, "unrecognized notification");
}
}
return None;
}
};
if req.jsonrpc != "2.0" {
return Some(err(id, E_INVALID_REQUEST, "jsonrpc must be \"2.0\""));
}
match req.method.as_str() {
"initialize" => Some(ok(
id,
json!({
"protocolVersion": negotiate_version(&req.params),
"capabilities": {
"tools": {},
"resources": { "subscribe": false, "listChanged": false },
"prompts": { "listChanged": false },
"completions": {},
},
"serverInfo": { "name": SERVER_NAME, "version": env!("CARGO_PKG_VERSION") },
}),
)),
"ping" => Some(ok(id, json!({}))),
"tools/list" => Some(ok(id, json!({ "tools": self.advertised_tools() }))),
"tools/call" => Some(match self.tools_call(&req.params).await {
Ok(v) => ok(id, v),
Err(e) if e.is_execution_error() => ok(id, tool_execution_error(e.message())),
Err(e) => err(id, e.code(), e.message()),
}),
"resources/list" => Some(match self.resources_list(&req.params).await {
Ok(v) => ok(id, v),
Err(e) => err(id, e.code(), e.message()),
}),
"resources/read" => Some(match self.resources_read(&req.params).await {
Ok(v) => ok(id, v),
Err(e) => err(id, e.code(), e.message()),
}),
"prompts/list" => Some(ok(id, json!({ "prompts": cached_prompt_schemas() }))),
"prompts/get" => Some(match self.prompts_get(&req.params).await {
Ok(v) => ok(id, v),
Err(e) => err(id, e.code(), e.message()),
}),
"completion/complete" => Some(match self.completion_complete(&req.params).await {
Ok(v) => ok(id, v),
Err(e) => err(id, e.code(), e.message()),
}),
other => Some(err(
id,
E_METHOD_NOT_FOUND,
format!("method not found: {}", other),
)),
}
}
async fn tools_call(&self, params: &Value) -> Result<Value, ToolError> {
let name = params
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("name"))?;
let args = params.get("arguments").cloned().unwrap_or(Value::Null);
let text = match name {
"memory_add_fact" => self.tool_add_fact(&args).await?,
"memory_query" => self.tool_query(&args).await?,
"memory_update_status" => self.tool_memory_update_status(&args).await?,
"memory_save_knowledge" => self.tool_memory_save_knowledge(&args).await?,
"memory_save_procedural" => self.tool_memory_save_procedural(&args).await?,
"memory_delete" => self.tool_memory_delete(&args).await?,
"memory_intervene" => self.tool_memory_intervene(&args).await?,
"memory_evaluate" => self.tool_memory_evaluate(&args).await?,
"skill_find" => self.tool_skill_find(&args).await?,
"skill_ingest" => self.tool_skill_ingest(&args).await?,
"skill_list" => self.tool_skill_list(&args).await?,
"verify" => self.tool_verify(&args)?,
"simulate" => self.tool_simulate(&args)?,
"equivalent" => self.tool_equivalent(&args)?,
"optimize" => self.tool_optimize(&args)?,
"policy_check" => self.tool_policy_check(&args)?,
other => match self.handlers.get(other) {
Some(handler) => handler.call(args).await?,
None => return Err(ToolError::UnknownTool(format!("unknown tool: {}", other))),
},
};
Ok(json!({
"content": [{ "type": "text", "text": text }],
"isError": false,
}))
}
async fn tool_add_fact(&self, args: &Value) -> Result<String, ToolError> {
let subject = args
.get("subject")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("subject"))?;
let body = args
.get("body")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("body"))?;
let kind = car_memgine::note_store::NoteKind::from_constraint_dialect(
args.get("kind").and_then(|v| v.as_str()),
);
let note = Note {
subject: subject.to_string(),
body: body.to_string(),
kind,
};
let mut notes = self.notes.lock().await;
if let Some(path) = &self.store {
let on_disk = car_memgine::note_store::load(path);
if on_disk.len() > notes.len() {
let mut engine = self.memgine.lock().await;
for (idx, n) in on_disk.iter().enumerate().skip(notes.len()) {
car_memgine::note_store::ingest(&mut engine, idx, n);
}
*notes = on_disk;
}
}
let idx = notes.len();
notes.push(note.clone());
if let Some(path) = &self.store {
if let Err(e) = car_memgine::note_store::save(path, ¬es) {
notes.pop();
return Err(ToolError::Internal(format!(
"fact not remembered — could not write the memory store: {e}"
)));
}
}
let mut engine = self.memgine.lock().await;
car_memgine::note_store::ingest(&mut engine, idx, ¬e);
Ok(format!(
"fact remembered id=assistant-note-{} total={} durable={}",
idx,
engine.valid_fact_count(),
self.store.is_some()
))
}
fn tool_policy_check(&self, args: &Value) -> Result<String, ToolError> {
let tool = args
.get("tool")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("tool"))?;
let params = args.get("params").cloned().unwrap_or_else(|| json!({}));
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
to_json_text(&car_policy::tool_gate::check(tool, ¶ms, &cwd))
}
async fn tool_query(&self, args: &Value) -> Result<String, ToolError> {
let query = args
.get("query")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("query"))?;
let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
let engine = self.memgine.lock().await;
let seeds = engine.graph.find_seeds(query, 5);
let hits = if !seeds.is_empty() {
engine.graph.retrieve(&seeds, 3, k, 0.6, 0.05)
} else {
vec![]
};
let out: Vec<Value> = hits
.iter()
.filter_map(|hit| {
let node = engine.graph.inner.node_weight(hit.node_ix)?;
Some(json!({
"subject": node.key,
"body": node.value,
"activation": hit.activation,
}))
})
.collect();
serde_json::to_string(&out).map_err(|e| ToolError::Internal(e.to_string()))
}
async fn tool_memory_update_status(&self, args: &Value) -> Result<String, ToolError> {
let body = args
.get("body")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("body"))?;
let tenant_id = args
.get("tenant_id")
.and_then(|v| v.as_str())
.map(str::to_string);
let mut engine = self.memgine.lock().await;
let status = engine.update_proactive_status(body, tenant_id);
to_json_text(&status)
}
async fn tool_memory_save_knowledge(&self, args: &Value) -> Result<String, ToolError> {
let save: car_memgine::ProactiveMemorySave =
from_tool_value(args, "memory_save_knowledge")?;
let mut engine = self.memgine.lock().await;
let saved = engine.save_proactive_knowledge(save);
to_json_text(&saved)
}
async fn tool_memory_save_procedural(&self, args: &Value) -> Result<String, ToolError> {
let save: car_memgine::ProactiveMemorySave =
from_tool_value(args, "memory_save_procedural")?;
let mut engine = self.memgine.lock().await;
let saved = engine.save_proactive_procedural(save);
to_json_text(&saved)
}
async fn tool_memory_delete(&self, args: &Value) -> Result<String, ToolError> {
let id = args
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("id"))?;
let mut engine = self.memgine.lock().await;
let deleted = engine.delete_proactive_memory(id);
to_json_text(&deleted)
}
async fn tool_memory_intervene(&self, args: &Value) -> Result<String, ToolError> {
let request: car_memgine::ProactiveMemoryRequest =
from_tool_value(args, "memory_intervene")?;
let mut engine = self.memgine.lock().await;
let decision = engine.proactive_intervention(&request);
to_json_text(&decision)
}
async fn tool_memory_evaluate(&self, args: &Value) -> Result<String, ToolError> {
let request: car_memgine::ProactiveEvaluationRequest =
from_tool_value(args, "memory_evaluate")?;
let engine = self.memgine.lock().await;
let report = engine.evaluate_proactive_memory(&request);
to_json_text(&report)
}
async fn tool_skill_find(&self, args: &Value) -> Result<String, ToolError> {
let persona = args.get("persona").and_then(|v| v.as_str()).unwrap_or("");
let url = args.get("url").and_then(|v| v.as_str()).unwrap_or("");
let task = args
.get("task")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("task"))?;
let k = args.get("k").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
let engine = self.memgine.lock().await;
let results = engine.find_skill(persona, url, task, k);
let out: Vec<Value> = results
.iter()
.map(|(meta, score)| json!({ "skill": meta, "score": score }))
.collect();
serde_json::to_string(&out).map_err(|e| ToolError::Internal(e.to_string()))
}
async fn tool_skill_ingest(&self, args: &Value) -> Result<String, ToolError> {
let name = args
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("name"))?;
let code = args
.get("code")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("code"))?;
let platform = args
.get("platform")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let persona = args.get("persona").and_then(|v| v.as_str()).unwrap_or("");
let url_pattern = args
.get("url_pattern")
.and_then(|v| v.as_str())
.unwrap_or("");
let description = args
.get("description")
.and_then(|v| v.as_str())
.unwrap_or("");
let supersedes = args.get("supersedes").and_then(|v| v.as_str());
let keywords: Vec<String> = args
.get("task_keywords")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let trigger = car_memgine::SkillTrigger {
persona: persona.into(),
url_pattern: url_pattern.into(),
task_keywords: keywords,
structured: None,
};
let mut engine = self.memgine.lock().await;
engine.ingest_skill(
name,
code,
platform,
trigger,
description,
supersedes,
vec![],
vec![],
);
Ok(format!("skill ingested: {}", name))
}
async fn tool_skill_list(&self, args: &Value) -> Result<String, ToolError> {
let domain = args.get("domain").and_then(|v| v.as_str());
let engine = self.memgine.lock().await;
let skills: Vec<Value> = engine
.graph
.inner
.node_indices()
.filter_map(|nix| {
let node = engine.graph.inner.node_weight(nix)?;
if node.kind != car_memgine::MemKind::Skill {
return None;
}
let meta = car_memgine::SkillMeta::from_node(node)?;
if let Some(d) = domain {
match &meta.scope {
car_memgine::SkillScope::Global => {}
car_memgine::SkillScope::Domain(sd) if sd == d => {}
_ => return None,
}
}
serde_json::to_value(&meta).ok()
})
.collect();
serde_json::to_string(&skills).map_err(|e| ToolError::Internal(e.to_string()))
}
async fn collect_resources(&self) -> Vec<(String, Value)> {
let engine = self.memgine.lock().await;
let mut all: Vec<(String, Value)> = Vec::new();
for nix in engine.graph.inner.node_indices() {
let Some(node) = engine.graph.inner.node_weight(nix) else {
continue;
};
match node.kind {
car_memgine::MemKind::Fact => {
let Some(fid) = node.fact_id.as_deref() else {
continue;
};
let uri = format!("car://memory/fact/{}", fid);
all.push((
uri.clone(),
json!({
"uri": uri,
"name": node.key,
"description": if node.is_constraint { "CAR constraint" } else { "CAR fact" },
"mimeType": "text/plain",
}),
));
}
car_memgine::MemKind::Skill => {
let uri = format!("car://memory/skill/{}", node.key);
all.push((
uri.clone(),
json!({
"uri": uri,
"name": node.key,
"description": "CAR skill",
"mimeType": "application/json",
}),
));
}
_ => {}
}
}
drop(engine);
all.sort_unstable_by(|a, b| a.0.cmp(&b.0));
all.dedup_by(|a, b| a.0 == b.0);
all
}
async fn resources_list(&self, params: &Value) -> Result<Value, ToolError> {
let after = match params.get("cursor") {
Some(Value::Null) | None => None,
Some(Value::String(c)) => Some(decode_cursor(c)?),
Some(_) => return Err(ToolError::InvalidParams("cursor must be a string".into())),
};
let all = self.collect_resources().await;
let start = match after.as_deref() {
Some(cursor) => all.partition_point(|(uri, _)| uri.as_str() <= cursor),
None => 0,
};
let end = start.saturating_add(RESOURCE_PAGE_SIZE).min(all.len());
let page: Vec<Value> = all[start..end].iter().map(|(_, v)| v.clone()).collect();
let next = if end < all.len() {
all.get(end - 1).map(|(uri, _)| encode_cursor(uri))
} else {
None
};
Ok(match next {
Some(c) => json!({ "resources": page, "nextCursor": c }),
None => json!({ "resources": page }),
})
}
async fn resources_read(&self, params: &Value) -> Result<Value, ToolError> {
let uri = params
.get("uri")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("uri"))?;
let engine = self.memgine.lock().await;
if let Some(fid) = uri.strip_prefix("car://memory/fact/") {
for nix in engine.graph.inner.node_indices() {
let Some(node) = engine.graph.inner.node_weight(nix) else {
continue;
};
if node.kind != car_memgine::MemKind::Fact {
continue;
}
if node.fact_id.as_deref() == Some(fid) {
return Ok(json!({
"contents": [{
"uri": uri,
"mimeType": "text/plain",
"text": format!("{}\n\n{}", node.key, node.value),
}],
}));
}
}
return Err(ToolError::InvalidParams(format!("fact not found: {}", fid)));
}
if let Some(name) = uri.strip_prefix("car://memory/skill/") {
for nix in engine.graph.inner.node_indices() {
let Some(node) = engine.graph.inner.node_weight(nix) else {
continue;
};
if node.kind != car_memgine::MemKind::Skill {
continue;
}
if node.key == name {
let meta = car_memgine::SkillMeta::from_node(node);
let body =
serde_json::to_string_pretty(&meta).unwrap_or_else(|_| node.value.clone());
return Ok(json!({
"contents": [{
"uri": uri,
"mimeType": "application/json",
"text": body,
}],
}));
}
}
return Err(ToolError::InvalidParams(format!(
"skill not found: {}",
name
)));
}
Err(ToolError::InvalidParams(format!(
"unsupported uri scheme: {}",
uri
)))
}
async fn prompts_get(&self, params: &Value) -> Result<Value, ToolError> {
let name = params
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("name"))?;
if name != "car_context" {
return Err(ToolError::InvalidParams(format!(
"unknown prompt: {}",
name
)));
}
let args = params.get("arguments").cloned().unwrap_or(Value::Null);
let query = args
.get("query")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("arguments.query"))?;
let mode = args.get("mode").and_then(|v| v.as_str()).unwrap_or("full");
let mut engine = self.memgine.lock().await;
let text = match mode {
"fast" => engine.build_context_fast(query),
"full" | "" => engine.build_context(query),
other => return Err(ToolError::InvalidParams(format!("unknown mode: {}", other))),
};
Ok(json!({
"description": "CAR four-layer context (identity → constraints → facts → conversation → environment → known-unknowns) assembled for the query.",
"messages": [{
"role": "user",
"content": { "type": "text", "text": text },
}],
}))
}
async fn completion_complete(&self, params: &Value) -> Result<Value, ToolError> {
let reference = match params.get("ref") {
Some(Value::Object(o)) => o,
Some(_) => return Err(ToolError::InvalidParams("ref must be an object".into())),
None => return Err(missing("ref")),
};
let argument = match params.get("argument") {
Some(Value::Object(o)) => o,
Some(_) => {
return Err(ToolError::InvalidParams(
"argument must be an object".into(),
))
}
None => return Err(missing("argument")),
};
let arg_name = argument
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| missing("argument.name"))?;
let value = argument.get("value").and_then(|v| v.as_str()).unwrap_or("");
let ref_type = reference.get("type").and_then(|v| v.as_str()).unwrap_or("");
let matches: Vec<String> = match ref_type {
"ref/prompt" => {
let prompt = reference.get("name").and_then(|v| v.as_str()).unwrap_or("");
match (prompt, arg_name) {
("car_context", "mode") => CONTEXT_MODES
.iter()
.filter(|m| m.starts_with(value))
.map(|m| (*m).to_string())
.collect(),
_ => Vec::new(),
}
}
"ref/resource" => {
let prefix = reference.get("uri").and_then(|v| v.as_str()).unwrap_or("");
self.collect_resources()
.await
.into_iter()
.map(|(uri, _)| uri)
.filter(|uri| uri.starts_with(prefix))
.collect()
}
_ => Vec::new(),
};
let total = matches.len();
let values: Vec<String> = matches.into_iter().take(MAX_COMPLETION_VALUES).collect();
let has_more = total > values.len();
Ok(json!({
"completion": {
"values": values,
"total": total,
"hasMore": has_more,
},
}))
}
fn tool_verify(&self, args: &Value) -> Result<String, ToolError> {
let proposal_val = args.get("proposal").ok_or_else(|| missing("proposal"))?;
let proposal: ActionProposal = serde_json::from_value(proposal_val.clone())
.map_err(|e| ToolError::InvalidParams(format!("proposal: {}", e)))?;
let max_actions = args
.get("max_actions")
.and_then(|v| v.as_u64())
.unwrap_or(30) as usize;
let result = car_verify::verify(&proposal, None, None, max_actions);
serde_json::to_string(&json!({
"valid": result.valid,
"issues": result.issues.iter().map(|i| json!({
"action_id": i.action_id,
"severity": i.severity,
"message": i.message,
"tier": i.tier.as_str(),
})).collect::<Vec<_>>(),
"simulated_state": result.simulated_state,
}))
.map_err(|e| ToolError::Internal(e.to_string()))
}
fn tool_simulate(&self, args: &Value) -> Result<String, ToolError> {
let proposal: ActionProposal = from_tool_value(
args.get("proposal").ok_or_else(|| missing("proposal"))?,
"proposal",
)?;
let initial_state: Option<HashMap<String, Value>> = match args.get("initial_state") {
None | Some(Value::Null) => None,
Some(v) => Some(from_tool_value(v, "initial_state")?),
};
to_json_text(&json!({
"final_state": car_verify::simulate(&proposal, initial_state.as_ref()),
}))
}
fn tool_equivalent(&self, args: &Value) -> Result<String, ToolError> {
let proposal_a: ActionProposal = from_tool_value(
args.get("proposal_a")
.ok_or_else(|| missing("proposal_a"))?,
"proposal_a",
)?;
let proposal_b: ActionProposal = from_tool_value(
args.get("proposal_b")
.ok_or_else(|| missing("proposal_b"))?,
"proposal_b",
)?;
let test_states: Option<Vec<HashMap<String, Value>>> = match args.get("test_states") {
None | Some(Value::Null) => None,
Some(v) => match from_tool_value::<Vec<HashMap<String, Value>>>(v, "test_states")? {
v if v.is_empty() => None,
v if v.len() > MAX_TEST_STATES => {
return Err(ToolError::InvalidParams(format!(
"test_states must hold at most {} states (got {})",
MAX_TEST_STATES,
v.len()
)));
}
v => Some(v),
},
};
let equivalent = car_verify::equivalent(&proposal_a, &proposal_b, test_states.as_deref());
to_json_text(&json!({
"equivalent": equivalent,
"tier": car_verify::EvidenceTier::Sampled.as_str(),
"states_tested": test_states.as_ref().map_or(2, Vec::len),
"used_default_states": test_states.is_none(),
}))
}
fn tool_optimize(&self, args: &Value) -> Result<String, ToolError> {
let proposal: ActionProposal = from_tool_value(
args.get("proposal").ok_or_else(|| missing("proposal"))?,
"proposal",
)?;
let optimized = car_verify::optimize(&proposal);
let pruned: Vec<Value> = proposal
.actions
.iter()
.zip(optimized.actions.iter())
.filter_map(|(before, after)| {
let removed: Vec<&String> = before
.state_dependencies
.iter()
.filter(|d| !after.state_dependencies.contains(d))
.collect();
(!removed.is_empty()).then(|| {
json!({
"action_id": before.id,
"removed": removed,
})
})
})
.collect();
to_json_text(&json!({
"proposal": optimized,
"pruned": pruned,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_request(method: &str, params: Value, id: u64) -> Request {
Request {
jsonrpc: "2.0".to_string(),
id: Some(json!(id)),
method: method.to_string(),
params,
}
}
async fn add_fact(server: &Server, subject: &str, body: &str, kind: Option<&str>) -> String {
let mut args = json!({ "subject": subject, "body": body });
if let Some(k) = kind {
args["kind"] = json!(k);
}
let resp = server
.handle(make_request(
"tools/call",
json!({ "name": "memory_add_fact", "arguments": args }),
1,
))
.await
.expect("response");
let result = resp.result.expect("tool result");
result["content"][0]["text"]
.as_str()
.expect("text")
.to_string()
}
async fn list_resources(server: &Server, cursor: Option<&str>) -> Response {
let params = match cursor {
Some(c) => json!({ "cursor": c }),
None => json!({}),
};
server
.handle(make_request("resources/list", params, 1))
.await
.expect("resources/list is a request, not a notification")
}
#[tokio::test]
async fn resources_list_paginates_and_the_cursor_resumes() {
let server = Server::new();
let total = 250;
for i in 0..total {
add_fact(&server, &format!("subject {:03}", i), "body", None).await;
}
let first = list_resources(&server, None).await;
let result = first.result.expect("page 1");
let page1 = result["resources"].as_array().expect("resources array");
assert_eq!(
page1.len(),
RESOURCE_PAGE_SIZE,
"page 1 must be capped at one page, not the whole graph"
);
let mut cursor = result["nextCursor"]
.as_str()
.expect("a full page must carry nextCursor")
.to_string();
let mut seen: Vec<String> = page1
.iter()
.map(|r| r["uri"].as_str().unwrap().to_string())
.collect();
let mut pages = 1;
loop {
let resp = list_resources(&server, Some(&cursor)).await;
let result = resp.result.expect("page result");
let page = result["resources"].as_array().expect("resources array");
pages += 1;
assert!(pages <= 10, "pagination did not terminate");
for r in page {
seen.push(r["uri"].as_str().unwrap().to_string());
}
match result.get("nextCursor") {
Some(c) => cursor = c.as_str().expect("cursor is a string").to_string(),
None => {
assert!(
page.len() < RESOURCE_PAGE_SIZE || seen.len() == total,
"a final page should not be a full page unless the total divides evenly"
);
break;
}
}
}
assert_eq!(
seen.len(),
total,
"the union of the pages lost or duplicated entries"
);
let mut sorted = seen.clone();
sorted.sort();
sorted.dedup();
assert_eq!(sorted.len(), total, "duplicate URIs across pages");
assert_eq!(
seen, sorted,
"pages must arrive in the total URI order the cursor is defined against"
);
}
#[tokio::test]
async fn resources_list_rejects_a_malformed_cursor() {
let server = Server::new();
add_fact(&server, "one", "body", None).await;
for bad in ["not-a-cursor", "zz", "6a6a", ""] {
let resp = list_resources(&server, Some(bad)).await;
let e = resp
.error
.unwrap_or_else(|| panic!("cursor {:?} was accepted", bad));
assert_eq!(e.code, E_INVALID_PARAMS, "cursor {:?}", bad);
}
let resp = server
.handle(make_request("resources/list", json!({ "cursor": 3 }), 1))
.await
.expect("response");
assert_eq!(resp.error.expect("error").code, E_INVALID_PARAMS);
}
#[tokio::test]
async fn resources_list_cursor_is_opaque() {
let server = Server::new();
for i in 0..(RESOURCE_PAGE_SIZE + 5) {
add_fact(&server, &format!("subject {:03}", i), "body", None).await;
}
let resp = list_resources(&server, None).await;
let cursor = resp.result.expect("result")["nextCursor"]
.as_str()
.expect("nextCursor")
.to_string();
assert!(!cursor.contains("car://"), "cursor leaks the resource URI");
assert!(
cursor.parse::<u64>().is_err(),
"cursor reads as a decimal offset: {}",
cursor
);
assert!(decode_cursor(&cursor)
.unwrap()
.starts_with("car://memory/fact/"));
}
#[tokio::test]
async fn a_cancellation_notification_is_recognized_and_answers_nothing() {
assert_eq!(
classify_notification(
"notifications/cancelled",
&json!({ "requestId": 42, "reason": "user pressed escape" }),
),
Notification::Cancelled {
request_id: Some(json!(42)),
reason: Some("user pressed escape".to_string()),
}
);
assert_eq!(
classify_notification("notifications/cancelled", &json!({ "requestId": "abc" })),
Notification::Cancelled {
request_id: Some(json!("abc")),
reason: None,
}
);
assert_eq!(
classify_notification("notifications/initialized", &json!({})),
Notification::Initialized
);
assert_eq!(
classify_notification("notifications/progress", &json!({ "progressToken": 7 })),
Notification::Progress {
token: Some(json!(7))
}
);
assert_eq!(
classify_notification("notifications/roots/list_changed", &json!({})),
Notification::RootsListChanged
);
assert_eq!(
classify_notification("notifications/nothing_we_know", &json!({})),
Notification::Unknown
);
let server = Server::new();
for method in [
"notifications/cancelled",
"notifications/initialized",
"notifications/progress",
"notifications/roots/list_changed",
"notifications/nothing_we_know",
] {
let req = Request {
jsonrpc: "2.0".to_string(),
id: None,
method: method.to_string(),
params: json!({ "requestId": 1 }),
};
assert!(
server.handle(req).await.is_none(),
"{} was answered; JSON-RPC 2.0 forbids replying to a notification",
method
);
}
}
#[tokio::test]
async fn a_remembered_fact_outlives_the_server() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("memory.json");
let first = Server::with_store(path.clone()).unwrap();
add_fact(&first, "deploy cadence", "we ship on Thursdays", None).await;
drop(first);
let second = Server::with_store(path.clone()).unwrap();
let engine = second.memgine.lock().await;
assert_eq!(
engine.valid_fact_count(),
1,
"the fact did not survive the restart"
);
}
#[tokio::test]
async fn a_fact_written_by_another_process_is_visible_to_query() {
use car_memgine::note_store::{save, Note, NoteKind};
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("memory.json");
save(
&path,
&[Note {
subject: "pet".into(),
body: "a corgi named Biscuit".into(),
kind: NoteKind::Fact,
}],
)
.unwrap();
let server = Server::with_store(path).unwrap();
let resp = server
.handle(make_request(
"tools/call",
json!({ "name": "memory_query", "arguments": { "query": "pet" } }),
1,
))
.await
.expect("response");
let text = resp.result.expect("result")["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
assert!(
text.contains("Biscuit"),
"query did not see the store: {text}"
);
}
#[tokio::test]
async fn a_constraint_survives_as_a_constraint() {
use car_memgine::note_store::{load, NoteKind};
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("memory.json");
let server = Server::with_store(path.clone()).unwrap();
add_fact(&server, "style", "always use tabs", Some("constraint")).await;
let notes = load(&path);
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].kind, NoteKind::Preference);
assert!(notes[0].kind.is_constraint());
}
#[tokio::test]
async fn appends_do_not_clobber_the_existing_store() {
use car_memgine::note_store::load;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("memory.json");
let server = Server::with_store(path.clone()).unwrap();
add_fact(&server, "one", "first", None).await;
add_fact(&server, "two", "second", None).await;
let notes = load(&path);
assert_eq!(notes.len(), 2);
assert_eq!(notes[0].subject, "one");
assert_eq!(notes[1].subject, "two");
}
#[tokio::test]
async fn a_concurrent_writers_append_is_picked_up_not_overwritten() {
use car_memgine::note_store::{load, save, Note, NoteKind};
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("memory.json");
let server = Server::with_store(path.clone()).unwrap();
add_fact(&server, "ours", "from mcp", None).await;
let mut theirs = load(&path);
theirs.push(Note {
subject: "theirs".into(),
body: "from the assistant".into(),
kind: NoteKind::Fact,
});
save(&path, &theirs).unwrap();
add_fact(&server, "ours-again", "second from mcp", None).await;
let notes = load(&path);
let subjects: Vec<&str> = notes.iter().map(|n| n.subject.as_str()).collect();
assert_eq!(subjects, vec!["ours", "theirs", "ours-again"]);
}
#[tokio::test]
async fn an_unreadable_store_refuses_rather_than_overwriting_it() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("memory.json");
std::fs::write(&path, "{ this is not the store").unwrap();
assert!(
Server::with_store(path.clone()).is_err(),
"started on a corrupt store, and would have overwritten it"
);
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
"{ this is not the store"
);
}
#[tokio::test]
async fn an_ephemeral_server_still_works_and_says_so() {
let server = Server::new();
let text = add_fact(&server, "s", "b", None).await;
assert!(text.contains("durable=false"), "{text}");
}
#[tokio::test]
async fn initialize_returns_protocol_version() {
let server = Server::new();
let resp = server
.handle(make_request("initialize", json!({}), 1))
.await
.expect("response");
let result = resp.result.unwrap();
assert_eq!(result["protocolVersion"], PROTOCOL_VERSION);
assert_eq!(result["serverInfo"]["name"], SERVER_NAME);
}
#[test]
fn negotiate_echoes_a_supported_version_the_client_asked_for() {
let supported = ["2024-11-05", "2025-06-18"];
for want in supported {
assert_eq!(
negotiate_version_in(&supported, &json!({ "protocolVersion": want })),
want,
"asked for {want}"
);
}
}
#[test]
fn negotiate_falls_back_for_unknown_malformed_and_absent() {
for params in [
json!({ "protocolVersion": "1999-01-01" }),
json!({ "protocolVersion": 5 }),
json!({}),
] {
assert_eq!(negotiate_version(¶ms), PROTOCOL_VERSION, "{params}");
}
}
#[tokio::test]
async fn initialize_negotiates_the_requested_version() {
let server = Server::new();
let params = json!({
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": { "name": "c", "version": "0" },
});
let resp = server
.handle(make_request("initialize", params, 1))
.await
.expect("response");
let result = resp.result.unwrap();
assert_eq!(result["protocolVersion"], PROTOCOL_VERSION);
assert_eq!(result["capabilities"]["resources"]["subscribe"], false);
assert_eq!(result["serverInfo"]["name"], SERVER_NAME);
}
#[tokio::test]
async fn initialize_answers_an_unsupported_version_with_our_latest() {
let server = Server::new();
let resp = server
.handle(make_request(
"initialize",
json!({ "protocolVersion": "1999-01-01" }),
1,
))
.await
.expect("response");
assert!(resp.error.is_none(), "{:?}", resp.error);
assert_eq!(resp.result.unwrap()["protocolVersion"], PROTOCOL_VERSION);
}
#[tokio::test]
async fn notification_returns_none() {
let server = Server::new();
let req = Request {
jsonrpc: "2.0".to_string(),
id: None,
method: "notifications/initialized".to_string(),
params: Value::Null,
};
assert!(server.handle(req).await.is_none());
}
#[tokio::test]
async fn a_tool_that_ran_and_failed_reports_is_error_in_the_result() {
let dir = tempfile::tempdir().expect("tempdir");
let store = dir.path().join("notes.json");
let server = Server::with_store(store.clone()).expect("store opens when absent");
std::fs::create_dir(&store).expect("occupy the store path with a directory");
let resp = server
.handle(make_request(
"tools/call",
json!({
"name": "memory_add_fact",
"arguments": { "subject": "s", "body": "b" }
}),
1,
))
.await
.expect("response");
assert!(
resp.error.is_none(),
"an execution failure must not take the JSON-RPC error channel: {:?}",
resp.error
);
let result = resp.result.expect("result");
assert_eq!(result["isError"], true);
let text = result["content"][0]["text"].as_str().unwrap();
assert!(
text.contains("could not write the memory store"),
"the model must be able to READ why it failed; got: {}",
text
);
}
#[tokio::test]
async fn an_unknown_tool_is_still_a_protocol_error() {
let server = Server::new();
let resp = server
.handle(make_request(
"tools/call",
json!({ "name": "no_such_tool", "arguments": {} }),
1,
))
.await
.expect("response");
assert!(resp.result.is_none(), "must not be reported as a result");
assert_eq!(resp.error.expect("error").code, E_METHOD_NOT_FOUND);
}
#[tokio::test]
async fn invalid_arguments_are_still_a_protocol_error() {
let server = Server::new();
let resp = server
.handle(make_request(
"tools/call",
json!({ "name": "memory_add_fact", "arguments": { "subject": "s" } }),
1,
))
.await
.expect("response");
assert!(resp.result.is_none(), "must not be reported as a result");
assert_eq!(resp.error.expect("error").code, E_INVALID_PARAMS);
}
#[tokio::test]
async fn unknown_method_returns_method_not_found_error() {
let server = Server::new();
let resp = server
.handle(make_request("bogus/method", json!({}), 1))
.await
.expect("response");
let err = resp.error.unwrap();
assert_eq!(err.code, E_METHOD_NOT_FOUND);
}
#[tokio::test]
async fn tools_list_returns_the_advertised_surface() {
let server = Server::new();
let resp = server
.handle(make_request("tools/list", json!({}), 1))
.await
.expect("response");
let result = resp.result.unwrap();
let mut names: Vec<&str> = result["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap())
.collect();
names.sort_unstable();
assert_eq!(
names,
vec![
"equivalent",
"memory_add_fact",
"memory_delete",
"memory_evaluate",
"memory_intervene",
"memory_query",
"memory_save_knowledge",
"memory_save_procedural",
"memory_update_status",
"optimize",
"policy_check",
"simulate",
"skill_find",
"skill_ingest",
"skill_list",
"verify",
]
);
}
struct TestTool {
text: &'static str,
fail: bool,
}
#[async_trait::async_trait]
impl ToolHandler for TestTool {
async fn call(&self, _args: Value) -> Result<String, ToolError> {
if self.fail {
Err(ToolError::Internal(self.text.to_string()))
} else {
Ok(self.text.to_string())
}
}
}
fn test_schema(name: &str) -> Value {
json!({
"name": name,
"description": "a test tool",
"inputSchema": { "type": "object", "properties": {} },
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false,
},
})
}
async fn tool_names(server: &Server) -> Vec<String> {
let resp = server
.handle(make_request("tools/list", json!({}), 1))
.await
.expect("response");
resp.result.expect("result")["tools"]
.as_array()
.expect("array")
.iter()
.map(|t| t["name"].as_str().expect("name").to_string())
.collect()
}
async fn call_tool(server: &Server, name: &str) -> Response {
server
.handle(make_request(
"tools/call",
json!({ "name": name, "arguments": {} }),
1,
))
.await
.expect("response")
}
#[tokio::test]
async fn with_nothing_registered_the_surface_is_exactly_the_built_ins() {
let server = Server::new();
let resp = server
.handle(make_request("tools/list", json!({}), 1))
.await
.expect("response");
let result = resp.result.expect("result");
assert_eq!(
serde_json::to_string(&result["tools"]).unwrap(),
serde_json::to_string(cached_tool_schemas()).unwrap(),
);
}
#[tokio::test]
async fn a_registered_tool_is_advertised_and_dispatched() {
let mut server = Server::new();
server
.register_tool(
test_schema("assistant_start"),
Arc::new(TestTool {
text: "ran",
fail: false,
}),
)
.expect("registers");
let names = tool_names(&server).await;
assert_eq!(names.len(), cached_tool_schemas().len() + 1);
assert_eq!(names.last().map(String::as_str), Some("assistant_start"));
let resp = call_tool(&server, "assistant_start").await;
assert!(resp.error.is_none(), "{:?}", resp.error);
let result = resp.result.expect("result");
assert_eq!(result["content"][0]["text"], "ran");
assert_eq!(result["isError"], false);
}
#[tokio::test]
async fn two_servers_in_one_process_advertise_different_tool_sets() {
let mut daemon = Server::new();
daemon
.register_tool(
test_schema("assistant_start"),
Arc::new(TestTool {
text: "ran",
fail: false,
}),
)
.expect("registers");
let stdio = Server::new();
let daemon_names = tool_names(&daemon).await;
let stdio_names = tool_names(&stdio).await;
assert_eq!(daemon_names.len(), cached_tool_schemas().len() + 1);
assert_eq!(stdio_names.len(), cached_tool_schemas().len());
assert!(daemon_names.iter().any(|n| n == "assistant_start"));
assert!(!stdio_names.iter().any(|n| n == "assistant_start"));
let resp = call_tool(&stdio, "assistant_start").await;
assert_eq!(resp.error.expect("error").code, E_METHOD_NOT_FOUND);
}
#[tokio::test]
async fn the_stdio_server_offers_no_assistant_tools() {
let dir = tempfile::tempdir().expect("tempdir");
let server =
Server::with_store(dir.path().join("notes.json")).expect("opens a fresh store");
let names = tool_names(&server).await;
for tool in ["assistant_start", "assistant_poll", "assistant_cancel"] {
assert!(
!names.iter().any(|n| n == tool),
"{tool} must not be advertised on stdio: {names:?}"
);
let resp = call_tool(&server, tool).await;
assert_eq!(resp.error.expect("error").code, E_METHOD_NOT_FOUND);
}
}
#[tokio::test]
async fn registering_a_built_in_name_is_rejected() {
let mut server = Server::new();
let e = server
.register_tool(
test_schema("memory_query"),
Arc::new(TestTool {
text: "hijacked",
fail: false,
}),
)
.expect_err("a built-in cannot be shadowed");
assert!(matches!(e, RegisterError::CollidesWithBuiltIn(ref n) if n == "memory_query"));
assert_eq!(tool_names(&server).await.len(), cached_tool_schemas().len());
let resp = server
.handle(make_request(
"tools/call",
json!({ "name": "memory_query", "arguments": { "query": "anything" } }),
1,
))
.await
.expect("response");
let text = resp.result.expect("result")["content"][0]["text"]
.as_str()
.expect("text")
.to_string();
assert_ne!(text, "hijacked");
}
#[tokio::test]
async fn registering_the_same_name_twice_is_rejected() {
let mut server = Server::new();
let tool = || {
Arc::new(TestTool {
text: "ran",
fail: false,
})
};
server
.register_tool(test_schema("assistant_start"), tool())
.expect("first registration");
let e = server
.register_tool(test_schema("assistant_start"), tool())
.expect_err("second registration");
assert!(matches!(e, RegisterError::AlreadyRegistered(ref n) if n == "assistant_start"));
assert_eq!(
tool_names(&server).await.len(),
cached_tool_schemas().len() + 1,
"a refused registration must not have been advertised"
);
}
#[tokio::test]
async fn a_registered_tool_without_annotations_is_rejected() {
let mut server = Server::new();
let mut schema = test_schema("assistant_start");
schema["annotations"]
.as_object_mut()
.expect("object")
.remove("idempotentHint");
let e = server
.register_tool(
schema,
Arc::new(TestTool {
text: "ran",
fail: false,
}),
)
.expect_err("an unclassified tool cannot be advertised");
assert!(matches!(e, RegisterError::MissingAnnotations(ref n) if n == "assistant_start"));
}
#[tokio::test]
async fn a_registered_tool_that_fails_reports_an_execution_error() {
let mut server = Server::new();
server
.register_tool(
test_schema("assistant_start"),
Arc::new(TestTool {
text: "the runtime refused",
fail: true,
}),
)
.expect("registers");
let resp = call_tool(&server, "assistant_start").await;
assert!(
resp.error.is_none(),
"an execution failure must not take the JSON-RPC error channel: {:?}",
resp.error
);
let result = resp.result.expect("result");
assert_eq!(result["isError"], true);
assert_eq!(result["content"][0]["text"], "the runtime refused");
}
#[test]
fn every_advertised_tool_carries_all_four_annotations() {
for tool in cached_tool_schemas() {
let name = tool["name"].as_str().expect("every tool has a name");
let ann = tool["annotations"]
.as_object()
.unwrap_or_else(|| panic!("{name} advertises no annotations object"));
let mut keys: Vec<&str> = ann.keys().map(String::as_str).collect();
keys.sort_unstable();
assert_eq!(
keys,
vec![
"destructiveHint",
"idempotentHint",
"openWorldHint",
"readOnlyHint",
],
"{name}'s annotation keys"
);
for (hint, value) in ann {
assert!(value.is_boolean(), "{name}.{hint} must be a bool");
}
if ann["readOnlyHint"] == json!(true) {
assert_eq!(
ann["destructiveHint"],
json!(false),
"{name} is read-only, so destructiveHint must be false"
);
}
}
}
#[test]
fn the_annotations_match_what_the_tools_actually_do() {
let hint = |tool: &str, key: &str| -> bool {
cached_tool_schemas()
.iter()
.find(|t| t["name"] == json!(tool))
.unwrap_or_else(|| panic!("{tool} is not advertised"))["annotations"][key]
.as_bool()
.unwrap_or_else(|| panic!("{tool}.{key} is not a bool"))
};
assert!(hint("memory_query", "readOnlyHint"));
assert!(hint("skill_find", "readOnlyHint"));
assert!(hint("policy_check", "readOnlyHint"));
assert!(hint("verify", "readOnlyHint"));
assert!(hint("memory_delete", "destructiveHint"));
assert!(!hint("memory_intervene", "readOnlyHint"));
assert!(hint("skill_ingest", "destructiveHint"));
assert!(hint("memory_update_status", "destructiveHint"));
assert!(!hint("memory_save_knowledge", "destructiveHint"));
for tool in cached_tool_schemas() {
let name = tool["name"].as_str().expect("name");
assert_eq!(
tool["annotations"]["openWorldHint"],
json!(false),
"{name} claims an open world — is that true, or a copy-paste?"
);
}
}
#[tokio::test]
async fn add_fact_then_query_round_trips() {
let server = Server::new();
let _add = server
.handle(make_request(
"tools/call",
json!({
"name": "memory_add_fact",
"arguments": { "subject": "color", "body": "the sky is blue" }
}),
1,
))
.await
.unwrap();
let q = server
.handle(make_request(
"tools/call",
json!({
"name": "memory_query",
"arguments": { "query": "color", "k": 5 }
}),
2,
))
.await
.unwrap();
let text = q.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
assert!(text.contains("color") || text.contains("sky"));
}
#[tokio::test]
async fn proactive_memory_tools_round_trip() {
let server = Server::new();
let save = server
.handle(make_request(
"tools/call",
json!({
"name": "memory_save_knowledge",
"arguments": {
"id": "mcp-policy",
"subject": "deployment policy",
"body": "Must verify before shipping",
"tags": ["policy"],
"is_constraint": true
}
}),
1,
))
.await
.unwrap();
let save_text = save.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
assert!(save_text.contains("mcp-policy"));
let intervene = server
.handle(make_request(
"tools/call",
json!({
"name": "memory_intervene",
"arguments": {
"query": "ship deployment",
"trigger": { "high_risk_action": true }
}
}),
2,
))
.await
.unwrap();
let intervene_text = intervene.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
assert!(intervene_text.contains("\"decision\":\"inject\""));
assert!(intervene_text.contains("Must verify before shipping"));
let eval = server
.handle(make_request(
"tools/call",
json!({
"name": "memory_evaluate",
"arguments": {
"cases": [{
"id": "ship",
"request": {
"query": "ship deployment",
"trigger": { "high_risk_action": true }
},
"relevant_fact_ids": ["mcp-policy"]
}]
}
}),
3,
))
.await
.unwrap();
let eval_text = eval.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
assert!(eval_text.contains("\"true_positives\":1"));
}
#[tokio::test]
async fn verify_issues_carry_their_evidence_tier() {
let server = Server::new();
let resp = server
.handle(make_request(
"tools/call",
json!({
"name": "verify",
"arguments": {
"proposal": {
"actions": [
{ "id": "a1", "type": "tool_call", "tool": "poll" },
{ "id": "a2", "type": "tool_call", "tool": "poll" },
{ "id": "a3", "type": "tool_call", "tool": "poll" }
]
}
}
}),
1,
))
.await
.expect("response");
let text = resp.result.unwrap()["content"][0]["text"]
.as_str()
.unwrap()
.to_string();
let parsed: Value = serde_json::from_str(&text).expect("verify result is JSON");
let issues = parsed["issues"].as_array().expect("issues array");
assert!(
!issues.is_empty(),
"three identical calls trip loop detection"
);
for issue in issues {
let tier = issue["tier"].as_str().expect("every issue carries a tier");
assert!(
["decision_procedure", "heuristic", "sampled"].contains(&tier),
"unexpected tier {tier} — the labels are shared with the FFI and \
JSON-RPC projections"
);
}
assert!(issues
.iter()
.any(|i| i["tier"] == "heuristic"
&& i["message"].as_str().unwrap().contains("likely loop")));
}
async fn call_verification_tool(name: &str, arguments: Value) -> Value {
let server = Server::new();
let resp = server
.handle(make_request(
"tools/call",
json!({ "name": name, "arguments": arguments }),
1,
))
.await
.expect("response");
let result = resp
.result
.unwrap_or_else(|| panic!("{name} returned no result"));
assert_eq!(result["isError"], false, "{name} reported a failure");
serde_json::from_str(result["content"][0]["text"].as_str().expect("text"))
.unwrap_or_else(|e| panic!("{name}'s text blob is not JSON: {e}"))
}
#[tokio::test]
async fn simulate_returns_the_state_the_declared_effects_imply() {
let parsed = call_verification_tool(
"simulate",
json!({
"proposal": {
"actions": [
{ "id": "a1", "type": "state_write", "parameters": { "key": "x", "value": 10 } },
{ "id": "a2", "type": "state_write", "parameters": { "key": "y", "value": 20 } }
]
},
"initial_state": { "seed": true }
}),
)
.await;
assert_eq!(parsed["final_state"]["x"], json!(10));
assert_eq!(parsed["final_state"]["y"], json!(20));
assert_eq!(parsed["final_state"]["seed"], json!(true));
}
#[tokio::test]
async fn simulate_does_not_credit_an_action_whose_dependency_is_missing() {
let parsed = call_verification_tool(
"simulate",
json!({
"proposal": {
"actions": [{
"id": "a1",
"type": "tool_call",
"tool": "deploy",
"state_dependencies": ["nobody_writes_this"],
"expected_effects": { "deployed": true }
}]
}
}),
)
.await;
assert!(
parsed["final_state"].get("deployed").is_none(),
"a blocked action must not contribute its declared effects: {}",
parsed["final_state"]
);
}
#[tokio::test]
async fn equivalent_reports_how_the_answer_was_derived() {
let write = |id: &str, key: &str, value: i64| json!({ "id": id, "type": "state_write", "parameters": { "key": key, "value": value } });
let same = call_verification_tool(
"equivalent",
json!({
"proposal_a": { "actions": [write("a1", "x", 1), write("a2", "y", 2)] },
"proposal_b": { "actions": [write("b1", "y", 2), write("b2", "x", 1)] },
}),
)
.await;
assert_eq!(same["equivalent"], json!(true));
assert_eq!(same["tier"], json!("sampled"));
assert_eq!(same["states_tested"], json!(2));
assert_eq!(same["used_default_states"], json!(true));
let differs = call_verification_tool(
"equivalent",
json!({
"proposal_a": { "actions": [write("a1", "x", 1)] },
"proposal_b": { "actions": [write("b1", "x", 99)] },
"test_states": [{}, { "x": 0 }, { "unrelated": 7 }],
}),
)
.await;
assert_eq!(differs["equivalent"], json!(false));
assert_eq!(differs["states_tested"], json!(3));
assert_eq!(differs["used_default_states"], json!(false));
}
#[tokio::test]
async fn an_empty_test_states_never_buys_a_zero_probe_true() {
let write = |id: &str, key: &str, value: i64| json!({ "id": id, "type": "state_write", "parameters": { "key": key, "value": value } });
let parsed = call_verification_tool(
"equivalent",
json!({
"proposal_a": { "actions": [write("a1", "x", 1)] },
"proposal_b": { "actions": [write("b1", "x", 2)] },
"test_states": [],
}),
)
.await;
assert_eq!(
parsed["equivalent"],
json!(false),
"x=1 and x=2 differ on the default states; [] must not vacuously agree"
);
assert_eq!(
parsed["states_tested"],
json!(2),
"[] falls back to the two defaults, so the probe count is never 0"
);
assert_eq!(parsed["used_default_states"], json!(true));
}
#[tokio::test]
async fn an_over_long_test_states_is_refused_rather_than_run() {
let server = Server::new();
let states: Vec<Value> = (0..=MAX_TEST_STATES).map(|i| json!({ "x": i })).collect();
let resp = server
.handle(make_request(
"tools/call",
json!({
"name": "equivalent",
"arguments": {
"proposal_a": { "actions": [] },
"proposal_b": { "actions": [] },
"test_states": states,
},
}),
1,
))
.await
.expect("response");
assert!(resp.result.is_none(), "the tool must not have run");
assert_eq!(resp.error.expect("error").code, E_INVALID_PARAMS);
}
#[tokio::test]
async fn optimize_prunes_a_phantom_dependency_and_names_it() {
let parsed = call_verification_tool(
"optimize",
json!({
"proposal": {
"actions": [
{ "id": "a1", "type": "state_write", "parameters": { "key": "x", "value": 1 } },
{
"id": "a2",
"type": "tool_call",
"tool": "report",
"state_dependencies": ["x", "nobody_writes_this"]
}
]
}
}),
)
.await;
let actions = parsed["proposal"]["actions"].as_array().expect("actions");
assert_eq!(
actions.len(),
2,
"optimize rewrites, it does not drop actions"
);
assert_eq!(actions[1]["state_dependencies"], json!(["x"]));
assert_eq!(
parsed["pruned"],
json!([{ "action_id": "a2", "removed": ["nobody_writes_this"] }]),
"the caller has to be told what the rewrite dropped"
);
}
#[tokio::test]
async fn a_malformed_proposal_is_a_protocol_error_on_every_sibling() {
let server = Server::new();
let cases = [
("simulate", json!({ "proposal": "not an object" })),
(
"equivalent",
json!({ "proposal_a": { "actions": [] }, "proposal_b": 7 }),
),
("optimize", json!({ "proposal": ["not", "an", "object"] })),
];
for (name, arguments) in cases {
let resp = server
.handle(make_request(
"tools/call",
json!({ "name": name, "arguments": arguments }),
1,
))
.await
.expect("response");
assert!(
resp.result.is_none(),
"{name} must not answer a malformed proposal with a result"
);
assert_eq!(
resp.error.expect("error").code,
E_INVALID_PARAMS,
"{name}'s error code"
);
}
}
#[test]
fn the_verification_siblings_are_advertised_as_pure_reads() {
for name in ["simulate", "equivalent", "optimize"] {
let ann = &cached_tool_schemas()
.iter()
.find(|t| t["name"] == json!(name))
.unwrap_or_else(|| panic!("{name} is not advertised"))["annotations"];
assert_eq!(ann["readOnlyHint"], json!(true), "{name}.readOnlyHint");
assert_eq!(
ann["destructiveHint"],
json!(false),
"{name}.destructiveHint"
);
assert_eq!(ann["idempotentHint"], json!(true), "{name}.idempotentHint");
assert_eq!(ann["openWorldHint"], json!(false), "{name}.openWorldHint");
}
}
#[tokio::test]
async fn invalid_jsonrpc_version_rejected() {
let server = Server::new();
let req = Request {
jsonrpc: "1.0".to_string(),
id: Some(json!(1)),
method: "ping".to_string(),
params: Value::Null,
};
let resp = server.handle(req).await.unwrap();
let err = resp.error.unwrap();
assert_eq!(err.code, E_INVALID_REQUEST);
}
#[tokio::test]
async fn prompts_get_unknown_returns_invalid_params() {
let server = Server::new();
let resp = server
.handle(make_request(
"prompts/get",
json!({ "name": "does_not_exist", "arguments": { "query": "x" } }),
1,
))
.await
.unwrap();
let err = resp.error.unwrap();
assert_eq!(err.code, E_INVALID_PARAMS);
}
async fn complete(server: &Server, params: Value) -> Value {
let resp = server
.handle(make_request("completion/complete", params, 1))
.await
.expect("completion/complete is a request, not a notification");
assert!(
resp.error.is_none(),
"expected a result, got error {:?}",
resp.error
);
resp.result.expect("result")["completion"].clone()
}
async fn complete_err(server: &Server, params: Value) -> i32 {
let resp = server
.handle(make_request("completion/complete", params, 1))
.await
.expect("response");
resp.error.expect("expected an error").code
}
fn values(completion: &Value) -> Vec<String> {
completion["values"]
.as_array()
.expect("values")
.iter()
.map(|v| v.as_str().expect("value is a string").to_string())
.collect()
}
#[tokio::test]
async fn completion_completes_the_mode_argument() {
let server = Server::new();
let all = complete(
&server,
json!({
"ref": { "type": "ref/prompt", "name": "car_context" },
"argument": { "name": "mode" },
}),
)
.await;
let mut got = values(&all);
got.sort();
assert_eq!(got, vec!["fast".to_string(), "full".to_string()]);
assert_eq!(all["total"], 2);
assert_eq!(all["hasMore"], false);
let narrowed = complete(
&server,
json!({
"ref": { "type": "ref/prompt", "name": "car_context" },
"argument": { "name": "mode", "value": "fu" },
}),
)
.await;
assert_eq!(values(&narrowed), vec!["full".to_string()]);
assert_eq!(narrowed["total"], 1);
assert_eq!(narrowed["hasMore"], false);
}
#[tokio::test]
async fn completion_of_a_free_form_argument_is_empty() {
let server = Server::new();
let c = complete(
&server,
json!({
"ref": { "type": "ref/prompt", "name": "car_context" },
"argument": { "name": "query", "value": "how do I" },
}),
)
.await;
assert_eq!(values(&c), Vec::<String>::new());
assert_eq!(c["total"], 0);
assert_eq!(c["hasMore"], false);
}
#[tokio::test]
async fn completion_of_an_unknown_prompt_is_empty_not_an_error() {
let server = Server::new();
let unknown_prompt = complete(
&server,
json!({
"ref": { "type": "ref/prompt", "name": "nope" },
"argument": { "name": "mode", "value": "f" },
}),
)
.await;
assert_eq!(values(&unknown_prompt), Vec::<String>::new());
assert_eq!(unknown_prompt["total"], 0);
let unknown_ref = complete(
&server,
json!({
"ref": { "type": "ref/something-new", "name": "car_context" },
"argument": { "name": "mode" },
}),
)
.await;
assert_eq!(values(&unknown_ref), Vec::<String>::new());
let no_type = complete(
&server,
json!({
"ref": { "name": "car_context" },
"argument": { "name": "mode" },
}),
)
.await;
assert_eq!(values(&no_type), Vec::<String>::new());
let unknown_arg = complete(
&server,
json!({
"ref": { "type": "ref/prompt", "name": "car_context" },
"argument": { "name": "not_an_argument" },
}),
)
.await;
assert_eq!(values(&unknown_arg), Vec::<String>::new());
}
#[tokio::test]
async fn completion_completes_resource_uris_by_prefix() {
let server = Server::new();
for i in 0..3 {
add_fact(&server, &format!("subject {}", i), "body", None).await;
}
server
.handle(make_request(
"tools/call",
json!({
"name": "skill_ingest",
"arguments": { "name": "a_skill", "code": "// noop" },
}),
1,
))
.await
.expect("response");
let listed: Vec<String> = list_resources(&server, None).await.result.expect("result")
["resources"]
.as_array()
.expect("resources")
.iter()
.map(|r| r["uri"].as_str().expect("uri").to_string())
.collect();
let listed_facts: Vec<String> = listed
.iter()
.filter(|u| u.starts_with("car://memory/fact/"))
.cloned()
.collect();
assert_eq!(listed_facts.len(), 3, "fixture should seed three facts");
assert!(
listed.iter().any(|u| u.starts_with("car://memory/skill/")),
"fixture should seed a skill too, so the prefix has something to exclude"
);
let c = complete(
&server,
json!({
"ref": { "type": "ref/resource", "uri": "car://memory/fact/" },
"argument": { "name": "uri", "value": "" },
}),
)
.await;
assert_eq!(values(&c), listed_facts);
assert_eq!(c["total"], 3);
assert_eq!(c["hasMore"], false);
}
#[tokio::test]
async fn completion_caps_values_and_reports_total() {
let server = Server::new();
let total = 250;
for i in 0..total {
add_fact(&server, &format!("subject {:03}", i), "body", None).await;
}
let c = complete(
&server,
json!({
"ref": { "type": "ref/resource", "uri": "car://memory/fact/" },
"argument": { "name": "uri" },
}),
)
.await;
assert_eq!(values(&c).len(), MAX_COMPLETION_VALUES);
assert_eq!(c["total"], total);
assert_eq!(c["hasMore"], true);
}
#[tokio::test]
async fn completion_rejects_malformed_params() {
let server = Server::new();
let cases = [
json!({ "argument": { "name": "mode" } }),
json!({ "ref": { "type": "ref/prompt", "name": "car_context" } }),
json!({
"ref": { "type": "ref/prompt", "name": "car_context" },
"argument": { "value": "fu" },
}),
json!({ "ref": "ref/prompt", "argument": { "name": "mode" } }),
json!({ "ref": { "type": "ref/prompt", "name": "car_context" }, "argument": "mode" }),
json!({}),
];
for params in cases {
assert_eq!(
complete_err(&server, params.clone()).await,
E_INVALID_PARAMS,
"{params}"
);
}
}
#[tokio::test]
async fn initialize_advertises_the_completions_capability() {
let server = Server::new();
let resp = server
.handle(make_request("initialize", json!({}), 1))
.await
.unwrap();
let caps = resp.result.expect("result")["capabilities"].clone();
assert!(
caps.get("completions").is_some(),
"initialize must advertise `completions`, got {caps}"
);
}
#[tokio::test]
async fn every_completed_mode_is_accepted_by_prompts_get() {
let server = Server::new();
let c = complete(
&server,
json!({
"ref": { "type": "ref/prompt", "name": "car_context" },
"argument": { "name": "mode" },
}),
)
.await;
let offered = values(&c);
assert!(!offered.is_empty(), "the completion must offer something");
for mode in offered {
let resp = server
.handle(make_request(
"prompts/get",
json!({
"name": "car_context",
"arguments": { "query": "anything", "mode": mode },
}),
1,
))
.await
.expect("response");
assert!(
resp.error.is_none(),
"prompts/get rejected the completed mode {mode:?}: {:?}",
resp.error
);
}
}
}