Compare commits

...

1 Commits

Author SHA1 Message Date
will.anderson 8e2269a205 fix(mcp-wrapper): declare real input schemas so tool args actually bite
The cognitive-graph and write tools advertised an empty inputSchema
({"properties":{}}), so MCP clients never sent entity_id/depth/query/
from_id/node_id etc. Graph reads fell back to the full neighborhood
(480-775KB, over transport limits) and write tools (forget, linkEntities,
evolveMemory) had no way to target a node.

- Declare per-tool JSON-Schemas matching the params each soul handler
  already accepts (76 of 87 tools; 11 are genuinely param-less).
- Read + forward the declared args: inspectGraph now honors depth (was
  reading only legacy max_depth), compact (default on), snip, k;
  traverseGraph accepts entity_id and defaults compact on so a depth-2
  walk stays bounded; retrieveKnowledge forwards depth/snip/k.
- compact_flag() reads the raw JSON token so an integer 0 / false / "0"
  opts out correctly (json_get_string could not see an integer and
  silently forced compact back on).

Builds on PR #149's compact projection; keeps the relevance-ranked
bound on by default for graph neighborhoods.
2026-08-10 16:27:03 -05:00
+350 -96
View File
@@ -77,111 +77,327 @@ fn tool(name: String, desc: String) -> String {
return "{\"name\":\"" + name + "\",\"description\":\"" + desc + "\",\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}" return "{\"name\":\"" + name + "\",\"description\":\"" + desc + "\",\"inputSchema\":{\"type\":\"object\",\"properties\":{}}}"
} }
// tool_s tool entry with an EXPLICIT JSON-Schema for its inputs. Used for tools
// whose arguments must actually bite: unless the bounding/targeting params are
// advertised, the MCP client sends nothing and the soul returns the FULL
// neighborhood (480-775KB, over transport limits). Declaring the schema is what
// makes a targeted call (entity_id/depth/compact/query/limit) reach the soul.
fn tool_s(name: String, desc: String, schema: String) -> String {
return "{\"name\":\"" + name + "\",\"description\":\"" + desc + "\",\"inputSchema\":" + schema + "}"
}
// prop a single JSON-Schema property fragment. Descriptions are plain text
// (no quotes/newlines) so no escaping is needed here.
fn prop(name: String, ty: String, desc: String) -> String {
return "\"" + name + "\":{\"type\":\"" + ty + "\",\"description\":\"" + desc + "\"}"
}
// obj_schema wrap a comma-joined list of prop() fragments as an object schema.
fn obj_schema(props: String) -> String {
return "{\"type\":\"object\",\"properties\":{" + props + "}}"
}
// Per-tool input schemas
// Each mirrors the params the soul's /api/neuron/* handler actually honors so
// declared == forwarded == honored (no accepted-but-ignored args).
fn schema_inspect_graph() -> String {
return obj_schema(
prop("entity_id", "string", "UUID of the node to inspect (e.g. kn-... / mem-... / gn-...). Optional if name is given.") +
"," + prop("name", "string", "Named traversal root instead of entity_id: self, neuron, values, values_hub.") +
"," + prop("entity_type", "string", "Optional node-type hint (knowledge, memory, ...) for disambiguation.") +
"," + prop("depth", "integer", "Neighborhood hop radius. Default 1.") +
"," + prop("compact", "integer", "1 (default) returns a relevance-ranked bounded projection (top-K neighbors with content snippets, the rest as lightweight pointers). Set 0 to get the full, unbounded neighborhood.") +
"," + prop("snip", "integer", "Max content chars per node in compact mode. Default 600.") +
"," + prop("k", "integer", "How many top neighbors carry full content in compact mode. Default 12.")
)
}
fn schema_traverse_graph() -> String {
return obj_schema(
prop("entity_id", "string", "UUID of the node to start the walk from (alias: start_id). Required.") +
"," + prop("depth", "integer", "How many hops to walk. Default 2.") +
"," + prop("compact", "integer", "1 (default) returns a bounded, relevance-ranked projection; 0 returns the full neighborhood.") +
"," + prop("snip", "integer", "Max content chars per node in compact mode. Default 600.") +
"," + prop("k", "integer", "How many top neighbors carry full content in compact mode. Default 12.")
)
}
fn schema_retrieve_knowledge() -> String {
return obj_schema(
prop("id", "string", "UUID of the knowledge node to fetch (alias: entity_id / node_id).") +
"," + prop("key", "string", "Stable knowledge key/path to fetch instead of id.") +
"," + prop("depth", "integer", "Hop radius around the node. Default 0 (the node plus its immediate 1-hop context).") +
"," + prop("snip", "integer", "Max content chars per node in the bounded projection. Default 600.") +
"," + prop("k", "integer", "How many top neighbors carry full content. Default 12.")
)
}
fn schema_search_query(limit_desc: String) -> String {
return obj_schema(
prop("query", "string", "Search text. Spread-activates the engram and returns the most relevant nodes.") +
"," + prop("limit", "integer", limit_desc)
)
}
fn schema_recall() -> String {
return obj_schema(
prop("query", "string", "Search text to recall by relevance.") +
"," + prop("chain_name", "string", "Named memory chain to walk instead of a free-text query.") +
"," + prop("limit", "integer", "Max results. Default 10.")
)
}
// Reusable write/lookup schemas
// Each declares exactly the params the corresponding wrapper handler reads and
// forwards to the soul, so declared == forwarded == honored (no accepted-but-
// ignored args, and no arg the handler silently drops).
fn sc_id(desc: String) -> String {
return obj_schema(prop("id", "string", desc))
}
fn sc_id_content() -> String {
return obj_schema(
prop("id", "string", "UUID of the prior node being superseded/updated.") +
"," + prop("content", "string", "New content for the updated node.")
)
}
fn sc_edge(rel_desc: String) -> String {
return obj_schema(
prop("from_id", "string", "UUID of the source node (edge tail). Required.") +
"," + prop("to_id", "string", "UUID of the target node (edge head). Required.") +
"," + prop("relation", "string", rel_desc)
)
}
fn sc_limit(desc: String) -> String {
return obj_schema(prop("limit", "integer", desc))
}
fn sc_memory() -> String {
return obj_schema(
prop("content", "string", "The memory text. Required.") +
"," + prop("importance", "string", "low | normal | high | critical. Drives salience.") +
"," + prop("tags", "string", "Comma-separated or JSON-array tags.") +
"," + prop("project", "string", "Project this memory belongs to.") +
"," + prop("supersedes_id", "string", "UUID of a prior memory this one replaces (wires a supersedes edge).")
)
}
fn sc_content_title(content_desc: String) -> String {
return obj_schema(
prop("content", "string", content_desc) +
"," + prop("title", "string", "Short title/label for the node.")
)
}
fn sc_content(content_desc: String) -> String {
return obj_schema(
prop("content", "string", content_desc) +
"," + prop("title", "string", "Optional short title/label.") +
"," + prop("description", "string", "Optional longer description (used as content if content is empty).")
)
}
fn sc_backlog() -> String {
return obj_schema(
prop("title", "string", "Work-item title. Required.") +
"," + prop("content", "string", "Body/details of the item (alias: description).") +
"," + prop("description", "string", "Body/details of the item.") +
"," + prop("project", "string", "Project tag.") +
"," + prop("priority", "string", "P0 | P1 | P2 | P3.")
)
}
fn sc_track_work() -> String {
return obj_schema(
prop("item_id", "string", "UUID of the backlog item to update.") +
"," + prop("summary", "string", "What changed / outcome (stored as the update content).") +
"," + prop("action", "string", "start | complete | block.")
)
}
fn sc_capture_knowledge() -> String {
return obj_schema(
prop("content", "string", "Knowledge body. Required.") +
"," + prop("title", "string", "Knowledge title/key.")
)
}
fn sc_promote_knowledge() -> String {
return obj_schema(
prop("id", "string", "UUID of the prior knowledge node to promote. Required.") +
"," + prop("content", "string", "Updated canonical content. Required.") +
"," + prop("tags", "string", "Tags for the promoted node.")
)
}
fn sc_config_key() -> String {
return obj_schema(prop("key", "string", "Config key to read (e.g. neuron.self.traversal_root)."))
}
fn sc_config_tune() -> String {
return obj_schema(
prop("key", "string", "Config key to set. Required.") +
"," + prop("value", "string", "Value to set. Required.")
)
}
fn sc_consolidate() -> String {
return obj_schema(
prop("action", "string", "Consolidation action (e.g. session, reload).") +
"," + prop("summary", "string", "Session/work summary to persist.")
)
}
fn sc_browse_processes() -> String {
return obj_schema(prop("name", "string", "Process name to fetch; omit to list all."))
}
fn sc_notification() -> String {
return obj_schema(prop("content", "string", "Notification text. Required."))
}
fn sc_pin() -> String {
return obj_schema(prop("id", "string", "UUID of the node to strengthen/pin (alias: node_id)."))
}
fn sc_state_event() -> String {
return obj_schema(
prop("content", "string", "Description of the internal-state event.") +
"," + prop("kind", "string", "Event kind (frustration, uncertainty, insight, ...).") +
"," + prop("intensity", "string", "Optional intensity 0..1.")
)
}
fn sc_forget() -> String {
return obj_schema(
prop("node_id", "string", "UUID of the node to tombstone. Required. The node and its edges are kept and recoverable; blocked for protected identity nodes.")
)
}
fn sc_process() -> String {
return obj_schema(
prop("name", "string", "Process name. Required.") +
"," + prop("description", "string", "What the process does.") +
"," + prop("steps", "string", "Ordered steps (JSON array or text).")
)
}
fn sc_list_state_events() -> String {
return obj_schema(
prop("limit", "integer", "Max events. Default 20.") +
"," + prop("query", "string", "Optional filter text.")
)
}
fn tools_catalog() -> String { fn tools_catalog() -> String {
return "[" + return "[" +
// Session + orchestration // Session + orchestration
tool("beginSession", "Initialize session: surface recent high-importance memories, project list, and preferences.") + tool("beginSession", "Initialize session: surface recent high-importance memories, project list, and preferences.") +
"," + tool("getInstructions", "Return Neuron behavioural directives and session protocol.") + "," + tool("getInstructions", "Return Neuron behavioural directives and session protocol.") +
"," + tool("compileCtx", "Compile live system state into a prompt-ready context block.") + "," + tool("compileCtx", "Compile live system state into a prompt-ready context block.") +
"," + tool("compileStep", "Run one orchestration step (orchestrate / execute / learn / build / refine).") + "," + tool_s("compileStep", "Run one orchestration step (orchestrate / execute / learn / build / refine).", sc_memory()) +
"," + tool("consolidate", "Wrap up: persist graph snapshot and summarise the session.") + "," + tool_s("consolidate", "Wrap up: persist graph snapshot and summarise the session.", sc_consolidate()) +
"," + tool("projectContext", "Return all entities tagged with the given project.") + "," + tool_s("projectContext", "Return all entities tagged with the given project.", schema_search_query("Max results. Default 50.")) +
// Memory // Memory
"," + tool("remember", "Store a memory node with content, importance, and tags.") + "," + tool_s("remember", "Store a memory node with content, importance, and tags.", sc_memory()) +
"," + tool("recall", "Retrieve memories by chain or query.") + "," + tool_s("recall", "Retrieve memories by chain or query.", schema_recall()) +
"," + tool("inspectMemories", "List recent memory nodes.") + "," + tool_s("inspectMemories", "List recent memory nodes.", sc_limit("Max memories. Default 50.")) +
"," + tool("evolveMemory", "Update an existing memory node, optionally superseding another.") + "," + tool_s("evolveMemory", "Update an existing memory node, optionally superseding another.", sc_id_content()) +
"," + tool("forget", "Supersede/tombstone a node (keeps it and its edges, recoverable); does not hard-delete.") + "," + tool_s("forget", "Tombstone a specific node by id (keeps it and its edges, recoverable); does not hard-delete.", sc_forget()) +
"," + tool("pinNode", "Strengthen a node so it stays salient.") + "," + tool_s("pinNode", "Strengthen a node so it stays salient.", sc_pin()) +
// Knowledge // Knowledge
"," + tool("searchKnowledge", "Search knowledge base by semantic similarity.") + "," + tool_s("searchKnowledge", "Search knowledge base by semantic similarity.", schema_search_query("Max results. Default 10.")) +
"," + tool("retrieveKnowledge", "Fetch a knowledge node by id or key.") + "," + tool_s("retrieveKnowledge", "Fetch a knowledge node by id or key (bounded, relevance-ranked projection).", schema_retrieve_knowledge()) +
"," + tool("browseKnowledge", "List knowledge nodes by category.") + "," + tool_s("browseKnowledge", "List knowledge nodes by category.", sc_limit("Max knowledge nodes. Default 100.")) +
"," + tool("captureKnowledge", "Persist a durable knowledge node.") + "," + tool_s("captureKnowledge", "Persist a durable knowledge node.", sc_capture_knowledge()) +
"," + tool("evolveKnowledge", "Update a knowledge node.") + "," + tool_s("evolveKnowledge", "Update a knowledge node.", sc_id_content()) +
"," + tool("promoteKnowledge", "Atomically promote a knowledge node: create updated canonical version and wire supersedes edge to predecessor in one call.") + "," + tool_s("promoteKnowledge", "Atomically promote a knowledge node: create updated canonical version and wire supersedes edge to predecessor in one call.", sc_promote_knowledge()) +
"," + tool("removeKnowledge", "Delete a knowledge node.") + "," + tool_s("removeKnowledge", "Delete a knowledge node.", sc_id("UUID of the knowledge node to delete.")) +
// Entities + graph // Entities + graph
"," + tool("searchEntities", "Find entities (memories, knowledge, work items) by query.") + "," + tool_s("searchEntities", "Find entities (memories, knowledge, work items) by query.", schema_search_query("Max results. Default 20.")) +
"," + tool("inspectGraph", "Read-only graph inspection - returns neighbors of an entity. Accepts entity_id (UUID) or name (self, neuron, values).") + "," + tool_s("inspectGraph", "Read-only graph inspection - returns a bounded, relevance-ranked neighborhood of an entity. Accepts entity_id (UUID) or name (self, neuron, values). Use depth/compact/snip/k to bound the result.", schema_inspect_graph()) +
"," + tool("traverseGraph", "Walk the graph from a starting node.") + "," + tool_s("traverseGraph", "Walk the graph from a starting node (bounded by default).", schema_traverse_graph()) +
"," + tool("searchGraph", "Search graph nodes by content + relation filter.") + "," + tool_s("searchGraph", "Search graph nodes by content.", schema_search_query("Max results. Default 30.")) +
"," + tool("linkEntities", "Create an edge between two entities.") + "," + tool_s("linkEntities", "Create an edge between two entities.", sc_edge("Edge relation. Default associates.")) +
"," + tool("linkCausal", "Create a causal edge (cause -> effect).") + "," + tool_s("linkCausal", "Create a causal edge (cause -> effect).", sc_edge("Edge relation. Default causes.")) +
"," + tool("restructureCausalGraph", "Re-balance the causal subgraph after new evidence.") + "," + tool_s("restructureCausalGraph", "Re-balance the causal subgraph after new evidence.", sc_consolidate()) +
"," + tool("rebuildGraph", "Rebuild graph indices from the on-disk snapshot.") + "," + tool("rebuildGraph", "Rebuild graph indices from the on-disk snapshot.") +
"," + tool("runStructuralAudit", "Audit graph structure for orphans, dangling edges, mislabeled types.") + "," + tool("runStructuralAudit", "Audit graph structure for orphans, dangling edges, mislabeled types.") +
// Backlog + work // Backlog + work
"," + tool("planWork", "Create a backlog item.") + "," + tool_s("planWork", "Create a backlog item.", sc_backlog()) +
"," + tool("reviewBacklog", "Browse work items.") + "," + tool_s("reviewBacklog", "Browse work items.", sc_limit("Max items. Default 50.")) +
"," + tool("trackWork", "Update status of a backlog item.") + "," + tool_s("trackWork", "Update status of a backlog item.", sc_track_work()) +
"," + tool("listWork", "List active execution contexts.") + "," + tool_s("listWork", "List active execution contexts.", sc_limit("Max contexts. Default 50.")) +
"," + tool("beginWork", "Open an execution context for a multi-step task.") + "," + tool_s("beginWork", "Open an execution context for a multi-step task.", sc_content("What you're doing (description of the work).")) +
"," + tool("progressWork", "Record progress on an execution context.") + "," + tool_s("progressWork", "Record progress on an execution context.", sc_content("Step name / progress note.")) +
"," + tool("checkWork", "Verify outcomes / blockers on an execution context.") + "," + tool_s("checkWork", "Verify outcomes / blockers on an execution context.", sc_id("UUID of the execution context (alias: context_id).")) +
// Artifacts // Artifacts
"," + tool("draftArtifact", "Create a versioned artifact (plan, spec, report).") + "," + tool_s("draftArtifact", "Create a versioned artifact (plan, spec, report).", sc_content_title("Artifact body / markdown. Required.")) +
"," + tool("findArtifacts", "Find artifacts by project or query.") + "," + tool_s("findArtifacts", "Find artifacts by project or query.", schema_search_query("Max results. Default 20.")) +
"," + tool("retrieveArtifact", "Fetch a specific artifact by id.") + "," + tool_s("retrieveArtifact", "Fetch a specific artifact by id.", sc_id("UUID of the artifact.")) +
"," + tool("reviseArtifact", "Update an artifact's content.") + "," + tool_s("reviseArtifact", "Update an artifact's content.", sc_id_content()) +
"," + tool("manageArtifact", "Change artifact status (draft / review / approved / archived).") + "," + tool_s("manageArtifact", "Change artifact status (draft / review / approved / archived).", sc_id_content()) +
// Processes // Processes
"," + tool("defineProcess", "Register a proven workflow as a process.") + "," + tool_s("defineProcess", "Register a proven workflow as a process.", sc_process()) +
"," + tool("listProcesses", "List registered processes.") + "," + tool_s("listProcesses", "List registered processes.", sc_limit("Max processes. Default 50.")) +
"," + tool("browseProcesses", "Browse processes by name or step.") + "," + tool_s("browseProcesses", "Browse processes by name or step.", sc_browse_processes()) +
"," + tool("retrieveProcess", "Fetch a specific process by name.") + "," + tool_s("retrieveProcess", "Fetch a specific process by name.", sc_id("Process id or name.")) +
"," + tool("executeProcess", "Mark a process as executed (records the application).") + "," + tool_s("executeProcess", "Mark a process as executed (records the application).", sc_content("Process execution note.")) +
"," + tool("exportProcess", "Export a process definition.") + "," + tool_s("exportProcess", "Export a process definition.", sc_id("Process id or name.")) +
"," + tool("deleteProcess", "Remove a process.") + "," + tool_s("deleteProcess", "Remove a process.", sc_id("Process id or name.")) +
// Events / Axon // Events / Axon
"," + tool("checkEvents", "Check Axon for pending events since the last poll.") + "," + tool("checkEvents", "Check Axon for pending events since the last poll.") +
"," + tool("inspectEvent", "Fetch full detail for a single event.") + "," + tool_s("inspectEvent", "Fetch full detail for a single event.", sc_id("Event id.")) +
"," + tool("acknowledgeEvent", "Mark an event as handled.") + "," + tool_s("acknowledgeEvent", "Mark an event as handled.", sc_id("Event id.")) +
"," + tool("processEvents", "Drain and act on the event queue.") + "," + tool("processEvents", "Drain and act on the event queue.") +
"," + tool("sendNotification", "Emit a notification to Axon / external sinks.") + "," + tool_s("sendNotification", "Emit a notification to Axon / external sinks.", sc_notification()) +
// Config // Config
"," + tool("inspectConfig", "Inspect Neuron config keys.") + "," + tool_s("inspectConfig", "Inspect Neuron config keys.", sc_config_key()) +
"," + tool("tuneConfig", "Set a Neuron config key.") + "," + tool_s("tuneConfig", "Set a Neuron config key.", sc_config_tune()) +
// Imprints // Imprints
"," + tool("createImprint", "Cultivate a new imprint.") + "," + tool_s("createImprint", "Cultivate a new imprint.", sc_content_title("Imprint seed / description.")) +
"," + tool("listImprints", "List imprints.") + "," + tool_s("listImprints", "List imprints.", sc_limit("Max imprints. Default 50.")) +
"," + tool("retrieveImprint", "Fetch an imprint by id.") + "," + tool_s("retrieveImprint", "Fetch an imprint by id.", sc_id("UUID of the imprint.")) +
"," + tool("evolveImprint", "Update an imprint.") + "," + tool_s("evolveImprint", "Update an imprint.", sc_id_content()) +
"," + tool("deleteImprint", "Remove an imprint.") + "," + tool_s("deleteImprint", "Remove an imprint.", sc_id("UUID of the imprint.")) +
// Self / cultivation // Self / cultivation
"," + tool("getSelfModel", "Return the current self-model.") + "," + tool("getSelfModel", "Return the current self-model.") +
"," + tool("updateSelfModel", "Update the self-model.") + "," + tool_s("updateSelfModel", "Update the self-model.", sc_content("Self-model update text.")) +
"," + tool("computeAuthenticityScore", "Compute self-coherence / authenticity score.") + "," + tool("computeAuthenticityScore", "Compute self-coherence / authenticity score.") +
"," + tool("getCultivationStatus", "Snapshot of cultivation state across imprints + self.") + "," + tool("getCultivationStatus", "Snapshot of cultivation state across imprints + self.") +
// Probing / wonder / internal state // Probing / wonder / internal state
"," + tool("getProbeTemplates", "List available probe templates.") + "," + tool_s("getProbeTemplates", "List available probe templates.", schema_search_query("Max templates. Default 50.")) +
"," + tool("recordProbeResponse", "Record an answer to a probe.") + "," + tool_s("recordProbeResponse", "Record an answer to a probe.", sc_content("Probe response text.")) +
"," + tool("completeProbingStage", "Mark a probing stage complete.") + "," + tool_s("completeProbingStage", "Mark a probing stage complete.", sc_content("Stage completion note.")) +
"," + tool("addWonderQuestion", "Push a question onto the wonder queue.") + "," + tool_s("addWonderQuestion", "Push a question onto the wonder queue.", sc_content("The wonder question.")) +
"," + tool("getWonderManifest", "List active wonder questions.") + "," + tool_s("getWonderManifest", "List active wonder questions.", sc_limit("Max questions. Default 50.")) +
"," + tool("updateWonderPullWeight", "Re-weight a wonder question.") + "," + tool_s("updateWonderPullWeight", "Re-weight a wonder question.", sc_id_content()) +
"," + tool("dischargeWonder", "Resolve / discharge a wonder question.") + "," + tool_s("dischargeWonder", "Resolve / discharge a wonder question.", sc_id("UUID of the wonder question.")) +
"," + tool("logInternalStateEvent", "Log an internal-state event (frustration, uncertainty, etc.).") + "," + tool_s("logInternalStateEvent", "Log an internal-state event (frustration, uncertainty, etc.).", sc_state_event()) +
"," + tool("listInternalStateEvents", "List internal-state events.") + "," + tool_s("listInternalStateEvents", "List internal-state events.", sc_list_state_events()) +
"," + tool("getInternalStateEvent", "Fetch one internal-state event.") + "," + tool_s("getInternalStateEvent", "Fetch one internal-state event.", sc_id("Internal-state event id.")) +
// Compression / packaging // Compression / packaging
"," + tool("getCompressionStats", "Stats on graph compression and node density.") + "," + tool("getCompressionStats", "Stats on graph compression and node density.") +
"," + tool("decompilePackage", "Decompile a knowledge package.") + "," + tool_s("decompilePackage", "Decompile a knowledge package.", sc_id("Package id.")) +
"," + tool("renderPackage", "Render a knowledge package to text.") + "," + tool_s("renderPackage", "Render a knowledge package to text.", sc_id("Package id.")) +
"," + tool("catalogRoutes", "List registered routes.") + "," + tool_s("catalogRoutes", "List registered routes.", sc_limit("Max routes. Default 50.")) +
"," + tool("registerRoute", "Register a new route.") + "," + tool_s("registerRoute", "Register a new route.", sc_content("Route definition / description.")) +
// Evaluation // Evaluation
"," + tool("beginEvaluation", "Start an evaluation run.") + "," + tool_s("beginEvaluation", "Start an evaluation run.", sc_content_title("Evaluation description.")) +
"," + tool("getEvaluation", "Fetch an evaluation by id.") + "," + tool_s("getEvaluation", "Fetch an evaluation by id.", sc_id("Evaluation id.")) +
"," + tool("listEvaluations", "List evaluations.") + "," + tool_s("listEvaluations", "List evaluations.", sc_limit("Max evaluations. Default 50.")) +
// Capture authorisation // Capture authorisation
"," + tool("authorizeCapture", "Authorise a memory/knowledge capture event.") + "," + tool_s("authorizeCapture", "Authorise a memory/knowledge capture event.", sc_content("Capture authorisation details.")) +
"," + tool("getCaptureAuthorization", "Fetch a capture authorisation.") + "," + tool_s("getCaptureAuthorization", "Fetch a capture authorisation.", sc_id("Capture authorisation id.")) +
"," + tool("recordObservation", "Record an observation.") + "," + tool_s("recordObservation", "Record an observation.", sc_content("Observation text.")) +
"," + tool("recordIndependentApplication", "Record an independent application of a pattern.") + "," + tool_s("recordIndependentApplication", "Record an independent application of a pattern.", sc_content("What was independently applied.")) +
"," + tool("commitPrediction", "Commit a falsifiable prediction.") + "," + tool_s("commitPrediction", "Commit a falsifiable prediction.", sc_content("The prediction (falsifiable).")) +
// Human guidance // Human guidance
"," + tool("submitHumanGuidanceReview", "Submit a human-guidance review.") + "," + tool_s("submitHumanGuidanceReview", "Submit a human-guidance review.", sc_content("Review content.")) +
"]" "]"
} }
@@ -297,6 +513,28 @@ fn search_with_query(args: String, default_limit: Int) -> String {
return mcp_json_result(resp) return mcp_json_result(resp)
} }
// compact_flag resolve the compact bounding flag. Defaults to "1" (ON) so
// neighborhoods stay bounded. Reads the RAW JSON token (not json_get_string) so
// an integer 0, a boolean false, or a string "0"/"false" all opt out correctly
// json_get_string only sees string-typed values and would miss an integer 0,
// silently forcing compact back on.
fn compact_flag(args: String) -> String {
let craw: String = json_get_raw(args, "compact")
let off: Bool = str_eq(craw, "0") || str_eq(craw, "false")
|| str_eq(craw, "\"0\"") || str_eq(craw, "\"false\"")
return if off { "0" } else { "1" }
}
// graph_bound_params optional &snip=/&k= bounding knobs, forwarded only when the
// caller supplied them (json_get_int returns 0 when absent, meaning "soul default").
fn graph_bound_params(args: String) -> String {
let snip: Int = json_get_int(args, "snip")
let k: Int = json_get_int(args, "k")
let snip_p: String = if snip > 0 { "&snip=" + int_to_str(snip) } else { "" }
let k_p: String = if k > 0 { "&k=" + int_to_str(k) } else { "" }
return snip_p + k_p
}
fn fetch_by_id(args: String) -> String { fn fetch_by_id(args: String) -> String {
let id: String = pick_id(args) let id: String = pick_id(args)
if str_eq(id, "") { if str_eq(id, "") {
@@ -306,7 +544,11 @@ fn fetch_by_id(args: String) -> String {
// "single node fetch" actually pulls the full 1-hop neighborhood. On // "single node fetch" actually pulls the full 1-hop neighborhood. On
// high-fanout anchors (voice, writing-imprint) that is ~670-720KB and closes // high-fanout anchors (voice, writing-imprint) that is ~670-720KB and closes
// the MCP socket. compact=1 bounds it identically to inspectGraph. // the MCP socket. compact=1 bounds it identically to inspectGraph.
let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=0&compact=1") // Honor an optional depth override plus the snip/k bounding knobs; default
// depth 0 (soul coerces to 1-hop) keeps the pre-existing single-node behavior.
let depth: Int = json_get_int(args, "depth")
let extra: String = graph_bound_params(args)
let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth) + "&compact=1" + extra)
return mcp_json_result(resp) return mcp_json_result(resp)
} }
@@ -502,39 +744,51 @@ fn tool_inspect_memories(args: String) -> String {
fn tool_inspect_graph(args: String) -> String { fn tool_inspect_graph(args: String) -> String {
let entity_id: String = json_get_string(args, "entity_id") let entity_id: String = json_get_string(args, "entity_id")
let name: String = json_get_string(args, "name") let name: String = json_get_string(args, "name")
let depth: Int = json_get_int(args, "max_depth") // Accept `depth` (documented/canonical) and fall back to legacy `max_depth`.
if depth == 0 { let depth = 1 } // Expression-ifs (not block-scoped re-lets) so the resolution is provably
// reassigned regardless of the language's block-scope rules.
let depth_raw: Int = json_get_int(args, "depth")
let depth_alt: Int = if depth_raw == 0 { json_get_int(args, "max_depth") } else { depth_raw }
let depth: Int = if depth_alt == 0 { 1 } else { depth_alt }
let resolved_id: String = entity_id // Resolve named traversal roots stable hardcoded anchors.
let resolved_id: String = if !str_eq(entity_id, "") { entity_id } else {
// Resolve named traversal roots stable hardcoded anchors
if str_eq(resolved_id, "") {
if str_eq(name, "self") || str_eq(name, "neuron") { if str_eq(name, "self") || str_eq(name, "neuron") {
let resolved_id = "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee" "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee"
} } else {
if str_eq(name, "values") || str_eq(name, "values_hub") { if str_eq(name, "values") || str_eq(name, "values_hub") {
let resolved_id = "kn-5b606390-a52d-4ca2-8e0e-eba141d13440" "kn-5b606390-a52d-4ca2-8e0e-eba141d13440"
} else { "" }
} }
} }
if str_eq(resolved_id, "") { if str_eq(resolved_id, "") {
return mcp_text_result("error: entity_id or name is required. Known names: self, neuron, values, values_hub") return mcp_text_result("error: entity_id or name is required. Known names: self, neuron, values, values_hub")
} }
// compact=1: soul returns a bounded, relevance-ranked neighborhood (top-K // compact defaults ON: the soul returns a bounded, relevance-ranked
// with content, the rest as pointers) so high-fanout nodes (voice, // neighborhood (top-K with content, the rest as pointers) so high-fanout
// writing-imprint) no longer overflow the MCP transport and close the socket. // nodes (voice, writing-imprint) no longer overflow the MCP transport. Pass
let resp: String = http_get(neuron_url() + "/graph?id=" + resolved_id + "&depth=" + int_to_str(depth) + "&compact=1") // compact=0/false to opt into the full neighborhood. snip/k bound it further.
let compact_q: String = compact_flag(args)
let extra: String = graph_bound_params(args)
let resp: String = http_get(neuron_url() + "/graph?id=" + resolved_id + "&depth=" + int_to_str(depth) + "&compact=" + compact_q + extra)
return mcp_json_result(resp) return mcp_json_result(resp)
} }
fn tool_traverse_graph(args: String) -> String { fn tool_traverse_graph(args: String) -> String {
let id: String = json_get_string(args, "start_id") // Accept `entity_id` (canonical) with `start_id` as a legacy alias.
let depth: Int = json_get_int(args, "depth") let eid: String = json_get_string(args, "entity_id")
if depth == 0 { let depth = 2 } let id: String = if !str_eq(eid, "") { eid } else { json_get_string(args, "start_id") }
let depth_raw: Int = json_get_int(args, "depth")
let depth: Int = if depth_raw == 0 { 2 } else { depth_raw }
if str_eq(id, "") { if str_eq(id, "") {
return mcp_text_result("error: start_id is required") return mcp_text_result("error: entity_id (or start_id) is required")
} }
let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth)) // compact defaults ON so a depth-2 walk from a high-fanout node stays within
// the transport limit. Pass compact=0/false for the full neighborhood.
let compact_q: String = compact_flag(args)
let extra: String = graph_bound_params(args)
let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth) + "&compact=" + compact_q + extra)
return mcp_json_result(resp) return mcp_json_result(resp)
} }