use crate::layer_handlers;
use crate::personality;
use crate::protocol::layers;
use crate::protocol::*;
use crate::report_handlers;
use crate::search_handlers;
use crate::session::SessionState;
use crate::surgical_handlers;
use crate::tools;
use m1nd_core::domain::DomainConfig;
use m1nd_core::error::{M1ndError, M1ndResult};
use std::io::{BufRead, Read, Write};
use std::path::PathBuf;
const M1ND_INSTRUCTIONS: &str = "\
m1nd is a neuro-symbolic code graph engine. It ingests codebases into a weighted \
graph and provides spreading-activation queries, impact analysis, prediction, and \
stateful perspective navigation. All tool calls require an `agent_id` parameter.
## WORKFLOWS
**Session Start**: `health` → `drift` (recover what changed since last session) → \
`ingest` (if graph is empty or stale). This gives you codebase-aware context.
**Research**: `ingest` → `activate(query)` → `why(source, target)` → `missing(topic)` → \
`learn(feedback)`. Use `seek` for keyword search, `scan` for broad discovery, \
`trace` for dependency chains, `timeline` for temporal ordering.
**Code Change**: `impact(node)` (blast radius) → `predict(node)` (co-change likelihood) → \
`counterfactual(nodes)` (simulate removal) → `warmup(task_description)` (prime graph). \
Use `differential` to compare two subgraphs. Use `hypothesize` to test what-if scenarios.
**Deep Analysis**: `resonate(query)` for standing-wave harmonic patterns. \
`fingerprint(nodes)` for duplicate/equivalence detection. `diverge(node)` for \
exploring unexpected connections. `federate` to query across graph namespaces.
## PERSPECTIVE SYSTEM (stateful navigation)
Perspectives are named, agent-scoped navigation sessions through the graph. \
Flow: `perspective_start(name, seed_nodes)` → `perspective_follow(node)` (move focus) → \
`perspective_branch(name)` (fork exploration) → `perspective_back` (undo last move) → \
`perspective_close`. Use `perspective_inspect` to see current state, `perspective_peek` \
to look at a node without moving, `perspective_list` for all open perspectives, \
`perspective_compare` to diff two perspectives, `perspective_suggest` for next-step \
recommendations, `perspective_routes` for paths between nodes, `perspective_affinity` \
for related-node scoring.
## CONCURRENCY & STATE
`lock_create` / `lock_release` — advisory locks for multi-agent coordination on graph \
regions. `lock_watch` monitors lock state. `lock_diff` shows changes within a lock scope. \
`lock_rebase` replays external changes into a locked region. \
`trail_save` / `trail_list` / `trail_resume` / `trail_merge` — persist and restore \
exploration trails across sessions. `validate_plan` checks a proposed multi-step plan \
for structural soundness.
## CRITICAL PATTERNS
1. **Always call `learn` after using `activate` results.** Feedback (correct/wrong/partial) \
trains the graph weights via Hebbian learning. Skipping this degrades future queries.
2. **Use `ingest` at session start** if the graph has zero nodes or the codebase changed.
3. **Use `drift` to recover context** between sessions — it shows weight changes since \
a baseline timestamp.
4. **`warmup` before focused work** — primes activation patterns for a specific task, \
making subsequent queries faster and more relevant.
5. **Never call `activate` without `agent_id`** — multi-agent isolation depends on it.
6. **Prefer `impact` over `activate` for code changes** — impact gives directional \
blast-radius analysis; activate gives associative exploration.
7. **Graph persists automatically** every 50 queries and on shutdown. Use `trail_save` \
for explicit exploration checkpoints.
8. **Use `boot_memory` for small canonical doctrine/state** that should persist quickly \
and stay hot in runtime memory without polluting trails or transcripts.
";
#[derive(Clone, Copy, Debug)]
enum TransportMode {
Framed,
Line,
}
fn read_request_payload<R: BufRead>(
reader: &mut R,
) -> std::io::Result<Option<(String, TransportMode)>> {
loop {
let buffer = reader.fill_buf()?;
if buffer.is_empty() {
return Ok(None);
}
let first_non_ws = buffer
.iter()
.copied()
.find(|byte| !byte.is_ascii_whitespace());
let starts_framed = matches!(first_non_ws, Some(byte) if byte != b'{' && byte != b'[');
if starts_framed {
let mut content_length: Option<usize> = None;
loop {
let mut header_line = String::new();
let bytes = reader.read_line(&mut header_line)?;
if bytes == 0 {
return Ok(None);
}
let trimmed = header_line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() {
break;
}
if let Some((name, value)) = trimmed.split_once(':') {
if name.trim().eq_ignore_ascii_case("Content-Length") {
content_length = value.trim().parse::<usize>().ok();
}
}
}
let length = content_length.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Missing Content-Length header",
)
})?;
let mut body = vec![0_u8; length];
reader.read_exact(&mut body)?;
let payload = String::from_utf8(body)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
return Ok(Some((payload, TransportMode::Framed)));
}
let mut line = String::new();
let bytes = reader.read_line(&mut line)?;
if bytes == 0 {
return Ok(None);
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
return Ok(Some((trimmed.to_owned(), TransportMode::Line)));
}
}
fn write_response<W: Write>(
writer: &mut W,
response: &JsonRpcResponse,
mode: TransportMode,
) -> std::io::Result<()> {
let json = serde_json::to_string(response).unwrap_or_default();
match mode {
TransportMode::Framed => {
write!(writer, "Content-Length: {}\r\n\r\n{}", json.len(), json)?;
}
TransportMode::Line => {
writeln!(writer, "{}", json)?;
}
}
writer.flush()
}
fn normalize_dispatch_tool_name(tool_name: &str) -> String {
let mut normalized = tool_name.strip_prefix("m1nd.").unwrap_or(tool_name);
normalized = normalized.strip_prefix("m1nd_").unwrap_or(normalized);
let normalized = normalized.replace('.', "_");
if normalized == "status" {
"health".to_string()
} else {
normalized
}
}
fn tool_schema_by_name(tool_name: &str) -> Option<serde_json::Value> {
tool_schemas()
.get("tools")
.and_then(|tools| tools.as_array())
.and_then(|tools| {
tools.iter().find(|schema| {
schema.get("name").and_then(|value| value.as_str()) == Some(tool_name)
})
})
.cloned()
}
fn tool_required_fields(tool_name: &str) -> Vec<String> {
tool_schema_by_name(tool_name)
.and_then(|schema| schema.get("inputSchema").cloned())
.and_then(|schema| schema.get("required").cloned())
.and_then(|required| required.as_array().cloned())
.map(|required| {
required
.iter()
.filter_map(|value| value.as_str().map(ToOwned::to_owned))
.collect()
})
.unwrap_or_default()
}
fn schema_example_value(schema: &serde_json::Value) -> serde_json::Value {
if let Some(default) = schema.get("default") {
return default.clone();
}
match schema.get("type").and_then(|value| value.as_str()) {
Some("string") => serde_json::json!("..."),
Some("integer") => serde_json::json!(1),
Some("number") => serde_json::json!(0.5),
Some("boolean") => serde_json::json!(true),
Some("array") => {
let item = schema
.get("items")
.map(schema_example_value)
.unwrap_or(serde_json::Value::Null);
serde_json::Value::Array(vec![item])
}
Some("object") => serde_json::Value::Object(serde_json::Map::new()),
_ => serde_json::json!("..."),
}
}
fn tool_minimal_example(tool_name: &str) -> Option<serde_json::Value> {
let schema = tool_schema_by_name(tool_name)?;
let input_schema = schema.get("inputSchema")?;
let properties = input_schema.get("properties")?.as_object()?;
let required = input_schema
.get("required")
.and_then(|value| value.as_array())
.cloned()
.unwrap_or_default();
let mut arguments = serde_json::Map::new();
for key in required.iter().filter_map(|value| value.as_str()) {
if let Some(property_schema) = properties.get(key) {
arguments.insert(key.to_string(), schema_example_value(property_schema));
}
}
Some(serde_json::json!({
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": arguments,
}
}))
}
fn tool_usage_hint(tool_name: &str) -> Option<&'static str> {
match tool_name {
"seek" => Some("Use `seek` for intent/natural-language retrieval. For exact text or regex, prefer `search`."),
"search" | "m1nd_search" => Some("Use `search` for exact text, regex, or filename-filtered lookup. For intent-based retrieval, prefer `seek`."),
"glob" | "m1nd_glob" => Some("Use `glob` for file/path discovery with shell-style patterns. For content lookup inside files, prefer `search`."),
"trace" => Some("Use `trace` when you have a real stacktrace or error output. If you only have a symbol or file, start with `seek` or `view`."),
"validate_plan" => Some("Use `validate_plan` after you already know which files/actions you want to change. If you still need connected edit context, start with `surgical_context_v2`."),
"surgical_context_v2" => Some("Use `surgical_context_v2` to prepare connected edits around a file. For direct text lookup, use `search` or `view` first."),
"apply_batch" => Some("Use `apply_batch` for coordinated multi-file writes after you already have validated content. If you still need to inspect risk, call `validate_plan` first."),
"trail_resume" | "trail.resume" => Some("Use `trail_resume` to continue a saved investigation. If you do not have a saved trail yet, start with `trail_list` or a fresh query tool."),
"timeline" => Some("Use `timeline` for git-backed historical proof on a known node or file. If you still need to locate the target, start with `seek`, `search`, or `activate`."),
"perspective_start" => Some("Use `perspective_start` to begin a stateful navigation session. Then use `perspective_routes`, `perspective_follow`, and `perspective_peek` inside that session."),
_ => None,
}
}
fn invalid_params_recovery(tool: &str, detail: &str) -> Option<serde_json::Value> {
if tool == "trail.resume" && detail.contains("Use force=true to resume.") {
return Some(serde_json::json!({
"hint": "This saved trail has drifted too far from the current graph for a normal resume. If partial continuity is still useful, retry with `force=true`.",
"suggested_next_step": "Retry `trail_resume` with `force=true` to recover degraded continuity, or start a new investigation if the trail is no longer trustworthy.",
"workflow_hint": "Typical recovery flow: `trail_resume(force=true) -> inspect next_focus / resume_hints -> continue or restart`.",
"example": {
"method": "tools/call",
"params": {
"name": "trail_resume",
"arguments": {
"agent_id": "dev",
"trail_id": "trail_123",
"force": true
}
}
}
}));
}
if tool == "m1nd_search" && detail.contains("query cannot be empty") {
return Some(serde_json::json!({
"hint": "This search is missing content to search for. If you are trying to discover files or paths, reroute to `glob`; if you know the intent but not the exact text, use `seek`.",
"suggested_next_step": "Retry with `glob` for file discovery, or retry `search` with an exact text/regex query.",
"workflow_hint": "Typical recovery flow: `glob -> view` for file discovery, or `search(literal/regex) -> view` for content lookup.",
"example": {
"method": "tools/call",
"params": {
"name": "glob",
"arguments": {
"agent_id": "dev",
"pattern": "m1nd-mcp/src/**/*search*.rs"
}
}
}
}));
}
if tool == "m1nd_search" && detail.contains("resolves to") && detail.contains("candidate paths")
{
return Some(serde_json::json!({
"hint": "The scope is ambiguous across multiple candidate paths. Retry with a more specific or absolute scope so `search` only targets one root.",
"suggested_next_step": "Retry `search` with an explicit scope path, or disable `auto_ingest` if you only want current roots.",
"workflow_hint": "Typical recovery flow: `search(scope=<exact path>) -> view`.",
}));
}
if tool == "m1nd_glob" && detail.contains("pattern cannot be empty") {
return Some(serde_json::json!({
"hint": "This glob call is missing a file pattern. Use shell-style globs such as `src/**/*.rs`, or switch to `search` if you meant to look for content.",
"suggested_next_step": "Retry `glob` with a file/path pattern, or use `search` for content lookup.",
"workflow_hint": "Typical recovery flow: `glob(pattern) -> view` for file discovery, or `search(query) -> view` for content lookup.",
"example": {
"method": "tools/call",
"params": {
"name": "glob",
"arguments": {
"agent_id": "dev",
"pattern": "src/**/*.rs"
}
}
}
}));
}
if tool == "m1nd_glob" && detail.contains("invalid glob pattern") {
return Some(serde_json::json!({
"hint": "The glob pattern is invalid or content-shaped. Use shell-style globs for file discovery, or switch to `search` when you want to find text inside files.",
"suggested_next_step": "Retry `glob` with a valid shell-style pattern, or reroute to `search` in literal mode for exact text.",
"workflow_hint": "Typical recovery flow: `search(literal) -> view` if the original pattern was actually content.",
}));
}
if tool == "edit_commit" && detail.contains("confirm must be true to commit") {
return Some(serde_json::json!({
"hint": "The preview is still valid, but commit is gated until you explicitly confirm it. Reuse the same `preview_id` with `confirm=true` after reviewing the preview.",
"suggested_next_step": "Retry `edit_commit` with the same `preview_id` and `confirm=true`.",
"workflow_hint": "Typical recovery flow: `edit_preview -> review -> edit_commit(confirm=true)`.",
"example": {
"method": "tools/call",
"params": {
"name": "edit_commit",
"arguments": {
"preview_id": "preview_dev_001",
"agent_id": "dev",
"confirm": true
}
}
}
}));
}
if tool == "edit_commit" && detail.contains("preview_id not found or expired") {
return Some(serde_json::json!({
"hint": "That preview handle is gone or expired. Mint a fresh preview for the same file, then retry the commit with the new `preview_id`.",
"suggested_next_step": "Call `edit_preview` again for the same file, then retry `edit_commit` with the new `preview_id`.",
"workflow_hint": "Typical recovery flow: `edit_preview -> edit_commit`.",
"example": {
"method": "tools/call",
"params": {
"name": "edit_preview",
"arguments": {
"file_path": "src/auth.rs",
"agent_id": "dev",
"new_content": "..."
}
}
}
}));
}
None
}
pub(crate) fn tool_error_payload(error: &M1ndError) -> serde_json::Value {
match error {
M1ndError::UnknownTool { name } => serde_json::json!({
"error_type": "unknown_tool",
"message": format!("Unknown tool `{}`.", name),
"detail": error.to_string(),
"hint": "Use canonical MCP tool names with underscores, not transport aliases. First call `tools/list` to inspect the available tools.",
"suggested_next_step": "Retry with the canonical tool name, for example `trail_resume`, `perspective_start`, or `apply_batch`.",
"example": {
"method": "tools/list"
},
}),
M1ndError::InvalidParams { tool, detail } => {
let required_fields = tool_required_fields(tool);
let example = tool_minimal_example(tool);
let mut payload = serde_json::json!({
"error_type": "invalid_params",
"tool": tool,
"message": format!("Invalid params for `{}`.", tool),
"detail": detail,
"hint": format!("Check the required fields for `{}` and retry with the MCP `tools/call` envelope.", tool),
"required_fields": required_fields,
"suggested_next_step": format!("Call `help` or `tools/list`, then retry `{}` with the required arguments.", tool),
});
if let Some(example) = example {
payload["example"] = example;
}
if let Some(usage_hint) = tool_usage_hint(tool) {
payload["usage_hint"] = serde_json::json!(usage_hint);
}
if let Some(recovery) = invalid_params_recovery(tool, detail) {
if let Some(hint) = recovery.get("hint") {
payload["hint"] = hint.clone();
}
if let Some(step) = recovery.get("suggested_next_step") {
payload["suggested_next_step"] = step.clone();
}
if let Some(workflow_hint) = recovery.get("workflow_hint") {
payload["workflow_hint"] = workflow_hint.clone();
}
if let Some(example) = recovery.get("example") {
payload["example"] = example.clone();
}
}
payload
}
M1ndError::EmptyGraph => serde_json::json!({
"error_type": "empty_graph",
"message": "The active graph is empty.",
"detail": error.to_string(),
"hint": "Run `ingest` first so m1nd has code or documents to analyze.",
"suggested_next_step": "Use `ingest`, then retry `activate`, `search`, `seek`, or the tool you originally called.",
"example": tool_minimal_example("ingest"),
"workflow_hint": "Typical recovery flow: `ingest -> activate/search`.",
}),
M1ndError::PerspectiveNotFound {
perspective_id,
agent_id,
} => serde_json::json!({
"error_type": "perspective_not_found",
"message": format!("Perspective `{}` was not found for agent `{}`.", perspective_id, agent_id),
"detail": error.to_string(),
"hint": "The perspective may be stale or closed. Inspect active perspectives before retrying.",
"suggested_next_step": "Call `perspective_list` to inspect active sessions, or create a fresh one with `perspective_start`.",
"example": tool_minimal_example("perspective_start"),
}),
M1ndError::NoEntryPoints => serde_json::json!({
"error_type": "no_entry_points",
"message": "The requested flow simulation has no entry points.",
"detail": error.to_string(),
"hint": "Pass explicit `entry_nodes`, or narrow the query/scope until a valid starting point exists.",
"suggested_next_step": "Locate a concrete node first with `seek`, `search`, or `activate`, then retry with `entry_nodes`.",
}),
M1ndError::NoValidInfectedNodes => serde_json::json!({
"error_type": "no_valid_infected_nodes",
"message": "No valid infected nodes were found for epidemic analysis.",
"detail": error.to_string(),
"hint": "Retry with existing node IDs such as `file::...` or function IDs that are already in the graph.",
"suggested_next_step": "Use `seek`, `search`, or `activate` to recover valid node identifiers before calling `epidemic`.",
}),
M1ndError::Serde(detail) => serde_json::json!({
"error_type": "invalid_json",
"message": "The request body was not valid JSON.",
"detail": detail.to_string(),
"hint": "Use a valid JSON object. For MCP tool calls, the request shape is `{\"method\":\"tools/call\",\"params\":{\"name\":\"tool\",\"arguments\":{...}}}`.",
"suggested_next_step": "Fix the JSON envelope and retry the request.",
}),
_ => serde_json::json!({
"error_type": "internal",
"message": "The tool call failed.",
"detail": error.to_string(),
}),
}
}
pub(crate) fn timeout_error_payload(timeout_secs: u64) -> serde_json::Value {
serde_json::json!({
"error_type": "timeout",
"message": format!("Tool execution exceeded the {}s limit.", timeout_secs),
"detail": format!("Tool execution exceeded {}s limit.", timeout_secs),
"hint": "Narrow `scope`, lower `top_k`, or split the work into smaller calls. For long write flows, prefer `apply_batch` and watch its progress phases.",
"suggested_next_step": "Retry with a smaller target set or a more focused query.",
})
}
pub(crate) fn parse_error_payload(detail: &str) -> serde_json::Value {
serde_json::json!({
"error_type": "invalid_jsonrpc",
"message": "Invalid JSON-RPC request.",
"detail": detail,
"hint": "Expected `jsonrpc`, `id`, `method`, and `params`. For tool calls, use `tools/call` with `{ \"name\": \"tool\", \"arguments\": { ... } }`.",
"example": {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "seek",
"arguments": {
"agent_id": "dev",
"query": "find the dispatch alias normalization helper"
}
}
}
})
}
pub(crate) fn method_not_found_payload(method: &str) -> serde_json::Value {
serde_json::json!({
"error_type": "method_not_found",
"message": format!("Unknown JSON-RPC method `{}`.", method),
"detail": format!("Method not found: {}", method),
"hint": "Use MCP protocol methods like `initialize`, `tools/list`, and `tools/call`.",
"suggested_next_step": "Retry with `tools/list` to inspect tools, or `tools/call` to execute one.",
})
}
#[derive(Clone, Debug, serde::Deserialize)]
pub struct McpConfig {
pub graph_source: PathBuf,
pub plasticity_state: PathBuf,
#[serde(default)]
pub runtime_dir: Option<PathBuf>,
pub auto_persist_interval: u32,
pub learning_rate: f32,
pub decay_rate: f32,
pub xlr_enabled: bool,
pub max_concurrent_reads: usize,
pub write_queue_size: usize,
#[serde(default)]
pub domain: Option<String>,
}
impl Default for McpConfig {
fn default() -> Self {
Self {
graph_source: PathBuf::from("./graph_snapshot.json"),
plasticity_state: PathBuf::from("./plasticity_state.json"),
runtime_dir: None,
auto_persist_interval: 50,
learning_rate: 0.08,
decay_rate: 0.005,
xlr_enabled: true,
max_concurrent_reads: 32,
write_queue_size: 64,
domain: None,
}
}
}
pub struct McpServer {
config: McpConfig,
state: SessionState,
}
pub fn tool_schemas() -> serde_json::Value {
serde_json::json!({
"tools": [
{
"name": "activate",
"description": "Spreading activation query across the connectome",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query for spreading activation" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"top_k": { "type": "integer", "default": 20, "description": "Number of top results to return" },
"dimensions": {
"type": "array",
"items": { "type": "string", "enum": ["structural", "semantic", "temporal", "causal"] },
"default": ["structural", "semantic", "temporal", "causal"],
"description": "Activation dimensions to include"
},
"xlr": { "type": "boolean", "default": true, "description": "Enable XLR noise cancellation" },
"include_ghost_edges": { "type": "boolean", "default": true, "description": "Include ghost edge detection" },
"include_structural_holes": { "type": "boolean", "default": false, "description": "Include structural hole detection" }
},
"required": ["query", "agent_id"]
}
},
{
"name": "impact",
"description": "Impact radius / blast analysis for a node",
"inputSchema": {
"type": "object",
"properties": {
"node_id": { "type": "string", "description": "Target node identifier" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"direction": {
"type": "string",
"enum": ["forward", "reverse", "both"],
"default": "forward",
"description": "Propagation direction for impact analysis"
},
"include_causal_chains": { "type": "boolean", "default": true, "description": "Include causal chain detection" }
},
"required": ["node_id", "agent_id"]
}
},
{
"name": "missing",
"description": "Detect structural holes and missing connections",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query to find structural holes around" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"min_sibling_activation": { "type": "number", "default": 0.3, "description": "Minimum sibling activation threshold" }
},
"required": ["query", "agent_id"]
}
},
{
"name": "why",
"description": "Path explanation between two nodes",
"inputSchema": {
"type": "object",
"properties": {
"source": { "type": "string", "description": "Source node identifier" },
"target": { "type": "string", "description": "Target node identifier" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"max_hops": { "type": "integer", "default": 6, "description": "Maximum hops in path search" }
},
"required": ["source", "target", "agent_id"]
}
},
{
"name": "warmup",
"description": "Task-based warmup and priming",
"inputSchema": {
"type": "object",
"properties": {
"task_description": { "type": "string", "description": "Description of the task to warm up for" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"boost_strength": { "type": "number", "default": 0.15, "description": "Priming boost strength" }
},
"required": ["task_description", "agent_id"]
}
},
{
"name": "counterfactual",
"description": "What-if node removal simulation",
"inputSchema": {
"type": "object",
"properties": {
"node_ids": {
"type": "array",
"items": { "type": "string" },
"description": "Node identifiers to simulate removal of"
},
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"include_cascade": { "type": "boolean", "default": true, "description": "Include cascade analysis" }
},
"required": ["node_ids", "agent_id"]
}
},
{
"name": "predict",
"description": "Co-change prediction for a modified node",
"inputSchema": {
"type": "object",
"properties": {
"changed_node": { "type": "string", "description": "Node identifier that was changed" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"top_k": { "type": "integer", "default": 10, "description": "Number of top predictions to return" },
"include_velocity": { "type": "boolean", "default": true, "description": "Include velocity scoring" }
},
"required": ["changed_node", "agent_id"]
}
},
{
"name": "fingerprint",
"description": "Activation fingerprint and equivalence detection",
"inputSchema": {
"type": "object",
"properties": {
"target_node": { "type": "string", "description": "Optional target node to find equivalents for" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"similarity_threshold": { "type": "number", "default": 0.85, "description": "Cosine similarity threshold for equivalence" },
"probe_queries": {
"type": "array",
"items": { "type": "string" },
"description": "Optional probe queries for fingerprinting"
}
},
"required": ["agent_id"]
}
},
{
"name": "drift",
"description": "Weight and structural drift analysis",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"since": { "type": "string", "default": "last_session", "description": "Baseline reference point for drift comparison" },
"include_weight_drift": { "type": "boolean", "default": true, "description": "Include edge weight drift analysis" }
},
"required": ["agent_id"]
}
},
{
"name": "learn",
"description": "Explicit feedback-based edge adjustment",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Original query this feedback relates to" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"feedback": {
"type": "string",
"enum": ["correct", "wrong", "partial"],
"description": "Feedback type: correct, wrong, or partial"
},
"node_ids": {
"type": "array",
"items": { "type": "string" },
"description": "Node identifiers to apply feedback to"
},
"strength": { "type": "number", "default": 0.2, "description": "Feedback strength for edge adjustment" }
},
"required": ["query", "agent_id", "feedback", "node_ids"]
}
},
{
"name": "ingest",
"description": "Ingest or re-ingest a codebase, descriptor, or memory corpus",
"inputSchema": {
"type": "object",
"properties": {
"path": { "type": "string", "description": "Filesystem path to the source root or memory corpus" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"incremental": { "type": "boolean", "default": false, "description": "Incremental ingest (code adapter only)" },
"adapter": {
"type": "string",
"default": "code",
"enum": ["code", "json", "memory", "light"],
"description": "Adapter to use for parsing the input corpus"
},
"mode": {
"type": "string",
"default": "replace",
"enum": ["replace", "merge"],
"description": "Replace the active graph or merge the ingest into it"
},
"namespace": {
"type": "string",
"description": "Optional namespace tag for memory/non-code nodes"
}
},
"required": ["path", "agent_id"]
}
},
{
"name": "resonate",
"description": "Resonance analysis: harmonics, sympathetic pairs, and resonant frequencies",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query to find seed nodes for resonance analysis" },
"node_id": { "type": "string", "description": "Specific node identifier to use as seed (alternative to query)" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"top_k": { "type": "integer", "default": 20, "description": "Number of top resonance results to return" }
},
"required": ["agent_id"]
}
},
{
"name": "health",
"description": "Server health and statistics",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" }
},
"required": ["agent_id"]
}
},
{
"name": "perspective_start",
"description": "Enter a perspective: creates a navigable route surface from a query",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"query": { "type": "string", "description": "Seed query for route synthesis" },
"anchor_node": { "type": "string", "description": "Optional: anchor to a specific node (activates anchored mode)" },
"lens": { "type": "object", "description": "Optional: starting lens configuration" }
},
"required": ["agent_id", "query"]
}
},
{
"name": "perspective_routes",
"description": "Browse the current route set with pagination",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string" },
"perspective_id": { "type": "string" },
"page": { "type": "integer", "default": 1, "description": "Page number (1-based)" },
"page_size": { "type": "integer", "default": 6, "description": "Routes per page (clamped to 1-10)" },
"route_set_version": { "type": "integer", "description": "Version from previous response for staleness check" }
},
"required": ["agent_id", "perspective_id"]
}
},
{
"name": "perspective_inspect",
"description": "Expand a route with fuller path, metrics, provenance, and affinity",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string" },
"perspective_id": { "type": "string" },
"route_id": { "type": "string", "description": "Stable content-addressed route ID" },
"route_index": { "type": "integer", "description": "1-based page-local position" },
"route_set_version": { "type": "integer" }
},
"required": ["agent_id", "perspective_id", "route_set_version"]
}
},
{
"name": "perspective_peek",
"description": "Extract a small relevant code/doc slice from a route target",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string" },
"perspective_id": { "type": "string" },
"route_id": { "type": "string" },
"route_index": { "type": "integer" },
"route_set_version": { "type": "integer" }
},
"required": ["agent_id", "perspective_id", "route_set_version"]
}
},
{
"name": "perspective_follow",
"description": "Follow a route: move focus to target, synthesize new routes",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string" },
"perspective_id": { "type": "string" },
"route_id": { "type": "string" },
"route_index": { "type": "integer" },
"route_set_version": { "type": "integer" }
},
"required": ["agent_id", "perspective_id", "route_set_version"]
}
},
{
"name": "perspective_suggest",
"description": "Get the next best move suggestion based on navigation history",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string" },
"perspective_id": { "type": "string" },
"route_set_version": { "type": "integer" }
},
"required": ["agent_id", "perspective_id", "route_set_version"]
}
},
{
"name": "perspective_affinity",
"description": "Discover probable connections a route target might have",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string" },
"perspective_id": { "type": "string" },
"route_id": { "type": "string" },
"route_index": { "type": "integer" },
"route_set_version": { "type": "integer" }
},
"required": ["agent_id", "perspective_id", "route_set_version"]
}
},
{
"name": "perspective_branch",
"description": "Fork the current navigation state into a new branch",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string" },
"perspective_id": { "type": "string" },
"branch_name": { "type": "string", "description": "Optional branch name" }
},
"required": ["agent_id", "perspective_id"]
}
},
{
"name": "perspective_back",
"description": "Navigate back to previous focus, restoring checkpoint state",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string" },
"perspective_id": { "type": "string" }
},
"required": ["agent_id", "perspective_id"]
}
},
{
"name": "perspective_compare",
"description": "Compare two perspectives on shared/unique nodes and dimension deltas",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string" },
"perspective_id_a": { "type": "string" },
"perspective_id_b": { "type": "string" },
"dimensions": { "type": "array", "items": { "type": "string" }, "description": "Dimensions to compare (empty = all)" }
},
"required": ["agent_id", "perspective_id_a", "perspective_id_b"]
}
},
{
"name": "perspective_list",
"description": "List all perspectives for an agent",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string" }
},
"required": ["agent_id"]
}
},
{
"name": "perspective_close",
"description": "Close a perspective and release associated locks",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string" },
"perspective_id": { "type": "string" }
},
"required": ["agent_id", "perspective_id"]
}
},
{
"name": "seek",
"description": "Intent-aware semantic code search — find code by PURPOSE, not text pattern",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Natural language description of what the agent is looking for" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"top_k": { "type": "integer", "default": 20, "description": "Maximum results to return" },
"scope": { "type": "string", "description": "File path prefix to limit search scope" },
"node_types": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Filter by node type: function, class, struct, module, file" },
"min_score": { "type": "number", "default": 0.1, "description": "Minimum combined score threshold" },
"graph_rerank": { "type": "boolean", "default": true, "description": "Whether to run graph re-ranking on embedding candidates" }
},
"required": ["query", "agent_id"]
}
},
{
"name": "scan",
"description": "Pattern-aware structural code analysis with graph-validated findings",
"inputSchema": {
"type": "object",
"properties": {
"pattern": { "type": "string", "description": "Pattern ID (error_handling, resource_cleanup, api_surface, state_mutation, concurrency, auth_boundary, test_coverage, dependency_injection) or custom pattern" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"scope": { "type": "string", "description": "File path prefix to limit scan scope" },
"severity_min": { "type": "number", "default": 0.3, "description": "Minimum severity threshold [0.0, 1.0]" },
"graph_validate": { "type": "boolean", "default": true, "description": "Whether to validate findings against graph edges" },
"limit": { "type": "integer", "default": 50, "description": "Maximum findings to return" }
},
"required": ["pattern", "agent_id"]
}
},
{
"name": "timeline",
"description": "Git-based temporal history for a node — changes, co-changes, velocity, stability",
"inputSchema": {
"type": "object",
"properties": {
"node": { "type": "string", "description": "Node external_id (e.g. file::backend/chat_handler.py)" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"depth": { "type": "string", "default": "30d", "description": "Time depth: 7d, 30d, 90d, all" },
"include_co_changes": { "type": "boolean", "default": true, "description": "Include co-changed files with coupling scores" },
"include_churn": { "type": "boolean", "default": true, "description": "Include lines added/deleted churn data" },
"top_k": { "type": "integer", "default": 10, "description": "Max co-change partners to return" }
},
"required": ["node", "agent_id"]
}
},
{
"name": "diverge",
"description": "Detect structural drift between a baseline and current graph state",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"baseline": { "type": "string", "description": "Baseline reference: ISO date, git ref, or last_session" },
"scope": { "type": "string", "description": "File path glob to limit scope" },
"include_coupling_changes": { "type": "boolean", "default": true, "description": "Include coupling matrix delta" },
"include_anomalies": { "type": "boolean", "default": true, "description": "Detect anomalies (test deficits, velocity spikes)" }
},
"required": ["agent_id", "baseline"]
}
},
{
"name": "trail_save",
"description": "Persist current investigation state — nodes visited, hypotheses, conclusions",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"label": { "type": "string", "description": "Human-readable label for this investigation" },
"hypotheses": { "type": "array", "items": { "type": "object" }, "default": [], "description": "Hypotheses formed during investigation" },
"conclusions": { "type": "array", "items": { "type": "object" }, "default": [], "description": "Conclusions reached" },
"open_questions": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Open questions remaining" },
"tags": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Tags for organization and search" },
"summary": { "type": "string", "description": "Optional summary (auto-generated if omitted)" },
"visited_nodes": { "type": "array", "items": { "type": "object" }, "default": [], "description": "Explicitly list visited nodes with annotations" },
"activation_boosts": { "type": "object", "default": {}, "description": "Map of node_external_id -> boost weight [0.0, 1.0]" }
},
"required": ["agent_id", "label"]
}
},
{
"name": "trail_resume",
"description": "Restore a saved investigation — re-inject activation boosts, detect staleness",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"trail_id": { "type": "string", "description": "Trail ID to resume" },
"force": { "type": "boolean", "default": false, "description": "Resume even if trail is stale (>50% missing nodes)" }
},
"required": ["agent_id", "trail_id"]
}
},
{
"name": "trail_merge",
"description": "Combine two or more investigation trails — discover cross-connections",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"trail_ids": { "type": "array", "items": { "type": "string" }, "description": "Two or more trail IDs to merge" },
"label": { "type": "string", "description": "Label for the merged trail (auto-generated if omitted)" }
},
"required": ["agent_id", "trail_ids"]
}
},
{
"name": "trail_list",
"description": "List saved investigation trails with optional filters",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"filter_agent_id": { "type": "string", "description": "Filter to a specific agent's trails" },
"filter_status": { "type": "string", "description": "Filter by status: active, saved, archived, stale, merged" },
"filter_tags": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Filter by tags (any match)" }
},
"required": ["agent_id"]
}
},
{
"name": "hypothesize",
"description": "Test a structural claim about the codebase — graph-based hypothesis testing",
"inputSchema": {
"type": "object",
"properties": {
"claim": { "type": "string", "description": "Natural language claim (e.g. 'chat_handler never validates session tokens')" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"max_hops": { "type": "integer", "default": 5, "description": "Max BFS hops for evidence search" },
"include_ghost_edges": { "type": "boolean", "default": true, "description": "Include ghost edges as weak evidence" },
"include_partial_flow": { "type": "boolean", "default": true, "description": "Include partial flow when full path not found" },
"path_budget": { "type": "integer", "default": 1000, "description": "Budget cap for all-paths enumeration" }
},
"required": ["claim", "agent_id"]
}
},
{
"name": "differential",
"description": "Focused structural diff between two graph snapshots",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"snapshot_a": { "type": "string", "description": "Path to snapshot A, or 'current'" },
"snapshot_b": { "type": "string", "description": "Path to snapshot B, or 'current'" },
"question": { "type": "string", "description": "Focus question to narrow the diff output" },
"focus_nodes": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Limit diff to neighborhood of specific nodes" }
},
"required": ["agent_id", "snapshot_a", "snapshot_b"]
}
},
{
"name": "trace",
"description": "Map runtime errors to structural root causes via stacktrace analysis",
"inputSchema": {
"type": "object",
"properties": {
"error_text": { "type": "string", "description": "Full error output (stacktrace + error message)" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"language": { "type": "string", "description": "Language hint: python, rust, typescript, javascript, go (auto-detected if omitted)" },
"window_hours": { "type": "number", "default": 24.0, "description": "Temporal window (hours) for co-change suspect scan" },
"top_k": { "type": "integer", "default": 10, "description": "Max suspects to return" }
},
"required": ["error_text", "agent_id"]
}
},
{
"name": "validate_plan",
"description": "Validate a modification plan against the code graph — detect gaps and risk",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"actions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"action_type": { "type": "string", "description": "modify, create, delete, rename, or test" },
"file_path": { "type": "string", "description": "Relative file path" },
"description": { "type": "string" },
"depends_on": { "type": "array", "items": { "type": "string" }, "default": [] }
},
"required": ["action_type", "file_path"]
},
"description": "Ordered list of planned actions"
},
"include_test_impact": { "type": "boolean", "default": true, "description": "Analyze test coverage for modified files" },
"include_risk_score": { "type": "boolean", "default": true, "description": "Compute composite risk score" }
},
"required": ["agent_id", "actions"]
}
},
{
"name": "federate",
"description": "Ingest multiple repos into a unified federated graph with cross-repo edge detection",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"repos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "Repository name (namespace prefix)" },
"path": { "type": "string", "description": "Absolute path to repository root" },
"adapter": { "type": "string", "default": "code", "description": "Ingest adapter override" }
},
"required": ["name", "path"]
},
"description": "List of repositories to federate"
},
"detect_cross_repo_edges": { "type": "boolean", "default": true, "description": "Auto-detect cross-repo edges" },
"incremental": { "type": "boolean", "default": false, "description": "Only re-ingest repos that changed" }
},
"required": ["agent_id", "repos"]
}
},
{
"name": "antibody_scan",
"description": "Scan code graph against stored bug antibodies (immune memory patterns). Returns matches where known bug patterns recur.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"scope": { "type": "string", "default": "all", "description": "\"all\" = entire graph, \"changed\" = nodes since last scan" },
"antibody_ids": { "type": "array", "items": { "type": "string" }, "description": "Optional: only scan specific antibodies" },
"max_matches": { "type": "integer", "default": 50, "description": "Maximum matches to return" },
"min_severity": { "type": "string", "default": "info", "description": "Minimum severity: info, warning, critical" },
"similarity_threshold": { "type": "number", "default": 0.7, "description": "Fuzzy match threshold for label matching (0.0-1.0)" },
"match_mode": { "type": "string", "default": "substring", "description": "Label match mode: exact, substring, regex" },
"max_matches_per_antibody": { "type": "integer", "default": 50, "description": "Maximum matches per individual antibody" }
},
"required": ["agent_id"]
}
},
{
"name": "antibody_list",
"description": "List all stored bug antibodies with metadata, match history, and specificity scores.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"include_disabled": { "type": "boolean", "default": false, "description": "Include disabled antibodies" }
},
"required": ["agent_id"]
}
},
{
"name": "antibody_create",
"description": "Create, disable, enable, or delete a bug antibody pattern.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"action": { "type": "string", "default": "create", "description": "Action: create, disable, enable, delete" },
"antibody_id": { "type": "string", "description": "Required for disable/enable/delete" },
"name": { "type": "string", "description": "Antibody name (for create)" },
"description": { "type": "string", "description": "What this pattern detects" },
"severity": { "type": "string", "default": "warning", "description": "info, warning, critical" },
"pattern": { "type": "object", "description": "Pattern definition with nodes/edges/negative_edges" }
},
"required": ["agent_id"]
}
},
{
"name": "flow_simulate",
"description": "Simulate concurrent execution flow. Detects race conditions via particle collision on shared mutable state without synchronization.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"entry_nodes": { "type": "array", "items": { "type": "string" }, "description": "Starting nodes. Auto-discovered if empty." },
"num_particles": { "type": "integer", "default": 2, "description": "Particles per entry point" },
"lock_patterns": { "type": "array", "items": { "type": "string" }, "description": "Regex patterns for lock/mutex detection" },
"read_only_patterns": { "type": "array", "items": { "type": "string" }, "description": "Regex patterns for read-only operations" },
"max_depth": { "type": "integer", "default": 15, "description": "Maximum BFS depth" },
"turbulence_threshold": { "type": "number", "default": 0.5, "description": "Minimum score to report" },
"include_paths": { "type": "boolean", "default": true, "description": "Include particle paths in output" },
"max_total_steps": { "type": "integer", "default": 50000, "description": "Global step budget across all particles" },
"scope_filter": { "type": "string", "description": "Substring filter to limit which nodes particles can enter" }
},
"required": ["agent_id"]
}
},
{
"name": "epidemic",
"description": "Predict bug propagation via SIR epidemiological model. Given known buggy modules, predicts which neighbors are most likely to harbor undiscovered bugs.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"infected_nodes": { "type": "array", "items": { "type": "string" }, "description": "Known buggy node IDs" },
"recovered_nodes": { "type": "array", "items": { "type": "string" }, "description": "Already-fixed node IDs" },
"infection_rate": { "type": "number", "description": "Uniform infection rate. If omitted, derived from edge weights." },
"recovery_rate": { "type": "number", "default": 0, "description": "SIR recovery rate" },
"iterations": { "type": "integer", "default": 50, "description": "Simulation iterations" },
"direction": { "type": "string", "default": "both", "description": "Propagation direction: forward, backward, both" },
"top_k": { "type": "integer", "default": 20, "description": "Max predictions to return" },
"auto_calibrate": { "type": "boolean", "default": true, "description": "Auto-adjust infection_rate based on graph density" },
"scope": { "type": "string", "default": "all", "description": "Filter predictions: files, functions, all" },
"min_probability": { "type": "number", "default": 0.001, "description": "Filter out predictions below this probability" }
},
"required": ["agent_id", "infected_nodes"]
}
},
{
"name": "tremor",
"description": "Detect code tremors: modules with accelerating change frequency (second derivative). Earthquake precursor analogy for imminent bugs.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"window": { "type": "string", "default": "30d", "description": "Time window: 7d, 30d, 90d, all" },
"threshold": { "type": "number", "default": 0.1, "description": "Minimum magnitude to report" },
"top_k": { "type": "integer", "default": 20, "description": "Max results" },
"node_filter": { "type": "string", "description": "Filter to nodes matching this prefix" },
"include_history": { "type": "boolean", "default": false, "description": "Include observation history" },
"min_observations": { "type": "integer", "default": 3, "description": "Minimum data points to compute tremor" },
"sensitivity": { "type": "number", "default": 1.0, "description": "Multiplier on acceleration threshold (higher = more sensitive)" }
},
"required": ["agent_id"]
}
},
{
"name": "trust",
"description": "Per-module trust scores from defect history. Actuarial risk assessment: more confirmed bugs = lower trust = higher risk.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"scope": { "type": "string", "default": "file", "description": "Node type scope: file, function, class, all" },
"min_history": { "type": "integer", "default": 1, "description": "Minimum learn events for inclusion" },
"top_k": { "type": "integer", "default": 20, "description": "Max results" },
"node_filter": { "type": "string", "description": "Filter to nodes matching this prefix" },
"sort_by": { "type": "string", "default": "trust_asc", "description": "Sort: trust_asc, trust_desc, defects_desc, recency" },
"decay_half_life_days": { "type": "number", "default": 30.0, "description": "How fast old defects lose weight (days)" },
"risk_cap": { "type": "number", "default": 3.0, "description": "Maximum risk multiplier" }
},
"required": ["agent_id"]
}
},
{
"name": "layers",
"description": "Auto-detect architectural layers from graph topology. Returns layer assignments plus dependency violations (edges going against expected flow).",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"scope": { "type": "string", "description": "File path prefix to limit scope" },
"max_layers": { "type": "integer", "default": 8, "description": "Maximum layers to detect" },
"include_violations": { "type": "boolean", "default": true, "description": "Include violation analysis" },
"min_nodes_per_layer": { "type": "integer", "default": 2, "description": "Minimum nodes for a layer to be reported" },
"node_types": { "type": "array", "items": { "type": "string" }, "description": "Filter by node types" },
"naming_strategy": { "type": "string", "default": "auto", "description": "Layer naming: auto, path_prefix, pagerank" },
"exclude_tests": { "type": "boolean", "default": false, "description": "Exclude test files from layer detection" },
"violation_limit": { "type": "integer", "default": 100, "description": "Maximum violations to return" }
},
"required": ["agent_id"]
}
},
{
"name": "layer_inspect",
"description": "Inspect a specific architectural layer: nodes, connections, violations, and health metrics.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"level": { "type": "integer", "description": "Layer level to inspect" },
"scope": { "type": "string", "description": "File path prefix to limit scope" },
"include_edges": { "type": "boolean", "default": true, "description": "Include inter-layer edges" },
"top_k": { "type": "integer", "default": 50, "description": "Max nodes to return per layer" }
},
"required": ["agent_id", "level"]
}
},
{
"name": "heuristics_surface",
"description": "Return an explicit explainability surface for a code target, showing why heuristics ranked it as risky or important.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"node_id": { "type": "string", "description": "Graph node ID to inspect" },
"file_path": { "type": "string", "description": "Absolute or workspace-relative path to inspect" }
},
"required": ["agent_id"]
}
},
{
"name": "surgical_context",
"description": "Return full context for surgical LLM editing: file contents, symbols, and graph neighbourhood (callers, callees, tests). Use before m1nd.apply.",
"inputSchema": {
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Absolute or workspace-relative path to the file being edited" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"symbol": { "type": "string", "description": "Optional: narrow context to a specific symbol (function/struct/class name)" },
"radius": { "type": "integer", "default": 1, "description": "BFS radius for graph neighbourhood (1 or 2)" },
"include_tests": { "type": "boolean", "default": true, "description": "Include test files in the neighbourhood" }
},
"required": ["file_path", "agent_id"]
}
},
{
"name": "apply",
"description": "Write LLM-edited code back to a file and trigger incremental re-ingest so the graph stays coherent. Always call m1nd.surgical_context first.",
"inputSchema": {
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Absolute or workspace-relative path of the file to overwrite" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"new_content": { "type": "string", "description": "New file contents (full replacement, UTF-8)" },
"description": { "type": "string", "description": "Human-readable description of the edit" },
"reingest": { "type": "boolean", "default": true, "description": "Re-ingest the file after writing (recommended)" }
},
"required": ["file_path", "agent_id", "new_content"]
}
},
{
"name": "view",
"description": "Fast file reader with line numbers. Replaces View/cat/head/tail. No graph traversal — just reads the file. Auto-ingests if not in graph. Use for quick file inspection before surgical_context.",
"inputSchema": {
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Absolute or workspace-relative path to the file" },
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"offset": { "type": "integer", "default": 0, "description": "Start line (0-based)" },
"limit": { "type": "integer", "description": "Max lines to return (default: all)" },
"auto_ingest": { "type": "boolean", "default": true, "description": "Auto-ingest file into graph if not present" }
},
"required": ["file_path", "agent_id"]
}
},
{
"name": "surgical_context_v2",
"description": "Get full surgical context for a file PLUS source code of connected files (callers, callees, tests). Returns a complete workspace snapshot in one call. Superset of m1nd.surgical_context.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"file_path": { "type": "string", "description": "Absolute or workspace-relative path to the primary file" },
"symbol": { "type": "string", "description": "Optional: narrow context to a specific symbol (function/struct/class name)" },
"include_tests": { "type": "boolean", "default": true, "description": "Include test files in the neighbourhood" },
"radius": { "type": "integer", "default": 1, "description": "BFS radius for graph neighbourhood (1 or 2)" },
"max_connected_files": { "type": "integer", "default": 5, "description": "Maximum number of connected files to include source for" },
"max_lines_per_file": { "type": "integer", "default": 60, "description": "Maximum lines per connected file (primary file is unbounded)" }
},
"required": ["agent_id", "file_path"]
}
},
{
"name": "apply_batch",
"description": "Atomically write multiple files and trigger a single bulk re-ingest. Use after m1nd.surgical_context_v2 when editing a file and its callers/tests together. All-or-nothing by default.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"edits": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Absolute or workspace-relative path of the file to write" },
"new_content": { "type": "string", "description": "New file contents (full replacement, UTF-8)" },
"description": { "type": "string", "description": "Optional human-readable label for this edit" }
},
"required": ["file_path", "new_content"]
},
"description": "List of file edits to apply"
},
"atomic": { "type": "boolean", "default": true, "description": "All-or-nothing: if any file fails, none are written" },
"reingest": { "type": "boolean", "default": true, "description": "Re-ingest all modified files after writing" }
},
"required": ["agent_id", "edits"]
}
},
{
"name": "edit_preview",
"description": "Build an in-memory preview of a single-file full-replacement edit. Returns a preview handle, source snapshot, diff, and validation report. Does not touch disk.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"file_path": { "type": "string", "description": "Absolute or workspace-relative path of the file to preview" },
"new_content": { "type": "string", "description": "Candidate file contents (full replacement, UTF-8)" },
"description": { "type": "string", "description": "Optional human-readable description of the preview" }
},
"required": ["agent_id", "file_path", "new_content"]
}
},
{
"name": "edit_commit",
"description": "Commit a previously created edit_preview handle after re-checking source freshness. Persists atomically through the existing apply path.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"preview_id": { "type": "string", "description": "Preview handle returned by edit_preview" },
"confirm": { "type": "boolean", "default": false, "description": "Must be true to confirm the commit. Safety guard against accidental writes." },
"reingest": { "type": "boolean", "default": true, "description": "Re-ingest the modified file after commit" }
},
"required": ["agent_id", "preview_id", "confirm"]
}
},
{
"name": "search",
"description": "Unified code search: literal, regex (with multiline), or semantic. Searches graph node labels AND file contents on disk. Supports invert (grep -v), count-only (grep -c), multiline regex (rg -U), and filename pattern filtering (grep --include). v0.5.0: regex mode now searches file contents (not just node IDs).",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"query": { "type": "string", "description": "Search query string" },
"mode": {
"type": "string",
"enum": ["literal", "regex", "semantic"],
"default": "literal",
"description": "Search mode: literal (substring), regex (pattern), semantic (graph-aware)"
},
"scope": { "type": "string", "description": "File path prefix filter" },
"top_k": { "type": "integer", "default": 50, "description": "Max results (1-500)" },
"context_lines": { "type": "integer", "default": 2, "description": "Lines of context before/after match (0-10)" },
"case_sensitive": { "type": "boolean", "default": false, "description": "Case-sensitive matching" },
"invert": { "type": "boolean", "default": false, "description": "Return lines that DON'T match (grep -v)" },
"count_only": { "type": "boolean", "default": false, "description": "Return just the count, no results (grep -c)" },
"multiline": { "type": "boolean", "default": false, "description": "Enable multiline regex: dot matches newline (rg -U). Only for regex mode." },
"auto_ingest": { "type": "boolean", "default": false, "description": "Auto-ingest exactly one resolved scope path outside current ingest roots before searching; ambiguous scopes return an error that lists candidate paths in detail" },
"filename_pattern": { "type": "string", "description": "Glob pattern to filter filenames (e.g. '*.rs', 'test_*.py')" }
},
"required": ["agent_id", "query"]
}
},
{
"name": "glob",
"description": "Graph-aware file glob: find files in the ingested graph by glob pattern. Zero I/O — pure graph query. Replaces find/glob for indexed codebases.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"pattern": { "type": "string", "description": "Glob pattern (e.g. '**/*.rs', 'src/**/mod.rs', '*.toml')" },
"scope": { "type": "string", "description": "Root directory prefix to narrow scope" },
"top_k": { "type": "integer", "default": 200, "description": "Max results (1-10000)" },
"sort": {
"type": "string",
"enum": ["path", "activation"],
"default": "path",
"description": "Sort order: path (alphabetical) or activation (most connected first)"
}
},
"required": ["agent_id", "pattern"]
}
},
{
"name": "help",
"description": "Get help text for m1nd tools. Returns overview or detailed help for a specific tool with visual identity.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"tool_name": { "type": "string", "description": "Specific tool name for detailed help (omit for overview)" }
},
"required": ["agent_id"]
}
},
{
"name": "report",
"description": "Session intelligence report: queries, bugs, graph evolution, and estimated savings.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" }
},
"required": ["agent_id"]
}
},
{
"name": "panoramic",
"description": "Panoramic graph health overview: per-module risk scores combining blast radius, centrality, and churn signals.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"scope": { "type": "string", "description": "File path prefix filter" },
"top_n": { "type": "integer", "default": 50, "description": "Max modules to return (1-1000)" }
},
"required": ["agent_id"]
}
},
{
"name": "savings",
"description": "Estimated token and cost savings from using m1nd. Shows current session and global totals with Gaia counter.",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" }
},
"required": ["agent_id"]
}
},
{
"name": "persist",
"description": "Persist/load graph and plasticity state; supports binary snapshots",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"action": { "type": "string", "enum": ["save", "load", "checkpoint", "status"], "description": "Action to perform" },
"format": { "type": "string", "enum": ["json", "bin"], "default": "json", "description": "Snapshot format" },
"path": { "type": "string", "description": "Override snapshot path (optional)" }
},
"required": ["agent_id", "action"]
}
},
{
"name": "boot_memory",
"description": "Persist a small canonical boot/state memory on disk and keep it hot in runtime cache",
"inputSchema": {
"type": "object",
"properties": {
"agent_id": { "type": "string", "description": "Calling agent identifier" },
"action": { "type": "string", "enum": ["set", "get", "list", "delete", "status"], "description": "Action to perform" },
"key": { "type": "string", "description": "Canonical boot memory key" },
"value": { "description": "JSON value to persist for the boot memory entry" },
"tags": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Optional tags for organization" },
"source_refs": { "type": "array", "items": { "type": "string" }, "default": [], "description": "Optional source references backing this boot memory" }
},
"required": ["agent_id", "action"]
}
}
]
})
}
pub fn dispatch_tool(
state: &mut SessionState,
tool_name: &str,
params: &serde_json::Value,
) -> M1ndResult<serde_json::Value> {
let normalized = normalize_dispatch_tool_name(tool_name);
let start = std::time::Instant::now();
let agent_id = params
.get("agent_id")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let query_preview = params
.get("query")
.and_then(|v| v.as_str())
.unwrap_or_else(|| {
params
.get("claim")
.and_then(|v| v.as_str())
.unwrap_or_else(|| params.get("node_id").and_then(|v| v.as_str()).unwrap_or(""))
})
.to_string();
let result = match normalized.as_str() {
name if name.starts_with("perspective_") => dispatch_perspective_tool(state, name, params),
name if name.starts_with("lock_") => dispatch_lock_tool(state, name, params),
_ => dispatch_core_tool(state, &normalized, params),
};
if let Ok(ref value) = result {
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
let result_count = value
.get("results")
.and_then(|v| v.as_array())
.map_or(0, |a| a.len());
if !matches!(
normalized.as_str(),
"health" | "help" | "savings" | "report"
) {
state.savings_tracker.record(&normalized, result_count);
state.global_savings.total_queries += 1;
}
state.log_query(
&normalized,
&agent_id,
elapsed_ms,
result_count,
&query_preview,
);
}
result
}
fn dispatch_core_tool(
state: &mut SessionState,
tool_name: &str,
params: &serde_json::Value,
) -> M1ndResult<serde_json::Value> {
match tool_name {
"activate" => {
let input: ActivateInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = tools::handle_activate(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"impact" => {
let input: ImpactInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = tools::handle_impact(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"missing" => {
let input: MissingInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
tools::handle_missing(state, input)
}
"why" => {
let input: WhyInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
tools::handle_why(state, input)
}
"warmup" => {
let input: WarmupInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
tools::handle_warmup(state, input)
}
"counterfactual" => {
let input: CounterfactualInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
tools::handle_counterfactual(state, input)
}
"predict" => {
let input: PredictInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
tools::handle_predict(state, input)
}
"fingerprint" => {
let input: FingerprintInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
tools::handle_fingerprint(state, input)
}
"drift" => {
let input: DriftInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
tools::handle_drift(state, input)
}
"learn" => {
let input: LearnInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
tools::handle_learn(state, input)
}
"ingest" => {
let input: IngestInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
tools::handle_ingest(state, input)
}
"resonate" => {
let input: ResonateInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
tools::handle_resonate(state, input)
}
"health" => {
let input: HealthInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = tools::handle_health(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"seek" => {
let input: layers::SeekInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_seek(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"scan" => {
let input: layers::ScanInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_scan(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"timeline" => {
let input: layers::TimelineInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_timeline(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"diverge" => {
let input: layers::DivergeInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_diverge(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"trail_save" => {
let input: layers::TrailSaveInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_trail_save(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"trail_resume" => {
let input: layers::TrailResumeInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_trail_resume(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"trail_merge" => {
let input: layers::TrailMergeInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_trail_merge(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"trail_list" => {
let input: layers::TrailListInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_trail_list(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"hypothesize" => {
let input: layers::HypothesizeInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_hypothesize(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"differential" => {
let input: layers::DifferentialInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_differential(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"trace" => {
let input: layers::TraceInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "trace".into(),
detail: e.to_string(),
})?;
let output = layer_handlers::handle_trace(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"validate_plan" => {
let input: layers::ValidatePlanInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_validate_plan(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"federate" => {
let input: layers::FederateInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = layer_handlers::handle_federate(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"antibody_scan" => {
let input: layers::AntibodyScanInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
layer_handlers::handle_antibody_scan(state, input)
}
"antibody_list" => {
let input: layers::AntibodyListInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
layer_handlers::handle_antibody_list(state, input)
}
"antibody_create" => {
let input: layers::AntibodyCreateInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
layer_handlers::handle_antibody_create(state, input)
}
"flow_simulate" => {
let input: layers::FlowSimulateInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
layer_handlers::handle_flow_simulate(state, input)
}
"epidemic" => {
let input: layers::EpidemicInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
layer_handlers::handle_epidemic(state, input)
}
"tremor" => {
let input: layers::TremorInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
layer_handlers::handle_tremor(state, input)
}
"trust" => {
let input: layers::TrustInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
layer_handlers::handle_trust(state, input)
}
"heuristics_surface" => {
let input: surgical::HeuristicsSurfaceInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = surgical_handlers::handle_heuristics_surface(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"layers" => {
let input: layers::LayersInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
layer_handlers::handle_layers(state, input)
}
"layer_inspect" => {
let input: layers::LayerInspectInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
layer_handlers::handle_layer_inspect(state, input)
}
"search" => {
let input: layers::SearchInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = search_handlers::handle_search(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"glob" => {
let input: layers::GlobInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = search_handlers::handle_glob(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"help" => {
let input: layers::HelpInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = search_handlers::handle_help(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"report" => {
let input: layers::ReportInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = report_handlers::handle_report(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"panoramic" => {
let input: layers::PanoramicInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = report_handlers::handle_panoramic(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"savings" => {
let input: layers::SavingsInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
let output = report_handlers::handle_savings(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"surgical_context" => {
let input: crate::protocol::surgical::SurgicalContextInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "surgical_context".into(),
detail: e.to_string(),
})?;
let output = surgical_handlers::handle_surgical_context(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"apply" => {
let input: crate::protocol::surgical::ApplyInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "apply".into(),
detail: e.to_string(),
})?;
let output = surgical_handlers::handle_apply(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"surgical_context_v2" => {
let input: crate::protocol::surgical::SurgicalContextV2Input =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "surgical_context_v2".into(),
detail: e.to_string(),
})?;
let output = surgical_handlers::handle_surgical_context_v2(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"apply_batch" => {
let input: crate::protocol::surgical::ApplyBatchInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "apply_batch".into(),
detail: e.to_string(),
})?;
let output = surgical_handlers::handle_apply_batch(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"edit_preview" => {
let input: crate::protocol::surgical::EditPreviewInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "edit_preview".into(),
detail: e.to_string(),
})?;
let output = surgical_handlers::handle_edit_preview(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"edit_commit" => {
let input: crate::protocol::surgical::EditCommitInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "edit_commit".into(),
detail: e.to_string(),
})?;
let output = surgical_handlers::handle_edit_commit(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"view" => {
let input: crate::protocol::surgical::ViewInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "view".into(),
detail: e.to_string(),
})?;
let output = surgical_handlers::handle_view(state, input)?;
serde_json::to_value(output).map_err(M1ndError::Serde)
}
"persist" => {
let input: crate::persist_handlers::PersistInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
crate::persist_handlers::handle_persist(state, input)
}
"boot_memory" => {
let input: crate::boot_memory_handlers::BootMemoryInput =
serde_json::from_value(params.clone()).map_err(M1ndError::Serde)?;
crate::boot_memory_handlers::handle_boot_memory(state, input)
}
_ => Err(M1ndError::UnknownTool {
name: tool_name.to_string(),
}),
}
}
fn dispatch_perspective_tool(
state: &mut SessionState,
tool_name: &str,
params: &serde_json::Value,
) -> M1ndResult<serde_json::Value> {
use crate::perspective_handlers;
use crate::protocol::perspective::*;
match tool_name {
"perspective_start" => {
let input: PerspectiveStartInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_start".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_start(state, input)
}
"perspective_routes" => {
let input: PerspectiveRoutesInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_routes".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_routes(state, input)
}
"perspective_inspect" => {
let input: PerspectiveInspectInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_inspect".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_inspect(state, input)
}
"perspective_peek" => {
let input: PerspectivePeekInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_peek".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_peek(state, input)
}
"perspective_follow" => {
let input: PerspectiveFollowInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_follow".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_follow(state, input)
}
"perspective_suggest" => {
let input: PerspectiveSuggestInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_suggest".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_suggest(state, input)
}
"perspective_affinity" => {
let input: PerspectiveAffinityInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_affinity".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_affinity(state, input)
}
"perspective_branch" => {
let input: PerspectiveBranchInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_branch".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_branch(state, input)
}
"perspective_back" => {
let input: PerspectiveBackInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_back".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_back(state, input)
}
"perspective_compare" => {
let input: PerspectiveCompareInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_compare".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_compare(state, input)
}
"perspective_list" => {
let input: PerspectiveListInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_list".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_list(state, input)
}
"perspective_close" => {
let input: PerspectiveCloseInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "perspective_close".into(),
detail: e.to_string(),
})?;
perspective_handlers::handle_perspective_close(state, input)
}
_ => Err(M1ndError::UnknownTool {
name: tool_name.to_string(),
}),
}
}
fn dispatch_lock_tool(
state: &mut SessionState,
tool_name: &str,
params: &serde_json::Value,
) -> M1ndResult<serde_json::Value> {
use crate::lock_handlers;
use crate::protocol::lock::*;
match tool_name {
"lock_create" => {
let input: LockCreateInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "lock_create".into(),
detail: e.to_string(),
})?;
lock_handlers::handle_lock_create(state, input)
}
"lock_watch" => {
let input: LockWatchInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "lock_watch".into(),
detail: e.to_string(),
})?;
lock_handlers::handle_lock_watch(state, input)
}
"lock_diff" => {
let input: LockDiffInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "lock_diff".into(),
detail: e.to_string(),
})?;
lock_handlers::handle_lock_diff(state, input)
}
"lock_rebase" => {
let input: LockRebaseInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "lock_rebase".into(),
detail: e.to_string(),
})?;
lock_handlers::handle_lock_rebase(state, input)
}
"lock_release" => {
let input: LockReleaseInput =
serde_json::from_value(params.clone()).map_err(|e| M1ndError::InvalidParams {
tool: "lock_release".into(),
detail: e.to_string(),
})?;
lock_handlers::handle_lock_release(state, input)
}
_ => Err(M1ndError::UnknownTool {
name: tool_name.to_string(),
}),
}
}
impl McpServer {
pub fn new(config: McpConfig) -> M1ndResult<Self> {
let domain_config = match config.domain.as_deref() {
Some("music") => DomainConfig::music(),
Some("memory") => DomainConfig::memory(),
Some("generic") => DomainConfig::generic(),
Some("code") | None => DomainConfig::code(),
Some(other) => {
eprintln!("[m1nd] Unknown domain '{}', falling back to 'code'", other);
DomainConfig::code()
}
};
eprintln!("[m1nd] Domain: {}", domain_config.name);
let (mut graph, graph_loaded) = if config.graph_source.exists() {
match m1nd_core::snapshot::load_graph(&config.graph_source) {
Ok(g) => {
eprintln!(
"[m1nd] Loaded graph snapshot: {} nodes, {} edges",
g.num_nodes(),
g.num_edges(),
);
(g, true)
}
Err(e) => {
eprintln!(
"[m1nd] Failed to load graph snapshot ({}), starting fresh",
e,
);
(m1nd_core::graph::Graph::new(), false)
}
}
} else {
eprintln!("[m1nd] No graph snapshot found, starting fresh");
(m1nd_core::graph::Graph::new(), false)
};
if graph_loaded && !graph.finalized && graph.num_nodes() > 0 {
if let Err(e) = graph.finalize() {
eprintln!(
"[m1nd] Failed to finalize loaded graph ({}), starting fresh",
e,
);
graph = m1nd_core::graph::Graph::new();
}
}
let mut state = SessionState::initialize(graph, &config, domain_config)?;
if graph_loaded && config.plasticity_state.exists() {
match m1nd_core::snapshot::load_plasticity_state(&config.plasticity_state) {
Ok(states) => {
let mut g = state.graph.write();
match state.plasticity.import_state(&mut g, &states) {
Ok(_) => {
eprintln!(
"[m1nd] Loaded plasticity state: {} synaptic records",
states.len(),
);
}
Err(e) => {
eprintln!(
"[m1nd] Failed to import plasticity state ({}), continuing without it",
e,
);
}
}
}
Err(e) => {
eprintln!(
"[m1nd] Failed to load plasticity state ({}), continuing without it",
e,
);
}
}
}
Ok(Self { config, state })
}
pub fn into_session_state(self) -> SessionState {
self.state
}
pub fn start(&mut self) -> M1ndResult<()> {
eprintln!(
"[m1nd-mcp] Server ready. {} nodes, {} edges",
self.state.graph.read().num_nodes(),
self.state.graph.read().num_edges(),
);
Ok(())
}
pub fn serve(&mut self) -> M1ndResult<()> {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
let mut reader = stdin.lock();
let mut writer = stdout.lock();
loop {
let (payload, transport_mode) = match read_request_payload(&mut reader) {
Ok(Some(value)) => value,
Ok(None) => break,
Err(_) => break,
};
let trimmed = payload.trim();
if trimmed.is_empty() {
continue;
}
if let Ok(raw) = serde_json::from_str::<serde_json::Value>(trimmed) {
if raw.get("id").is_none() {
continue;
}
}
let request: JsonRpcRequest = match serde_json::from_str(trimmed) {
Ok(r) => r,
Err(e) => {
let payload = parse_error_payload(&e.to_string());
let err_resp = JsonRpcResponse {
jsonrpc: "2.0".into(),
id: serde_json::Value::Null,
result: None,
error: Some(JsonRpcError {
code: -32700,
message: payload["message"]
.as_str()
.unwrap_or("Invalid JSON-RPC request.")
.to_string(),
data: Some(payload),
}),
};
let _ = write_response(&mut writer, &err_resp, transport_mode);
continue;
}
};
let response = self.dispatch(&request);
let resp = match response {
Ok(r) => r,
Err(e) => {
let payload = tool_error_payload(&e);
JsonRpcResponse {
jsonrpc: "2.0".into(),
id: request.id.clone(),
result: None,
error: Some(JsonRpcError {
code: -32603,
message: payload["message"]
.as_str()
.unwrap_or("Tool call failed.")
.to_string(),
data: Some(payload),
}),
}
}
};
if write_response(&mut writer, &resp, transport_mode).is_err() {
break; }
}
Ok(())
}
pub fn shutdown(&mut self) -> M1ndResult<()> {
eprintln!("[m1nd-mcp] Shutting down...");
let _ = self.state.persist();
eprintln!("[m1nd-mcp] State persisted. Goodbye.");
Ok(())
}
fn dispatch(&mut self, request: &JsonRpcRequest) -> M1ndResult<JsonRpcResponse> {
let method = request.method.as_str();
match method {
"initialize" => Ok(JsonRpcResponse {
jsonrpc: "2.0".into(),
id: request.id.clone(),
result: Some(serde_json::json!({
"protocolVersion": "2024-11-05",
"serverInfo": {
"name": "m1nd-mcp",
"version": env!("CARGO_PKG_VERSION"),
},
"capabilities": {
"tools": {},
},
"instructions": M1ND_INSTRUCTIONS,
})),
error: None,
}),
"notifications/initialized" => {
Ok(JsonRpcResponse {
jsonrpc: "2.0".into(),
id: request.id.clone(),
result: Some(serde_json::Value::Null),
error: None,
})
}
"tools/list" => Ok(JsonRpcResponse {
jsonrpc: "2.0".into(),
id: request.id.clone(),
result: Some(tool_schemas()),
error: None,
}),
"tools/call" => {
let tool_name = request
.params
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("");
let arguments = request
.params
.get("arguments")
.cloned()
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
if let Some(agent_id) = arguments.get("agent_id").and_then(|v| v.as_str()) {
self.state.track_agent(agent_id);
}
match self.dispatch_tool_call(tool_name, &arguments) {
Ok(result) => Ok(JsonRpcResponse {
jsonrpc: "2.0".into(),
id: request.id.clone(),
result: Some(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&result).unwrap_or_default(),
}]
})),
error: None,
}),
Err(e) => Ok(JsonRpcResponse {
jsonrpc: "2.0".into(),
id: request.id.clone(),
result: Some(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&tool_error_payload(&e))
.unwrap_or_else(|_| format!("Error: {}", e)),
}],
"isError": true
})),
error: None,
}),
}
}
_ => {
Ok(JsonRpcResponse {
jsonrpc: "2.0".into(),
id: request.id.clone(),
result: None,
error: Some(JsonRpcError {
code: -32601,
message: format!("Method not found: {}", method),
data: Some(method_not_found_payload(method)),
}),
})
}
}
}
fn dispatch_tool_call(
&mut self,
tool_name: &str,
params: &serde_json::Value,
) -> M1ndResult<serde_json::Value> {
dispatch_tool(&mut self.state, tool_name, params)
}
}
#[cfg(test)]
mod tests {
use super::{
method_not_found_payload, normalize_dispatch_tool_name, parse_error_payload,
tool_error_payload,
};
use m1nd_core::error::M1ndError;
#[test]
fn normalizes_m1nd_aliases_to_canonical_tool_names() {
let cases = [
("activate", "activate"),
("m1nd.ingest", "ingest"),
("m1nd_ingest", "ingest"),
("m1nd.perspective.start", "perspective_start"),
("m1nd_perspective.start", "perspective_start"),
("m1nd.status", "health"),
("status", "health"),
("m1nd.health", "health"),
];
for (input, expected) in cases {
assert_eq!(normalize_dispatch_tool_name(input), expected);
}
}
#[test]
fn preserves_existing_canonical_and_nested_tool_names() {
let cases = [
("health", "health"),
("perspective_follow", "perspective_follow"),
("trail_save", "trail_save"),
("surgical_context_v2", "surgical_context_v2"),
("m1nd.surgical.context.v2", "surgical_context_v2"),
];
for (input, expected) in cases {
assert_eq!(normalize_dispatch_tool_name(input), expected);
}
}
#[test]
fn unknown_tool_payload_teaches_canonical_retry() {
let payload = tool_error_payload(&M1ndError::UnknownTool {
name: "m1nd.foo".into(),
});
assert_eq!(payload["error_type"], "unknown_tool");
assert!(payload["hint"]
.as_str()
.expect("hint")
.contains("canonical MCP tool names"));
assert_eq!(payload["example"]["method"], "tools/list");
}
#[test]
fn invalid_params_payload_includes_required_fields_and_example() {
let payload = tool_error_payload(&M1ndError::InvalidParams {
tool: "trace".into(),
detail: "missing field `error_text`".into(),
});
assert_eq!(payload["error_type"], "invalid_params");
assert_eq!(payload["tool"], "trace");
let required_fields = payload["required_fields"]
.as_array()
.expect("required fields array");
assert!(required_fields.iter().any(|value| value == "agent_id"));
assert!(required_fields.iter().any(|value| value == "error_text"));
assert_eq!(payload["example"]["params"]["name"], "trace");
}
#[test]
fn stale_trail_resume_invalid_params_payload_teaches_force_recovery() {
let payload = tool_error_payload(&M1ndError::InvalidParams {
tool: "trail.resume".into(),
detail:
"Trail trail_123 is stale: 8 of 12 nodes missing (67%). Use force=true to resume."
.into(),
});
assert_eq!(payload["error_type"], "invalid_params");
assert_eq!(payload["tool"], "trail.resume");
assert!(payload["hint"]
.as_str()
.expect("hint")
.contains("force=true"));
assert!(payload["workflow_hint"]
.as_str()
.expect("workflow hint")
.contains("trail_resume(force=true)"));
assert_eq!(payload["example"]["params"]["name"], "trail_resume");
assert_eq!(payload["example"]["params"]["arguments"]["force"], true);
}
#[test]
fn empty_search_invalid_params_payload_reroutes_to_glob() {
let payload = tool_error_payload(&M1ndError::InvalidParams {
tool: "m1nd_search".into(),
detail: "query cannot be empty; add exact text/regex to search content; for file/path discovery use m1nd.glob instead; for natural-language intent use m1nd.seek".into(),
});
assert_eq!(payload["error_type"], "invalid_params");
assert_eq!(payload["tool"], "m1nd_search");
assert!(payload["hint"]
.as_str()
.expect("hint")
.contains("reroute to `glob`"));
assert!(payload["workflow_hint"]
.as_str()
.expect("workflow hint")
.contains("glob -> view"));
assert_eq!(payload["example"]["params"]["name"], "glob");
}
#[test]
fn ambiguous_search_scope_invalid_params_payload_teaches_exact_scope_retry() {
let payload = tool_error_payload(&M1ndError::InvalidParams {
tool: "m1nd_search".into(),
detail: "scope 'src/handlers' resolves to 2 candidate paths: [/repo/a/src/handlers, /repo/b/src/handlers]. Use a more specific or absolute scope, or disable auto_ingest if you only want to search current roots".into(),
});
assert_eq!(payload["error_type"], "invalid_params");
assert!(payload["hint"]
.as_str()
.expect("hint")
.contains("ambiguous across multiple candidate paths"));
assert!(payload["suggested_next_step"]
.as_str()
.expect("step")
.contains("explicit scope path"));
}
#[test]
fn invalid_glob_pattern_payload_teaches_search_reroute() {
let payload = tool_error_payload(&M1ndError::InvalidParams {
tool: "m1nd_glob".into(),
detail: "invalid glob pattern 'normalize_dispatch_tool_name(': dangling metacharacter; use shell-style globs like 'src/**/*.rs' or '*.md'; for content search use m1nd.search instead".into(),
});
assert_eq!(payload["error_type"], "invalid_params");
assert_eq!(payload["tool"], "m1nd_glob");
assert!(payload["hint"]
.as_str()
.expect("hint")
.contains("switch to `search`"));
assert!(payload["suggested_next_step"]
.as_str()
.expect("step")
.contains("literal mode"));
}
#[test]
fn edit_commit_confirm_gate_payload_teaches_same_preview_retry() {
let payload = tool_error_payload(&M1ndError::InvalidParams {
tool: "edit_commit".into(),
detail: "confirm must be true to commit".into(),
});
assert_eq!(payload["error_type"], "invalid_params");
assert_eq!(payload["tool"], "edit_commit");
assert!(payload["hint"]
.as_str()
.expect("hint")
.contains("same `preview_id`"));
assert!(payload["workflow_hint"]
.as_str()
.expect("workflow hint")
.contains("edit_commit(confirm=true)"));
assert_eq!(payload["example"]["params"]["name"], "edit_commit");
assert_eq!(payload["example"]["params"]["arguments"]["confirm"], true);
}
#[test]
fn edit_commit_expired_preview_payload_teaches_fresh_preview_recovery() {
let payload = tool_error_payload(&M1ndError::InvalidParams {
tool: "edit_commit".into(),
detail: "preview_id not found or expired (TTL=5min): preview_dev_001".into(),
});
assert_eq!(payload["error_type"], "invalid_params");
assert_eq!(payload["tool"], "edit_commit");
assert!(payload["hint"]
.as_str()
.expect("hint")
.contains("fresh preview"));
assert!(payload["suggested_next_step"]
.as_str()
.expect("step")
.contains("edit_preview"));
assert_eq!(payload["example"]["params"]["name"], "edit_preview");
}
#[test]
fn empty_graph_payload_points_to_ingest() {
let payload = tool_error_payload(&M1ndError::EmptyGraph);
assert_eq!(payload["error_type"], "empty_graph");
assert!(payload["hint"].as_str().expect("hint").contains("ingest"));
assert_eq!(payload["example"]["params"]["name"], "ingest");
}
#[test]
fn parse_error_payload_contains_jsonrpc_example() {
let payload = parse_error_payload("expected ident");
assert_eq!(payload["error_type"], "invalid_jsonrpc");
assert_eq!(payload["example"]["method"], "tools/call");
assert_eq!(payload["example"]["params"]["name"], "seek");
}
#[test]
fn method_not_found_payload_suggests_mcp_methods() {
let payload = method_not_found_payload("foo/bar");
assert_eq!(payload["error_type"], "method_not_found");
assert!(payload["hint"]
.as_str()
.expect("hint")
.contains("tools/list"));
}
}