#![cfg(feature = "serve")]
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use axum::{
body::Bytes,
http::{HeaderMap, StatusCode},
response::{sse, IntoResponse, Response, Sse},
};
use futures::stream::StreamExt;
use parking_lot::Mutex;
use crate::http_server::{AppState, SseEvent};
use crate::protocol::{JsonRpcError, JsonRpcRequest, JsonRpcResponse};
use crate::server::{dispatch_tool, handle_mcp_method};
use crate::session::SessionState;
use crate::util::now_ms;
const MCP_SESSION_HEADER: &str = "mcp-session-id";
const CALLER_ROOT_HEADER: &str = "m1nd-caller-root";
const MCP_TOOL_TIMEOUT_SECS: u64 = 120;
const GRAPH_CHANGED_METHOD: &str = "notifications/m1nd/graph_changed";
const MCP_SSE_KEEPALIVE_SECS: u64 = 15;
const GRAPH_MUTATION_TOOLS: &[&str] = &[
"ingest",
"apply",
"apply_batch",
"edit_commit",
"memorize",
"learn",
"daemon_start",
"auto_ingest_start",
];
fn bare_tool_name(tool: &str) -> &str {
tool.strip_prefix("m1nd.")
.or_else(|| tool.strip_prefix("m1nd_"))
.unwrap_or(tool)
}
pub(crate) fn graph_mutation_event_name(event: &SseEvent) -> Option<&str> {
match event.event_type.as_str() {
"tool_result" => {
let tool = event.data.get("tool").and_then(|v| v.as_str())?;
if !GRAPH_MUTATION_TOOLS.contains(&bare_tool_name(tool)) {
return None;
}
if event.data.get("success").and_then(|v| v.as_bool()) == Some(false) {
return None;
}
Some(tool)
}
"apply_batch_handoff" | "apply_batch_progress" => Some(
event
.data
.get("tool")
.and_then(|v| v.as_str())
.unwrap_or("apply_batch"),
),
"tool_timeout" => {
let tool = event.data.get("tool").and_then(|v| v.as_str())?;
if !GRAPH_MUTATION_TOOLS.contains(&bare_tool_name(tool)) {
return None;
}
Some(tool)
}
_ => None,
}
}
fn graph_changed_notification(event: &SseEvent) -> Option<serde_json::Value> {
let relay_event_name: &str = graph_mutation_event_name(event)?;
let mut detail = serde_json::Map::new();
if let Some(agent_id) = event.data.get("agent_id") {
detail.insert("agent_id".into(), agent_id.clone());
}
if let Some(source) = event.data.get("source") {
detail.insert("source".into(), source.clone());
}
if let Some(batch_id) = event.data.get("batch_id") {
detail.insert("batch_id".into(), batch_id.clone());
}
if let Some(ts) = event.data.get("timestamp_ms") {
detail.insert("timestamp_ms".into(), ts.clone());
}
detail.insert("kind".into(), serde_json::json!(event.event_type));
Some(serde_json::json!({
"jsonrpc": "2.0",
"method": GRAPH_CHANGED_METHOD,
"params": {
"event": relay_event_name,
"detail": serde_json::Value::Object(detail),
},
}))
}
fn event_origin_is(event: &SseEvent, viewer: &str) -> bool {
event
.data
.get(ORIGIN_SESSION_FIELD)
.and_then(|v| v.as_str())
== Some(viewer)
}
#[derive(Clone, Debug)]
pub struct McpTransportSession {
pub protocol_version: String,
pub created_ms: u64,
pub last_seen_ms: u64,
pub caller_root: Option<String>,
pub bound_project_root: Option<String>,
}
pub type McpSessionRegistry = Arc<Mutex<HashMap<String, McpTransportSession>>>;
pub fn new_mcp_session_registry() -> McpSessionRegistry {
Arc::new(Mutex::new(HashMap::new()))
}
static MCP_SESSION_SEQ: AtomicU64 = AtomicU64::new(0);
pub fn generate_mcp_session_id() -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let seq = MCP_SESSION_SEQ.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let t = now_ms();
let mut hi = DefaultHasher::new();
pid.hash(&mut hi);
t.hash(&mut hi);
seq.hash(&mut hi);
"m1nd-mcp-hi".hash(&mut hi);
let mut lo = DefaultHasher::new();
seq.hash(&mut lo);
t.hash(&mut lo);
pid.hash(&mut lo);
"m1nd-mcp-lo".hash(&mut lo);
format!("{:016x}{:016x}", hi.finish(), lo.finish())
}
enum ParsedMessage {
Request(JsonRpcRequest),
NotificationOrResponse,
}
fn jsonrpc_error_response(
status: StatusCode,
id: serde_json::Value,
code: i32,
message: impl Into<String>,
) -> Response {
let body = JsonRpcResponse {
jsonrpc: "2.0".into(),
id,
result: None,
error: Some(JsonRpcError {
code,
message: message.into(),
data: None,
}),
};
let json = serde_json::to_string(&body).unwrap_or_default();
(
status,
[(axum::http::header::CONTENT_TYPE, "application/json")],
json,
)
.into_response()
}
fn jsonrpc_ok_response(resp: &JsonRpcResponse, session_id: Option<&str>) -> Response {
let json = serde_json::to_string(resp).unwrap_or_default();
let mut response = (
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "application/json")],
json,
)
.into_response();
if let Some(sid) = session_id {
if let Ok(value) = axum::http::HeaderValue::from_str(sid) {
response.headers_mut().insert("mcp-session-id", value);
}
}
response
}
async fn run_mcp_method(
app: Arc<AppState>,
request: JsonRpcRequest,
caller_root: Option<String>,
) -> JsonRpcResponse {
let id = request.id.clone();
let result = tokio::time::timeout(
Duration::from_secs(MCP_TOOL_TIMEOUT_SECS),
tokio::task::spawn_blocking(move || {
let mut session = app.session.lock();
session.caller_root = caller_root;
handle_mcp_method(&mut session, &request)
}),
)
.await;
match result {
Ok(Ok(resp)) => resp,
Ok(Err(_join_err)) => JsonRpcResponse {
jsonrpc: "2.0".into(),
id,
result: None,
error: Some(JsonRpcError {
code: -32603,
message: "Internal error: tool task panicked".into(),
data: None,
}),
},
Err(_elapsed) => JsonRpcResponse {
jsonrpc: "2.0".into(),
id,
result: None,
error: Some(JsonRpcError {
code: -32000,
message: format!("Tool execution exceeded {}s timeout", MCP_TOOL_TIMEOUT_SECS),
data: None,
}),
},
}
}
fn bootstrap_project_root(request: &JsonRpcRequest) -> Option<String> {
if request.method != "tools/call" {
return None;
}
let name = request.params.get("name")?.as_str()?;
if bare_tool_name(name) != "ingest" {
return None;
}
let root = request
.params
.get("arguments")?
.get("project_root")?
.as_str()?
.trim();
if root.is_empty() {
None
} else {
Some(root.to_string())
}
}
fn promote_request(request: &JsonRpcRequest) -> Option<crate::promote_handlers::PromoteInput> {
if request.method != "tools/call" {
return None;
}
let name = request.params.get("name")?.as_str()?;
if bare_tool_name(name) != "promote" {
return None;
}
let args = request.params.get("arguments")?;
Some(crate::promote_handlers::PromoteInput {
agent_id: args
.get("agent_id")
.and_then(|v| v.as_str())
.unwrap_or("promote")
.to_string(),
brain: args
.get("brain")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
claim: args
.get("claim")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
reason: args
.get("reason")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
})
}
fn run_promote(
app: &Arc<AppState>,
request: &JsonRpcRequest,
input: &crate::promote_handlers::PromoteInput,
) -> JsonRpcResponse {
let id = request.id.clone();
if input.brain.trim().is_empty() || input.claim.trim().is_empty() {
return tool_error_response(
id,
"promote requires a non-empty `brain` (the source project root) and `claim` (the slug to promote)".into(),
);
}
let (medulla_runtime_root, medulla_store_dir, brain_is_bound) = {
let session = app.session.lock();
let rr = session.runtime_root.clone();
let store = rr.join("agent-memory");
let covered = session.covers_root(&input.brain);
(rr, store, covered)
};
if brain_is_bound {
return tool_error_response(
id,
format!(
"brain '{}' is the owner's bound graph (the medulla itself) — a claim there is \
already doctrine; there is nothing to promote UP to.",
input.brain
),
);
}
let canonical = crate::project_brains::ProjectBrainRegistry::canonical_key(&input.brain);
if !app.project_brains.knows(&canonical) {
return tool_error_response(
id,
format!(
"no project brain for '{}' — bootstrap it first (ingest with project_root=...), \
or check the path. Promotion reads a real, hosted source store.",
input.brain
),
);
}
let source_store_dir = app
.project_brains
.store_dir_for(&canonical)
.join("agent-memory");
match crate::promote_handlers::promote_claim(
input,
&source_store_dir,
&medulla_store_dir,
&medulla_runtime_root,
) {
Ok(outcome) => {
{
let mut session = app.session.lock();
let ingest = crate::protocol::core::IngestInput {
path: outcome.medulla_path.to_string_lossy().to_string(),
agent_id: input.agent_id.clone(),
incremental: false,
adapter: "light".into(),
mode: "merge".into(),
namespace: Some("light".into()),
include_dotfiles: false,
dotfile_patterns: vec![],
project_root: None,
};
let _ = crate::tools::handle_ingest(&mut session, ingest);
}
let payload = crate::promote_handlers::promote_response(input, &outcome);
tool_result_response(id, &payload)
}
Err(e) => tool_error_response(id, e.to_string()),
}
}
fn tool_result_response(id: serde_json::Value, payload: &serde_json::Value) -> JsonRpcResponse {
JsonRpcResponse {
jsonrpc: "2.0".into(),
id,
result: Some(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(payload).unwrap_or_default(),
}]
})),
error: None,
}
}
fn tool_error_response(id: serde_json::Value, message: String) -> JsonRpcResponse {
JsonRpcResponse {
jsonrpc: "2.0".into(),
id,
result: Some(serde_json::json!({
"content": [{ "type": "text", "text": format!("Error: {}", message) }],
"isError": true
})),
error: None,
}
}
fn run_bootstrap(
app: &Arc<AppState>,
request: &JsonRpcRequest,
project_root: &str,
session_id: Option<&str>,
) -> JsonRpcResponse {
let arguments = request
.params
.get("arguments")
.cloned()
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
let bound_covers = { app.session.lock().covers_root(project_root) };
if bound_covers {
return tool_error_response(
request.id.clone(),
format!(
"project_root {project_root} is already covered by this owner's bound graph — \
you are home; call verbs directly (bootstrap refused so the bound brain is \
never shadowed by a duplicate)"
),
);
}
match app.project_brains.bootstrap(project_root, &arguments) {
Err(e) => tool_error_response(
request.id.clone(),
format!("one-call bootstrap of {project_root} failed: {e}"),
),
Ok((brain, ingest_result, reused)) => {
let key = crate::project_brains::ProjectBrainRegistry::canonical_key(project_root);
if let Some(sid) = session_id {
if let Some(s) = app.mcp_sessions.lock().get_mut(sid) {
s.bound_project_root = Some(key.clone());
}
}
let agent_id = arguments
.get("agent_id")
.and_then(|v| v.as_str())
.unwrap_or("bootstrap")
.to_string();
let north = {
let mut state = brain.lock();
state.caller_root = Some(key.clone());
crate::server::dispatch_tool(
&mut state,
"north",
&serde_json::json!({
"agent_id": agent_id,
"task": format!(
"first orientation of the {key} project brain right after its one-call bootstrap"
),
}),
)
.unwrap_or_else(|e| {
serde_json::json!({
"error": format!("north after bootstrap failed: {e}"),
})
})
};
let packet = serde_json::json!({
"schema": "m1nd-project-brain-bootstrap-v0",
"project_root": key,
"store_dir": app.project_brains.store_dir_for(&key),
"reused_existing_brain": reused,
"ingest": ingest_result,
"north": north,
"routing": "this wire session is now bound to your project brain; any call — this session or a brand NEW session — whose resolved caller root is this repo routes here automatically, silent on match (TT-INV-12)",
});
tool_result_response(request.id.clone(), &packet)
}
}
}
async fn route_and_run(
app: Arc<AppState>,
request: JsonRpcRequest,
caller_root: Option<String>,
session_id: String,
) -> JsonRpcResponse {
let id = request.id.clone();
let result = tokio::time::timeout(
Duration::from_secs(MCP_TOOL_TIMEOUT_SECS),
tokio::task::spawn_blocking(move || {
if let Some(project_root) = bootstrap_project_root(&request) {
return run_bootstrap(&app, &request, &project_root, Some(&session_id));
}
if let Some(promote) = promote_request(&request) {
return run_promote(&app, &request, &promote);
}
let sticky = {
let sessions = app.mcp_sessions.lock();
sessions
.get(&session_id)
.and_then(|s| s.bound_project_root.clone())
};
if let Some(root) = sticky {
if let Some(brain) = app.project_brains.resolve(&root) {
return serve_and_compose(&app, brain, &request, caller_root.clone(), true);
}
}
if let Some(root) = caller_root.as_deref() {
let bound_covers = { app.session.lock().covers_root(root) };
if !bound_covers {
if let Some(brain) = app.project_brains.resolve(root) {
let key = crate::project_brains::ProjectBrainRegistry::canonical_key(root);
if let Some(s) = app.mcp_sessions.lock().get_mut(&session_id) {
s.bound_project_root = Some(key);
}
return serve_and_compose(&app, brain, &request, caller_root.clone(), true);
}
}
}
let response = serve_and_compose(
&app,
app.session.clone(),
&request,
caller_root.clone(),
false,
);
enrich_reception_with_roster(response, &app, caller_root.as_deref())
}),
)
.await;
match result {
Ok(Ok(resp)) => resp,
Ok(Err(_join_err)) => JsonRpcResponse {
jsonrpc: "2.0".into(),
id,
result: None,
error: Some(JsonRpcError {
code: -32603,
message: "Internal error: tool task panicked".into(),
data: None,
}),
},
Err(_elapsed) => JsonRpcResponse {
jsonrpc: "2.0".into(),
id,
result: None,
error: Some(JsonRpcError {
code: -32000,
message: format!("Tool execution exceeded {}s timeout", MCP_TOOL_TIMEOUT_SECS),
data: None,
}),
},
}
}
fn is_tier_recall_tool(tool: &str) -> bool {
matches!(
bare_tool_name(tool),
"seek" | "north" | "boot_memory" | "delegate"
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum MemoryTier {
Project,
Medulla,
ProjectAndMedulla,
AllBrains,
}
impl MemoryTier {
fn from_request(request: &JsonRpcRequest) -> MemoryTier {
let raw = request
.params
.get("arguments")
.and_then(|a| a.get("tier"))
.and_then(|v| v.as_str())
.unwrap_or("")
.trim();
match raw {
"project" => MemoryTier::Project,
"medulla" => MemoryTier::Medulla,
"all-brains" | "all_brains" => MemoryTier::AllBrains,
_ => MemoryTier::ProjectAndMedulla,
}
}
}
fn serve_and_compose(
app: &Arc<AppState>,
primary: Arc<Mutex<SessionState>>,
request: &JsonRpcRequest,
caller_root: Option<String>,
primary_is_project: bool,
) -> JsonRpcResponse {
let tool_name = if request.method == "tools/call" {
request.params.get("name").and_then(|v| v.as_str())
} else {
None
};
let Some(tool) = tool_name.filter(|t| is_tier_recall_tool(t)) else {
let mut state = primary.lock();
state.caller_root = caller_root;
return handle_mcp_method(&mut state, request);
};
let tier = MemoryTier::from_request(request);
let id = request.id.clone();
let (mut payload, primary_origin) = {
let mut state = primary.lock();
state.caller_root = caller_root.clone();
let args = request
.params
.get("arguments")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
if let Some(aid) = args.get("agent_id").and_then(|v| v.as_str()) {
state.track_agent(aid);
}
let origin = state.origin_brain();
match dispatch_tool(&mut state, bare_tool_name(tool), &args) {
Ok(v) => (v, origin),
Err(e) => return tool_error_response(id, e.to_string()),
}
};
if tier == MemoryTier::Project {
return tool_result_response(id, &payload);
}
let agent_id = request
.params
.get("arguments")
.and_then(|a| a.get("agent_id"))
.and_then(|v| v.as_str())
.unwrap_or("tier-recall")
.to_string();
let query = tier_recall_query(tool, request);
let mut folded: Vec<serde_json::Value> = Vec::new();
let want_medulla = matches!(
tier,
MemoryTier::Medulla | MemoryTier::ProjectAndMedulla | MemoryTier::AllBrains
);
if want_medulla && primary_is_project {
let medulla = app.session.clone();
folded.extend(store_recall_rows(
tool, &medulla, &agent_id, &query, "medulla",
));
}
if tier == MemoryTier::Medulla && primary_is_project {
strip_memory_feed(&mut payload, tool);
}
if tier == MemoryTier::AllBrains {
let primary_key = canonical_of(&primary_origin);
for (root, _facts, _dir) in app.project_brains.disk_roster() {
if canonical_of(&root) == primary_key {
continue; }
if let Some(brain) = app.project_brains.resolve(&root) {
let origin = { brain.lock().origin_brain() };
folded.extend(store_recall_rows(tool, &brain, &agent_id, &query, &origin));
}
}
}
if !folded.is_empty() {
append_memory_rows(&mut payload, tool, folded);
}
if let Some(obj) = payload.as_object_mut() {
obj.insert(
"tier".into(),
serde_json::json!(match tier {
MemoryTier::Project => "project",
MemoryTier::Medulla => "medulla",
MemoryTier::ProjectAndMedulla => "project+medulla",
MemoryTier::AllBrains => "all-brains",
}),
);
}
if bare_tool_name(tool) == "delegate"
&& payload.get("verdict").and_then(|v| v.as_str()) == Some("packet")
{
let budget_tokens = request
.params
.get("arguments")
.and_then(|a| a.get("budget"))
.and_then(|b| b.get("tokens"))
.and_then(|v| v.as_u64())
.unwrap_or(crate::delegation_handlers::DEFAULT_BUDGET_TOKENS)
.min(crate::delegation_handlers::HARD_BUDGET_TOKENS);
let md = crate::delegation_handlers::render_delegation_packet(&payload, budget_tokens);
if let Some(obj) = payload.as_object_mut() {
obj.insert("prompt_markdown".into(), serde_json::json!(md));
}
}
tool_result_response(id, &payload)
}
fn enrich_reception_with_roster(
response: JsonRpcResponse,
app: &Arc<AppState>,
caller_root: Option<&str>,
) -> JsonRpcResponse {
let Some(caller_root) = caller_root else {
return response; };
let Some(result) = response.result.as_ref() else {
return response;
};
let Some(text) = result
.get("content")
.and_then(|c| c.get(0))
.and_then(|c| c.get("text"))
.and_then(|t| t.as_str())
else {
return response;
};
let Ok(mut payload) = serde_json::from_str::<serde_json::Value>(text) else {
return response; };
let is_mismatch = payload
.get("reception")
.and_then(|r| r.get("match"))
.and_then(|m| m.as_str())
== Some("caller_root_mismatch");
if !is_mismatch {
return response;
}
let Some(brain_root) = app.project_brains.covering_brain(caller_root) else {
return response; };
if let Some(reception) = payload.get_mut("reception").and_then(|r| r.as_object_mut()) {
reception.insert("known_brain".into(), serde_json::json!(brain_root));
reception.insert(
"honest".into(),
serde_json::json!(
"this graph does NOT cover your repo — but a known project brain covers your \
caller root; rebind to it instead of the host cwd (reconnect-rebind, §C5.4)"
),
);
if let Some(options) = reception.get_mut("options").and_then(|o| o.as_array_mut()) {
for option in options.iter_mut() {
if option.get("action").and_then(|a| a.as_str()) == Some("ingest_your_repo") {
if let Some(obj) = option.as_object_mut() {
obj.insert(
"call".into(),
serde_json::json!(format!(
"ingest with project_root={brain_root} — a project brain already \
exists for this root; ONE call warm-resolves it and binds this \
session to it (a re-bind after reconnect, not a fresh birth), \
then every call from your root routes to it automatically"
)),
);
}
}
}
}
}
tool_result_response(response.id, &payload)
}
fn canonical_of(origin: &str) -> String {
if origin == "medulla" {
return origin.to_string();
}
crate::project_brains::ProjectBrainRegistry::canonical_key(origin)
}
fn tier_recall_query(tool: &str, request: &JsonRpcRequest) -> String {
let args = request.params.get("arguments");
let field = match bare_tool_name(tool) {
"seek" => "query",
"north" | "delegate" => "task",
_ => "",
};
args.and_then(|a| a.get(field))
.and_then(|v| v.as_str())
.map(str::to_string)
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| "memory decision finding note claim".to_string())
}
fn store_recall_rows(
tool: &str,
brain: &Arc<Mutex<SessionState>>,
agent_id: &str,
query: &str,
origin_brain: &str,
) -> Vec<serde_json::Value> {
match bare_tool_name(tool) {
"boot_memory" => store_boot_rows(brain, agent_id, origin_brain),
_ => store_memory_rows(brain, agent_id, query, origin_brain),
}
}
fn store_boot_rows(
brain: &Arc<Mutex<SessionState>>,
agent_id: &str,
origin_brain: &str,
) -> Vec<serde_json::Value> {
let mut state = brain.lock();
let this_tier = if state.is_medulla_store() {
"medulla"
} else {
"project"
};
let list = crate::boot_memory_handlers::handle_boot_memory(
&mut state,
crate::boot_memory_handlers::BootMemoryInput {
agent_id: agent_id.to_string(),
action: "list".into(),
key: None,
value: None,
tags: Vec::new(),
source_refs: Vec::new(),
},
);
list.ok()
.and_then(|v| v.get("entries").and_then(|e| e.as_array()).cloned())
.unwrap_or_default()
.into_iter()
.map(|mut entry| {
if let Some(obj) = entry.as_object_mut() {
obj.insert("origin_brain".into(), serde_json::json!(origin_brain));
obj.insert("tier".into(), serde_json::json!(this_tier));
}
entry
})
.collect()
}
fn store_memory_rows(
brain: &Arc<Mutex<SessionState>>,
agent_id: &str,
query: &str,
origin_brain: &str,
) -> Vec<serde_json::Value> {
const LIGHT_RECALL_SCOPE: &str = "light::";
let mut state = brain.lock();
let this_tier = if state.is_medulla_store() {
"medulla"
} else {
"project"
};
let out = crate::layer_handlers::handle_seek(
&mut state,
crate::protocol::layers::SeekInput {
query: query.to_string(),
agent_id: agent_id.to_string(),
top_k: 24,
scope: Some(LIGHT_RECALL_SCOPE.to_string()),
node_types: Vec::new(),
min_score: 0.1,
graph_rerank: true,
conformance_aware: true,
token_budget: None,
},
);
let now = now_ms();
let stale_after_ms: u64 = 30 * 24 * 60 * 60 * 1000;
let mut seen = std::collections::HashSet::new();
out.map(|o| o.results)
.unwrap_or_default()
.into_iter()
.filter(|r| r.source_agent.is_some() || r.authored_ms_ago.is_some())
.filter(|r| seen.insert(r.node_id.clone()))
.take(5)
.map(|r| {
let age_ms = r.authored_ms_ago;
let mut obj = serde_json::Map::new();
obj.insert("kind".into(), serde_json::json!("light"));
obj.insert("claim".into(), serde_json::json!(r.label));
if let Some(age) = age_ms {
obj.insert("age_ms".into(), serde_json::json!(age));
obj.insert("stale".into(), serde_json::json!(age > stale_after_ms));
}
obj.insert(
"source_agent".into(),
r.source_agent
.map(serde_json::Value::String)
.unwrap_or(serde_json::Value::Null),
);
let origin = r.origin_brain.unwrap_or_else(|| origin_brain.to_string());
obj.insert("origin_brain".into(), serde_json::json!(origin));
obj.insert("tier".into(), serde_json::json!(this_tier));
obj.insert("node_id".into(), serde_json::json!(r.node_id));
serde_json::Value::Object(obj)
})
.collect()
}
fn memory_feed_path(tool: &str) -> &'static [&'static str] {
match bare_tool_name(tool) {
"seek" => &["results"],
"boot_memory" => &["entries"],
"delegate" => &["context", "memory"],
_ => &["memory"],
}
}
fn memory_feed_mut<'a>(
payload: &'a mut serde_json::Value,
tool: &str,
) -> Option<&'a mut Vec<serde_json::Value>> {
let path = memory_feed_path(tool);
let (last, parents) = path.split_last()?;
let mut cursor = payload;
for key in parents {
let obj = cursor.as_object_mut()?;
cursor = obj
.entry((*key).to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
}
let obj = cursor.as_object_mut()?;
obj.entry((*last).to_string())
.or_insert_with(|| serde_json::Value::Array(Vec::new()))
.as_array_mut()
}
fn append_memory_rows(payload: &mut serde_json::Value, tool: &str, rows: Vec<serde_json::Value>) {
let Some(arr) = memory_feed_mut(payload, tool) else {
return;
};
let row_id = |r: &serde_json::Value| -> Option<String> {
r.get("node_id")
.and_then(|v| v.as_str())
.or_else(|| r.get("key").and_then(|v| v.as_str()))
.map(String::from)
};
let mut have: std::collections::HashSet<String> = arr.iter().filter_map(row_id).collect();
for row in rows {
if let Some(rid) = row_id(&row) {
if !have.insert(rid) {
continue; }
}
arr.push(row);
}
}
fn strip_memory_feed(payload: &mut serde_json::Value, tool: &str) {
if let Some(arr) = memory_feed_mut(payload, tool) {
arr.clear();
}
}
fn parse_message(body: &Bytes) -> Result<ParsedMessage, String> {
let value: serde_json::Value =
serde_json::from_slice(body).map_err(|e| format!("Invalid JSON: {}", e))?;
if value.is_array() {
return Err("JSON-RPC batches are not supported".into());
}
let has_id = value.get("id").is_some_and(|v| !v.is_null());
let has_method = value.get("method").and_then(|v| v.as_str()).is_some();
if has_id && has_method {
let req: JsonRpcRequest = serde_json::from_value(value)
.map_err(|e| format!("Malformed JSON-RPC request: {}", e))?;
Ok(ParsedMessage::Request(req))
} else {
Ok(ParsedMessage::NotificationOrResponse)
}
}
fn session_id_from_headers(headers: &HeaderMap) -> Option<String> {
headers
.get(MCP_SESSION_HEADER)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
fn caller_root_from_headers(headers: &HeaderMap) -> Option<String> {
headers
.get(CALLER_ROOT_HEADER)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
pub async fn handle_mcp_post(
axum::extract::State(app): axum::extract::State<Arc<AppState>>,
headers: HeaderMap,
body: Bytes,
) -> Response {
let incoming_session = session_id_from_headers(&headers);
let incoming_caller_root = caller_root_from_headers(&headers);
let parsed = match parse_message(&body) {
Ok(p) => p,
Err(msg) => {
return jsonrpc_error_response(
StatusCode::BAD_REQUEST,
serde_json::Value::Null,
-32700,
msg,
);
}
};
let request = match parsed {
ParsedMessage::NotificationOrResponse => {
if let Some(sid) = &incoming_session {
let mut reg = app.mcp_sessions.lock();
if let Some(s) = reg.get_mut(sid) {
s.last_seen_ms = now_ms();
}
}
return StatusCode::ACCEPTED.into_response();
}
ParsedMessage::Request(req) => req,
};
if request.method == "initialize" {
let session_id = generate_mcp_session_id();
let now = now_ms();
let response = run_mcp_method(app.clone(), request, incoming_caller_root.clone()).await;
let protocol_version = response
.result
.as_ref()
.and_then(|r| r.get("protocolVersion"))
.and_then(|v| v.as_str())
.unwrap_or(crate::server::MCP_PROTOCOL_VERSION)
.to_string();
{
let mut reg = app.mcp_sessions.lock();
reg.insert(
session_id.clone(),
McpTransportSession {
protocol_version,
created_ms: now,
last_seen_ms: now,
caller_root: incoming_caller_root,
bound_project_root: None,
},
);
}
return jsonrpc_ok_response(&response, Some(&session_id));
}
let session_id = match incoming_session {
None => {
return jsonrpc_error_response(
StatusCode::BAD_REQUEST,
request.id.clone(),
-32600,
"Missing Mcp-Session-Id header",
);
}
Some(sid) => sid,
};
let resolved_caller_root = {
let mut reg = app.mcp_sessions.lock();
match reg.get_mut(&session_id) {
None => {
return jsonrpc_error_response(
StatusCode::NOT_FOUND,
request.id.clone(),
-32001,
"Unknown or expired Mcp-Session-Id; re-initialize",
);
}
Some(s) => {
s.last_seen_ms = now_ms();
if incoming_caller_root.is_some() {
s.caller_root = incoming_caller_root.clone();
}
s.caller_root.clone()
}
}
};
let mutation_meta = mutation_event_meta(&request);
let response = route_and_run(
app.clone(),
request,
resolved_caller_root,
session_id.clone(),
)
.await;
if let Some((tool, agent_id)) = mutation_meta {
publish_graph_mutation_event(
&app,
&tool,
agent_id.as_deref(),
Some(session_id.as_str()),
response.error.is_none(),
);
}
jsonrpc_ok_response(&response, None)
}
fn mutation_event_meta(request: &JsonRpcRequest) -> Option<(String, Option<String>)> {
if request.method != "tools/call" {
return None;
}
let tool = request.params.get("name").and_then(|v| v.as_str())?;
if !GRAPH_MUTATION_TOOLS.contains(&bare_tool_name(tool)) {
return None;
}
let agent_id = request
.params
.get("arguments")
.and_then(|a| a.get("agent_id"))
.and_then(|v| v.as_str())
.map(str::to_owned);
Some((tool.to_string(), agent_id))
}
const ORIGIN_SESSION_FIELD: &str = "origin_mcp_session";
fn publish_graph_mutation_event(
app: &Arc<AppState>,
tool: &str,
agent_id: Option<&str>,
origin_session: Option<&str>,
success: bool,
) {
let mut data = serde_json::json!({
"tool": tool,
"source": "mcp_http",
"agent_id": agent_id,
"success": success,
"timestamp_ms": now_ms(),
});
if let Some(obj) = data.as_object_mut() {
obj.insert(
ORIGIN_SESSION_FIELD.to_string(),
match origin_session {
Some(sid) => serde_json::Value::String(sid.to_string()),
None => serde_json::Value::Null,
},
);
}
let sse_event = SseEvent {
event_type: "tool_result".to_string(),
data,
};
let _ = app.event_tx.send(sse_event);
}
#[allow(clippy::result_large_err)]
fn validate_session(app: &Arc<AppState>, headers: &HeaderMap) -> Result<String, Response> {
let session_id = match session_id_from_headers(headers) {
None => {
return Err((StatusCode::BAD_REQUEST, "Missing Mcp-Session-Id header").into_response());
}
Some(sid) => sid,
};
{
let mut reg = app.mcp_sessions.lock();
match reg.get_mut(&session_id) {
None => {
return Err((
StatusCode::NOT_FOUND,
"Unknown or expired Mcp-Session-Id; re-initialize",
)
.into_response());
}
Some(s) => {
s.last_seen_ms = now_ms();
}
}
}
Ok(session_id)
}
pub async fn handle_mcp_get(
axum::extract::State(app): axum::extract::State<Arc<AppState>>,
headers: HeaderMap,
) -> Response {
let own_session = match validate_session(&app, &headers) {
Ok(sid) => sid,
Err(resp) => return resp,
};
let rx = app.event_tx.subscribe();
let mut next_id: u64 = 0;
let stream = tokio_stream::wrappers::BroadcastStream::new(rx).filter_map(move |event| {
let frame = match event {
Ok(ref e) if event_origin_is(e, &own_session) => None,
Ok(e) => graph_changed_notification(&e),
Err(_) => None,
};
let item = frame.and_then(|notification| {
let id = next_id;
next_id += 1;
sse::Event::default()
.id(id.to_string())
.json_data(notification)
.ok()
.map(Ok::<_, std::convert::Infallible>)
});
async move { item }
});
Sse::new(stream)
.keep_alive(sse::KeepAlive::new().interval(Duration::from_secs(MCP_SSE_KEEPALIVE_SECS)))
.into_response()
}
pub async fn handle_mcp_delete(
axum::extract::State(app): axum::extract::State<Arc<AppState>>,
headers: HeaderMap,
) -> Response {
let session_id = match validate_session(&app, &headers) {
Ok(sid) => sid,
Err(resp) => return resp,
};
{
let mut reg = app.mcp_sessions.lock();
reg.remove(&session_id);
}
StatusCode::OK.into_response()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::{tool_schemas, McpConfig};
use crate::session::SessionState;
use m1nd_core::domain::DomainConfig;
use m1nd_core::graph::Graph;
use tokio::sync::broadcast;
fn ev(event_type: &str, data: serde_json::Value) -> SseEvent {
SseEvent {
event_type: event_type.to_string(),
data,
}
}
#[test]
fn read_tool_result_is_not_relayed() {
let e = ev(
"tool_result",
serde_json::json!({"tool": "seek", "success": true, "agent_id": "a"}),
);
assert!(graph_changed_notification(&e).is_none());
}
#[test]
fn mutation_tool_result_is_relayed_with_namespaced_method() {
let e = ev(
"tool_result",
serde_json::json!({
"tool": "memorize",
"success": true,
"agent_id": "agent-b",
"source": "http",
"timestamp_ms": 1234,
}),
);
let frame = graph_changed_notification(&e).expect("memorize relays");
assert_eq!(frame["jsonrpc"], "2.0");
assert_eq!(frame["method"], "notifications/m1nd/graph_changed");
assert_eq!(frame["params"]["event"], "memorize");
assert_eq!(frame["params"]["detail"]["agent_id"], "agent-b");
assert_eq!(frame["params"]["detail"]["kind"], "tool_result");
}
#[test]
fn event_from_own_session_is_recognized_as_self() {
let e = ev(
"tool_result",
serde_json::json!({
"tool": "ingest", "success": true, "origin_mcp_session": "sess-A",
}),
);
assert!(
event_origin_is(&e, "sess-A"),
"own session must be detected"
);
assert!(
!event_origin_is(&e, "sess-B"),
"another session must NOT see it as its own"
);
assert!(
graph_changed_notification(&e).is_some(),
"the event itself is still a real graph change (relayed to others)"
);
}
#[test]
fn event_without_origin_stamp_is_never_self() {
let e = ev(
"tool_result",
serde_json::json!({"tool": "memorize", "success": true}),
);
assert!(!event_origin_is(&e, "sess-A"));
assert!(!event_origin_is(&e, ""));
}
#[test]
fn prefixed_mutation_tool_is_relayed() {
for tool in ["m1nd.apply", "m1nd_apply", "apply"] {
let e = ev(
"tool_result",
serde_json::json!({"tool": tool, "success": true}),
);
assert!(
graph_changed_notification(&e).is_some(),
"{} should relay",
tool
);
}
}
#[test]
fn failed_mutation_is_suppressed() {
let e = ev(
"tool_result",
serde_json::json!({"tool": "ingest", "success": false}),
);
assert!(graph_changed_notification(&e).is_none());
}
#[test]
fn apply_batch_handoff_and_progress_relay() {
for et in ["apply_batch_handoff", "apply_batch_progress"] {
let e = ev(
et,
serde_json::json!({"tool": "apply_batch", "batch_id": "b1"}),
);
let frame = graph_changed_notification(&e).expect("relays");
assert_eq!(frame["params"]["event"], "apply_batch");
assert_eq!(frame["params"]["detail"]["batch_id"], "b1");
}
}
#[test]
fn read_tool_timeout_is_not_relayed_but_mutation_timeout_is() {
let read = ev("tool_timeout", serde_json::json!({"tool": "seek"}));
assert!(graph_changed_notification(&read).is_none());
let mutation = ev("tool_timeout", serde_json::json!({"tool": "ingest"}));
assert!(graph_changed_notification(&mutation).is_some());
}
#[test]
fn unrelated_event_types_are_dropped() {
for et in ["health", "heartbeat", "ui_refresh", "instance_changed"] {
let e = ev(et, serde_json::json!({"foo": "bar"}));
assert!(
graph_changed_notification(&e).is_none(),
"{} must not relay",
et
);
}
}
fn build_app_state(root: &std::path::Path) -> Arc<AppState> {
let runtime_dir = root.join("runtime");
std::fs::create_dir_all(&runtime_dir).expect("runtime dir");
let config = McpConfig {
graph_source: runtime_dir.join("graph.json"),
plasticity_state: runtime_dir.join("plasticity.json"),
runtime_dir: Some(runtime_dir),
..Default::default()
};
let session = SessionState::initialize(Graph::new(), &config, DomainConfig::code())
.expect("init session");
let (event_tx, _) = broadcast::channel::<SseEvent>(16);
let tool_schemas_cache = tool_schemas()
.get("tools")
.cloned()
.unwrap_or(serde_json::Value::Array(vec![]));
let project_brains = Arc::new(crate::project_brains::ProjectBrainRegistry::new(
root.join("runtime")
.join(crate::project_brains::PROJECT_BRAINS_DIR),
None,
));
Arc::new(AppState {
session: Arc::new(Mutex::new(session)),
tool_schemas_cache,
event_tx,
event_log_path: None,
registry_dir: None,
mcp_sessions: new_mcp_session_registry(),
project_brains,
})
}
fn header_map_with_session(sid: &str) -> HeaderMap {
let mut h = HeaderMap::new();
h.insert(MCP_SESSION_HEADER, sid.parse().unwrap());
h
}
fn seed_session(app: &Arc<AppState>) -> String {
let sid = generate_mcp_session_id();
let now = now_ms();
app.mcp_sessions.lock().insert(
sid.clone(),
McpTransportSession {
protocol_version: "test".into(),
created_ms: now,
last_seen_ms: now,
caller_root: None,
bound_project_root: None,
},
);
sid
}
#[tokio::test]
async fn get_missing_session_is_400() {
let temp = tempfile::tempdir().expect("tempdir");
let app = build_app_state(temp.path());
let resp = handle_mcp_get(axum::extract::State(app), HeaderMap::new()).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn get_unknown_session_is_404() {
let temp = tempfile::tempdir().expect("tempdir");
let app = build_app_state(temp.path());
let headers = header_map_with_session("does-not-exist");
let resp = handle_mcp_get(axum::extract::State(app), headers).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn get_known_session_opens_event_stream() {
let temp = tempfile::tempdir().expect("tempdir");
let app = build_app_state(temp.path());
let sid = seed_session(&app);
let headers = header_map_with_session(&sid);
let resp = handle_mcp_get(axum::extract::State(app), headers).await;
assert_eq!(resp.status(), StatusCode::OK);
let ct = resp
.headers()
.get(axum::http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or_default();
assert!(
ct.starts_with("text/event-stream"),
"expected SSE content-type, got {ct}"
);
}
#[tokio::test]
async fn delete_missing_session_is_400() {
let temp = tempfile::tempdir().expect("tempdir");
let app = build_app_state(temp.path());
let resp = handle_mcp_delete(axum::extract::State(app), HeaderMap::new()).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn delete_unknown_session_is_404() {
let temp = tempfile::tempdir().expect("tempdir");
let app = build_app_state(temp.path());
let headers = header_map_with_session("nope");
let resp = handle_mcp_delete(axum::extract::State(app), headers).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn delete_removes_session_then_revalidation_is_404() {
let temp = tempfile::tempdir().expect("tempdir");
let app = build_app_state(temp.path());
let sid = seed_session(&app);
let resp = handle_mcp_delete(
axum::extract::State(app.clone()),
header_map_with_session(&sid),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
assert!(!app.mcp_sessions.lock().contains_key(&sid));
let resp2 = handle_mcp_get(
axum::extract::State(app.clone()),
header_map_with_session(&sid),
)
.await;
assert_eq!(resp2.status(), StatusCode::NOT_FOUND);
let body = axum::body::Bytes::from(
serde_json::to_vec(&serde_json::json!({
"jsonrpc": "2.0", "id": 1, "method": "tools/list"
}))
.unwrap(),
);
let resp3 = handle_mcp_post(
axum::extract::State(app),
header_map_with_session(&sid),
body,
)
.await;
assert_eq!(resp3.status(), StatusCode::NOT_FOUND);
}
}