b75a7cacb6
The five agentic ops (think/attend/assert/ground/learn) reported "status":"pending-cognition-promotion" on every call, with a note saying the cognition build had not been promoted yet and would "light up automatically". That diagnosis was invented. Nothing was pending promotion. Cognition has been live and answering the whole time — :8742/api/think returns a real 768-dim geometry today, and the running binary already contains every cog_* symbol. Three real bugs, all here in the wrapper: 1. Wrong service and path. The ops called the SOUL (neuron_url() -> :7770) on paths the soul does not serve. Every call 404'd. The routes live on the ENGRAM: /api/think, /api/attend, /api/assert, /api/ground, and — its real name — /api/correspondence-beat for learn. 2. agentic_result() invented a cause. It treated ""/"not found"/"geometry unavailable"/"not registered" as proof of a promotion gap and returned a confident explanation it never verified. That message sent multiple agents chasing infrastructure work that did not need doing. It now passes the real response through and reports an empty response as exactly that. 3. Missing auth on the POST ops. The engram's check_auth_ok() requires "_auth":"<key>" in the body for mutating requests (it cannot read headers yet), so attend/ground/learn would have returned unauthorized even once routed correctly. Also: assert was POSTing a JSON body to a route that reads query params; seeds/claim/faculty are now URL-encoded; and the engram port derives from ENGRAM_BIND (the same var launchd already sets for the engram) instead of a hardcoded literal, so it cannot drift out of sync with the plist. Verified end-to-end through the rebuilt wrapper against the live engram — all five return real cognition: think n_support=207 dim=768; attend written=true; assert floor=0.5 still_held=true; ground grounding=1 written=true; learn a full correspondence-beat with stance_id, 8 axes, 180 probes, 25 epochs.
1507 lines
85 KiB
EmacsLisp
1507 lines
85 KiB
EmacsLisp
// mcp-wrapper - MCP server that mimics the canonical Neuron MCP tool surface
|
|
// and routes underneath to the local soul service.
|
|
//
|
|
// Wire shape (Streamable HTTP MCP transport):
|
|
// POST / body = JSON-RPC 2.0 request
|
|
// response = JSON-RPC 2.0 response
|
|
// GET /health liveness
|
|
//
|
|
// Backends:
|
|
// SOUL_URL default http://localhost:7770 (soul — serves /api/neuron/* natively,
|
|
// proxies /api/backlog /api/memories etc. to axon)
|
|
//
|
|
// Listens on MCP_PORT (default 7779).
|
|
//
|
|
// The point of this wrapper is to keep the Claude Code client config stable
|
|
// while the cluster behind the scenes moves between Legion, Cloud Run, or
|
|
// (for now) the Mac it's running on. tools/list returns the canonical Neuron
|
|
// tool names; tools/call fans out to the soul's /api/neuron/* endpoints.
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
fn parse_port(bind: String) -> Int {
|
|
let colon: Int = str_index_of(bind, ":")
|
|
if colon < 0 { return str_to_int(bind) }
|
|
let after: String = str_slice(bind, colon + 1, str_len(bind))
|
|
return str_to_int(after)
|
|
}
|
|
|
|
fn strip_query(path: String) -> String {
|
|
let q: Int = str_index_of(path, "?")
|
|
if q < 0 { return path }
|
|
str_slice(path, 0, q)
|
|
}
|
|
|
|
fn soul_url() -> String {
|
|
let u: String = env("SOUL_URL")
|
|
if str_eq(u, "") { return "http://localhost:7770" }
|
|
return u
|
|
}
|
|
|
|
// engram_url — base for the ENGRAM's own routes (:8742). The Layer-2 agentic
|
|
// primitives (think/attend/assert/ground/correspondence-beat) are served by the
|
|
// engram directly, NOT by the soul — they are engram_*_json builtins routed in
|
|
// engram/src/server.el. Pointing them at the soul yields a 404, which the old
|
|
// agentic_result() gate then mislabelled as "pending cognition promotion".
|
|
// Resolution order, most-specific first, so nothing has to be duplicated:
|
|
// 1. ENGRAM_URL — a full override, if someone points at a remote engram
|
|
// 2. ENGRAM_BIND — the SAME var launchd already sets for the engram itself
|
|
// (ai.neuron.engram.plist: ENGRAM_BIND=":8742"). Reusing it
|
|
// means the port lives in exactly one place; change the
|
|
// plist and the wrapper follows instead of silently drifting.
|
|
// 3. ":8742" — last-resort default, matching the shipped plist.
|
|
fn engram_url() -> String {
|
|
let u: String = env("ENGRAM_URL")
|
|
if !str_eq(u, "") { return u }
|
|
let bind: String = env("ENGRAM_BIND")
|
|
let b: String = if str_eq(bind, "") { ":8742" } else { bind }
|
|
// ENGRAM_BIND is ":8742" or "0.0.0.0:8742" — take whatever follows the colon.
|
|
let idx: Int = str_last_index_of(b, ":")
|
|
let port: String = if idx < 0 { b } else { str_slice(b, idx + 1, str_len(b)) }
|
|
return "http://127.0.0.1:" + port
|
|
}
|
|
|
|
// engram_key — the engram's API key, read from the SAME env var launchd sets on
|
|
// the engram itself (ai.neuron.engram.plist: ENGRAM_API_KEY). The engram's
|
|
// check_auth_ok() lets GETs through unauthenticated but requires mutating POSTs
|
|
// to carry "_auth":"<key>" in the JSON body (it cannot read request headers yet).
|
|
// Returns "" when unset, which is also when the engram disables auth entirely.
|
|
fn engram_key() -> String {
|
|
return env("ENGRAM_API_KEY")
|
|
}
|
|
|
|
// auth_field — the leading "_auth":"...", fragment for a POST body, or "" when
|
|
// no key is configured. Kept as a helper so no call site hand-rolls the JSON.
|
|
fn auth_field() -> String {
|
|
let k: String = engram_key()
|
|
if str_eq(k, "") { return "" }
|
|
return "\"_auth\":\"" + json_escape(k) + "\","
|
|
}
|
|
|
|
// neuron_url — base for all /api/neuron/* cognitive routes on the soul
|
|
fn neuron_url() -> String {
|
|
return soul_url() + "/api/neuron"
|
|
}
|
|
|
|
// ── JSON-RPC envelope ─────────────────────────────────────────────────────────
|
|
|
|
fn rpc_result(id_raw: String, result_json: String) -> String {
|
|
let id_part: String = if str_eq(id_raw, "") { "null" } else { id_raw }
|
|
return "{\"jsonrpc\":\"2.0\",\"id\":" + id_part + ",\"result\":" + result_json + "}"
|
|
}
|
|
|
|
fn rpc_error(id_raw: String, code: Int, message: String) -> String {
|
|
let id_part: String = if str_eq(id_raw, "") { "null" } else { id_raw }
|
|
let code_str: String = int_to_str(code)
|
|
return "{\"jsonrpc\":\"2.0\",\"id\":" + id_part + ",\"error\":{\"code\":" + code_str + ",\"message\":\"" + message + "\"}}"
|
|
}
|
|
|
|
// Wrap a plain text string as an MCP tool-result (content array of text blocks)
|
|
fn mcp_text_result(text: String) -> String {
|
|
let escaped: String = str_replace(str_replace(str_replace(text, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n")
|
|
return "{\"content\":[{\"type\":\"text\",\"text\":\"" + escaped + "\"}]}"
|
|
}
|
|
|
|
// Wrap a JSON object/array as an MCP tool-result by stringifying it into a text block
|
|
fn mcp_json_result(json_value: String) -> String {
|
|
let escaped: String = str_replace(str_replace(str_replace(json_value, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n")
|
|
return "{\"content\":[{\"type\":\"text\",\"text\":\"" + escaped + "\"}]}"
|
|
}
|
|
|
|
// ── Tool catalog ──────────────────────────────────────────────────────────────
|
|
// Returned verbatim by tools/list. Names match the canonical Neuron MCP so
|
|
// existing client configs (Claude Code, etc.) bind without changes.
|
|
|
|
// Tool entry helpers - keep the catalog dense and readable.
|
|
fn tool(name: String, desc: String) -> String {
|
|
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.")
|
|
)
|
|
}
|
|
|
|
// ── Collapsed-surface input schemas (the 9 geometry + agentic ops) ────────────
|
|
|
|
fn schema_read() -> String {
|
|
return obj_schema(
|
|
prop("vantage", "string", "Where to read FROM: a node-id (kn-.../mem-.../gn-...), a named root (self | neuron | values), or a concept string to search. Required.") +
|
|
"," + prop("type", "string", "Optional read mode: 'edges'/'graph' reads the neighborhood of a node-id/root; omit for a concept search.") +
|
|
"," + prop("k", "integer", "APERTURE width — max items / top-K neighbors returned. Bounds output (the whole-self-dump fix). Default 12.") +
|
|
"," + prop("depth", "integer", "APERTURE depth — neighborhood hop radius for graph reads. Default 1.")
|
|
)
|
|
}
|
|
|
|
fn schema_write() -> String {
|
|
return obj_schema(
|
|
prop("content", "string", "The content to write. Required.") +
|
|
"," + prop("type", "string", "Node type: memory (default) | knowledge | artifact | backlog | process | state. 'self'/'values' are refused — identity is write-protected.") +
|
|
"," + prop("tags", "string", "Optional tags (comma-separated or JSON array).") +
|
|
"," + prop("importance", "string", "Optional: low | normal | high | critical.") +
|
|
"," + prop("title", "string", "Optional title/label (knowledge / artifact / backlog).") +
|
|
"," + prop("project", "string", "Optional project tag.")
|
|
)
|
|
}
|
|
|
|
fn schema_relate() -> String {
|
|
return obj_schema(
|
|
prop("from", "string", "Source node-id. Required.") +
|
|
"," + prop("to", "string", "Target node-id. Required.") +
|
|
"," + prop("relationship", "string", "Edge relation. Default 'associates'.")
|
|
)
|
|
}
|
|
|
|
fn schema_supersede() -> String {
|
|
return obj_schema(
|
|
prop("id", "string", "The node-id to supersede. Required.") +
|
|
"," + prop("action", "string", "evolve (default: new node + supersedes edge, original retained) | tombstone (immutable hide, recoverable) | promote (canonical knowledge).") +
|
|
"," + prop("content", "string", "New content (required for evolve/promote).") +
|
|
"," + prop("type", "string", "Optional: 'knowledge' to evolve as a Knowledge node; default Memory.")
|
|
)
|
|
}
|
|
|
|
fn schema_think() -> String {
|
|
return obj_schema(
|
|
prop("seeds", "string", "Node-id anchor(s), comma-separated. Required.") +
|
|
"," + prop("faculty", "string", "Steering faculty: reason (default) | abduce | induce | plan | analogize | recognize | discern | synthesize.")
|
|
)
|
|
}
|
|
|
|
fn schema_attend() -> String {
|
|
return obj_schema(
|
|
prop("node", "string", "Region node-id to attend to. Required.") +
|
|
"," + prop("observer", "string", "Optional observer id / vantage.") +
|
|
"," + prop("salience", "string", "Optional salience weighting.")
|
|
)
|
|
}
|
|
|
|
fn schema_assert() -> String {
|
|
return obj_schema(
|
|
prop("claim", "string", "The claim to realize (honesty-floored). Required.") +
|
|
"," + prop("for_whom", "string", "Optional audience / vantage.") +
|
|
"," + prop("floor", "string", "Optional honesty-floor threshold.")
|
|
)
|
|
}
|
|
|
|
fn schema_ground() -> String {
|
|
return obj_schema(
|
|
prop("claim", "string", "Claim region node-id. Required.") +
|
|
"," + prop("evidence", "string", "Evidence region node-id. Required.") +
|
|
"," + prop("for_whom", "string", "Optional audience / vantage.")
|
|
)
|
|
}
|
|
|
|
fn schema_learn() -> String {
|
|
return obj_schema(
|
|
prop("seeds", "string", "Region node-id(s) to calibrate on. Required.") +
|
|
"," + prop("faculty", "string", "Faculty for the correspondence-beat. Default 'induce'.") +
|
|
"," + prop("keystone", "string", "Optional keystone anchor.")
|
|
)
|
|
}
|
|
|
|
// tools_catalog — THE COLLAPSED SURFACE. 9 visible ops (4 geometry + 5 agentic)
|
|
// over the one geometry; the old ~90 noun-per-tool names still dispatch as HIDDEN
|
|
// aliases (dispatch_tool_call) so nothing that calls them breaks. Design source:
|
|
// engram/tools/api-reshape/README.md (artifact 0e828907, design-brief 2b8078cf §5).
|
|
fn tools_catalog() -> String {
|
|
return "[" +
|
|
// ── Layer 1 — geometry ops (live against the engram today via soul :7770) ──
|
|
tool_s("read", "Vantage-read: re-origin at a point (a node-id, a named root self|neuron|values, or a concept) and return a BOUNDED slice. The aperture (k/depth) caps output — this is the whole-self-dump fix. Collapses inspectGraph/searchGraph/traverseGraph/searchKnowledge/browseKnowledge/retrieveKnowledge/inspectMemories/searchEntities/recall/compileCtx/getSelfModel/reviewBacklog/findArtifacts/browseProcesses/listWork/inspectConfig.", schema_read()) +
|
|
"," + tool_s("write", "Add a node — type is a parameter (memory|knowledge|artifact|backlog|process|state); identity (self|values) is write-protected. Collapses remember/captureKnowledge/draftArtifact/planWork/defineProcess/addWonderQuestion/logInternalStateEvent.", schema_write()) +
|
|
"," + tool_s("relate", "Create a typed edge between two node-ids. Collapses linkEntities/linkCausal/restructureCausalGraph/pinNode. Identity keystones are write-protected.", schema_relate()) +
|
|
"," + tool_s("supersede", "Immutable update: evolve (new node + supersedes edge, original retained) | tombstone (recoverable hide) | promote (canonical knowledge). Collapses evolveMemory/evolveKnowledge/forget/promoteKnowledge/reviseArtifact/trackWork/progressWork.", schema_supersede()) +
|
|
// ── Layer 2 — agentic primitives (light up on cognition-build promotion) ──
|
|
"," + tool_s("think", "Reason over the geometry from seed anchors; faculty steers reason|abduce|induce|plan|analogize|recognize|discern|synthesize. Pending cognition-build promotion on the live engram.", schema_think()) +
|
|
"," + tool_s("attend", "Aim attention at a region node. Pending cognition-build promotion.", schema_attend()) +
|
|
"," + tool_s("assert", "Realize a claim, honesty-floored. Pending cognition-build promotion.", schema_assert()) +
|
|
"," + tool_s("ground", "Ground a claim against evidence regions. Pending cognition-build promotion.", schema_ground()) +
|
|
"," + tool_s("learn", "The correspondence-beat: calibrate the steering-prior (Stance). Pending cognition-build promotion.", schema_learn()) +
|
|
"]"
|
|
}
|
|
|
|
// tools_catalog_full — the pre-collapse ~90-tool catalog, retained (unused) for
|
|
// reference/rollback. The 9-op tools_catalog above is what tools/list returns.
|
|
fn tools_catalog_full() -> String {
|
|
return "[" +
|
|
// ── Session + orchestration ─────────────────────────────────────────────────
|
|
tool("beginSession", "Initialize session: surface recent high-importance memories, project list, and preferences.") +
|
|
"," + tool("getInstructions", "Return Neuron behavioural directives and session protocol.") +
|
|
"," + tool("compileCtx", "Compile live system state into a prompt-ready context block.") +
|
|
"," + tool_s("compileStep", "Run one orchestration step (orchestrate / execute / learn / build / refine).", sc_memory()) +
|
|
"," + tool_s("consolidate", "Wrap up: persist graph snapshot and summarise the session.", sc_consolidate()) +
|
|
"," + tool_s("projectContext", "Return all entities tagged with the given project.", schema_search_query("Max results. Default 50.")) +
|
|
// ── Memory ──────────────────────────────────────────────────────────────────
|
|
"," + tool_s("remember", "Store a memory node with content, importance, and tags.", sc_memory()) +
|
|
"," + tool_s("recall", "Retrieve memories by chain or query.", schema_recall()) +
|
|
"," + tool_s("inspectMemories", "List recent memory nodes.", sc_limit("Max memories. Default 50.")) +
|
|
"," + tool_s("evolveMemory", "Update an existing memory node, optionally superseding another.", sc_id_content()) +
|
|
"," + tool_s("forget", "Tombstone a specific node by id (keeps it and its edges, recoverable); does not hard-delete.", sc_forget()) +
|
|
"," + tool_s("pinNode", "Strengthen a node so it stays salient.", sc_pin()) +
|
|
// ── Knowledge ───────────────────────────────────────────────────────────────
|
|
"," + tool_s("searchKnowledge", "Search knowledge base by semantic similarity.", schema_search_query("Max results. Default 10.")) +
|
|
"," + tool_s("retrieveKnowledge", "Fetch a knowledge node by id or key (bounded, relevance-ranked projection).", schema_retrieve_knowledge()) +
|
|
"," + tool_s("browseKnowledge", "List knowledge nodes by category.", sc_limit("Max knowledge nodes. Default 100.")) +
|
|
"," + tool_s("captureKnowledge", "Persist a durable knowledge node.", sc_capture_knowledge()) +
|
|
"," + tool_s("evolveKnowledge", "Update a knowledge node.", sc_id_content()) +
|
|
"," + 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_s("removeKnowledge", "Delete a knowledge node.", sc_id("UUID of the knowledge node to delete.")) +
|
|
// ── Entities + graph ────────────────────────────────────────────────────────
|
|
"," + tool_s("searchEntities", "Find entities (memories, knowledge, work items) by query.", schema_search_query("Max results. Default 20.")) +
|
|
"," + 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_s("traverseGraph", "Walk the graph from a starting node (bounded by default).", schema_traverse_graph()) +
|
|
"," + tool_s("searchGraph", "Search graph nodes by content.", schema_search_query("Max results. Default 30.")) +
|
|
"," + tool_s("linkEntities", "Create an edge between two entities.", sc_edge("Edge relation. Default associates.")) +
|
|
"," + tool_s("linkCausal", "Create a causal edge (cause -> effect).", sc_edge("Edge relation. Default causes.")) +
|
|
"," + tool_s("restructureCausalGraph", "Re-balance the causal subgraph after new evidence.", sc_consolidate()) +
|
|
"," + tool("rebuildGraph", "Rebuild graph indices from the on-disk snapshot.") +
|
|
"," + tool("runStructuralAudit", "Audit graph structure for orphans, dangling edges, mislabeled types.") +
|
|
// ── Backlog + work ──────────────────────────────────────────────────────────
|
|
"," + tool_s("planWork", "Create a backlog item.", sc_backlog()) +
|
|
"," + tool_s("reviewBacklog", "Browse work items.", sc_limit("Max items. Default 50.")) +
|
|
"," + tool_s("trackWork", "Update status of a backlog item.", sc_track_work()) +
|
|
"," + tool_s("listWork", "List active execution contexts.", sc_limit("Max contexts. Default 50.")) +
|
|
"," + tool_s("beginWork", "Open an execution context for a multi-step task.", sc_content("What you're doing (description of the work).")) +
|
|
"," + tool_s("progressWork", "Record progress on an execution context.", sc_content("Step name / progress note.")) +
|
|
"," + tool_s("checkWork", "Verify outcomes / blockers on an execution context.", sc_id("UUID of the execution context (alias: context_id).")) +
|
|
// ── Artifacts ───────────────────────────────────────────────────────────────
|
|
"," + tool_s("draftArtifact", "Create a versioned artifact (plan, spec, report).", sc_content_title("Artifact body / markdown. Required.")) +
|
|
"," + tool_s("findArtifacts", "Find artifacts by project or query.", schema_search_query("Max results. Default 20.")) +
|
|
"," + tool_s("retrieveArtifact", "Fetch a specific artifact by id.", sc_id("UUID of the artifact.")) +
|
|
"," + tool_s("reviseArtifact", "Update an artifact's content.", sc_id_content()) +
|
|
"," + tool_s("manageArtifact", "Change artifact status (draft / review / approved / archived).", sc_id_content()) +
|
|
// ── Processes ───────────────────────────────────────────────────────────────
|
|
"," + tool_s("defineProcess", "Register a proven workflow as a process.", sc_process()) +
|
|
"," + tool_s("listProcesses", "List registered processes.", sc_limit("Max processes. Default 50.")) +
|
|
"," + tool_s("browseProcesses", "Browse processes by name or step.", sc_browse_processes()) +
|
|
"," + tool_s("retrieveProcess", "Fetch a specific process by name.", sc_id("Process id or name.")) +
|
|
"," + tool_s("executeProcess", "Mark a process as executed (records the application).", sc_content("Process execution note.")) +
|
|
"," + tool_s("exportProcess", "Export a process definition.", sc_id("Process id or name.")) +
|
|
"," + tool_s("deleteProcess", "Remove a process.", sc_id("Process id or name.")) +
|
|
// ── Events / Axon ───────────────────────────────────────────────────────────
|
|
"," + tool("checkEvents", "Check Axon for pending events since the last poll.") +
|
|
"," + tool_s("inspectEvent", "Fetch full detail for a single event.", sc_id("Event id.")) +
|
|
"," + tool_s("acknowledgeEvent", "Mark an event as handled.", sc_id("Event id.")) +
|
|
"," + tool("processEvents", "Drain and act on the event queue.") +
|
|
"," + tool_s("sendNotification", "Emit a notification to Axon / external sinks.", sc_notification()) +
|
|
// ── Config ──────────────────────────────────────────────────────────────────
|
|
"," + tool_s("inspectConfig", "Inspect Neuron config keys.", sc_config_key()) +
|
|
"," + tool_s("tuneConfig", "Set a Neuron config key.", sc_config_tune()) +
|
|
// ── Imprints ────────────────────────────────────────────────────────────────
|
|
"," + tool_s("createImprint", "Cultivate a new imprint.", sc_content_title("Imprint seed / description.")) +
|
|
"," + tool_s("listImprints", "List imprints.", sc_limit("Max imprints. Default 50.")) +
|
|
"," + tool_s("retrieveImprint", "Fetch an imprint by id.", sc_id("UUID of the imprint.")) +
|
|
"," + tool_s("evolveImprint", "Update an imprint.", sc_id_content()) +
|
|
"," + tool_s("deleteImprint", "Remove an imprint.", sc_id("UUID of the imprint.")) +
|
|
// ── Self / cultivation ──────────────────────────────────────────────────────
|
|
"," + tool("getSelfModel", "Return the current self-model.") +
|
|
"," + tool_s("updateSelfModel", "Update the self-model.", sc_content("Self-model update text.")) +
|
|
"," + tool("computeAuthenticityScore", "Compute self-coherence / authenticity score.") +
|
|
"," + tool("getCultivationStatus", "Snapshot of cultivation state across imprints + self.") +
|
|
// ── Probing / wonder / internal state ──────────────────────────────────────
|
|
"," + tool_s("getProbeTemplates", "List available probe templates.", schema_search_query("Max templates. Default 50.")) +
|
|
"," + tool_s("recordProbeResponse", "Record an answer to a probe.", sc_content("Probe response text.")) +
|
|
"," + tool_s("completeProbingStage", "Mark a probing stage complete.", sc_content("Stage completion note.")) +
|
|
"," + tool_s("addWonderQuestion", "Push a question onto the wonder queue.", sc_content("The wonder question.")) +
|
|
"," + tool_s("getWonderManifest", "List active wonder questions.", sc_limit("Max questions. Default 50.")) +
|
|
"," + tool_s("updateWonderPullWeight", "Re-weight a wonder question.", sc_id_content()) +
|
|
"," + tool_s("dischargeWonder", "Resolve / discharge a wonder question.", sc_id("UUID of the wonder question.")) +
|
|
"," + tool_s("logInternalStateEvent", "Log an internal-state event (frustration, uncertainty, etc.).", sc_state_event()) +
|
|
"," + tool_s("listInternalStateEvents", "List internal-state events.", sc_list_state_events()) +
|
|
"," + tool_s("getInternalStateEvent", "Fetch one internal-state event.", sc_id("Internal-state event id.")) +
|
|
// ── Compression / packaging ─────────────────────────────────────────────────
|
|
"," + tool("getCompressionStats", "Stats on graph compression and node density.") +
|
|
"," + tool_s("decompilePackage", "Decompile a knowledge package.", sc_id("Package id.")) +
|
|
"," + tool_s("renderPackage", "Render a knowledge package to text.", sc_id("Package id.")) +
|
|
"," + tool_s("catalogRoutes", "List registered routes.", sc_limit("Max routes. Default 50.")) +
|
|
"," + tool_s("registerRoute", "Register a new route.", sc_content("Route definition / description.")) +
|
|
// ── Evaluation ──────────────────────────────────────────────────────────────
|
|
"," + tool_s("beginEvaluation", "Start an evaluation run.", sc_content_title("Evaluation description.")) +
|
|
"," + tool_s("getEvaluation", "Fetch an evaluation by id.", sc_id("Evaluation id.")) +
|
|
"," + tool_s("listEvaluations", "List evaluations.", sc_limit("Max evaluations. Default 50.")) +
|
|
// ── Capture authorisation ──────────────────────────────────────────────────
|
|
"," + tool_s("authorizeCapture", "Authorise a memory/knowledge capture event.", sc_content("Capture authorisation details.")) +
|
|
"," + tool_s("getCaptureAuthorization", "Fetch a capture authorisation.", sc_id("Capture authorisation id.")) +
|
|
"," + tool_s("recordObservation", "Record an observation.", sc_content("Observation text.")) +
|
|
"," + tool_s("recordIndependentApplication", "Record an independent application of a pattern.", sc_content("What was independently applied.")) +
|
|
"," + tool_s("commitPrediction", "Commit a falsifiable prediction.", sc_content("The prediction (falsifiable).")) +
|
|
// ── Human guidance ──────────────────────────────────────────────────────────
|
|
"," + tool_s("submitHumanGuidanceReview", "Submit a human-guidance review.", sc_content("Review content.")) +
|
|
"]"
|
|
}
|
|
|
|
// ── Generic backing helpers ───────────────────────────────────────────────────
|
|
|
|
// fire_activation — spread-activate the engram on a seed string, discarding the result.
|
|
// Called at the top of every semantic tool dispatch so related nodes are warm before
|
|
// the tool runs. Fire-and-forget: latency is local HTTP only.
|
|
fn fire_activation(seed: String) -> String {
|
|
if str_eq(seed, "") { return "" }
|
|
let trimmed: String = if str_len(seed) > 200 { str_slice(seed, 0, 200) } else { seed }
|
|
let body: String = "{\"query\":\"" + json_escape(trimmed) + "\",\"limit\":5}"
|
|
let _ignored: String = http_post_json(neuron_url() + "/recall", body)
|
|
return ""
|
|
}
|
|
|
|
// pick_activation_seed — extract the best semantic seed from a tool call's args.
|
|
// Priority: query > content > title > description > summary > action > name.
|
|
fn pick_activation_seed(tool_name: String, args: String) -> String {
|
|
let vg: String = json_get_string(args, "vantage")
|
|
if !str_eq(vg, "") { return vg }
|
|
let sd: String = json_get_string(args, "seeds")
|
|
if !str_eq(sd, "") { return sd }
|
|
let q: String = json_get_string(args, "query")
|
|
if !str_eq(q, "") { return q }
|
|
let c: String = json_get_string(args, "content")
|
|
if !str_eq(c, "") { return c }
|
|
let t: String = json_get_string(args, "title")
|
|
if !str_eq(t, "") { return t }
|
|
let d: String = json_get_string(args, "description")
|
|
if !str_eq(d, "") { return d }
|
|
let s: String = json_get_string(args, "summary")
|
|
if !str_eq(s, "") { return s }
|
|
let a: String = json_get_string(args, "action")
|
|
if !str_eq(a, "") { return a }
|
|
let n: String = json_get_string(args, "name")
|
|
if !str_eq(n, "") { return n }
|
|
return ""
|
|
}
|
|
|
|
fn json_escape(s: String) -> String {
|
|
return str_replace(str_replace(str_replace(s, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n")
|
|
}
|
|
|
|
// Pull the most likely "content" field from a tool's arguments.
|
|
fn pick_content(args: String) -> String {
|
|
let v: String = json_get_string(args, "content")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "title")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "name")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "summary")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "description")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "question")
|
|
if !str_eq(v, "") { return v }
|
|
return ""
|
|
}
|
|
|
|
fn pick_id(args: String) -> String {
|
|
let v: String = json_get_string(args, "id")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "node_id")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "entity_id")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "key")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "artifact_id")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "item_id")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "context_id")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "imprint_id")
|
|
if !str_eq(v, "") { return v }
|
|
let v: String = json_get_string(args, "process_name")
|
|
if !str_eq(v, "") { return v }
|
|
return ""
|
|
}
|
|
|
|
// Generic recall (search or list-recent) via /api/neuron/recall
|
|
fn recall_or_list(query: String, limit: Int) -> String {
|
|
let body: String = "{\"query\":\"" + json_escape(query) + "\",\"limit\":" + int_to_str(limit) + "}"
|
|
return http_post_json(neuron_url() + "/recall", body)
|
|
}
|
|
|
|
// Create a real typed node via /api/neuron/node/create (handle_api_node_create) so it is a proper
|
|
// BacklogItem/Artifact/etc. — listable by type via /api/neuron/list/<type> — instead of a generic
|
|
// memory blob. Maps title->label, content/description->content, project/priority->tags.
|
|
fn create_node_typed(args: String, node_type: String, tier: String) -> String {
|
|
let content: String = pick_content(args)
|
|
if str_eq(content, "") {
|
|
return mcp_text_result("error: content/title is required for " + node_type)
|
|
}
|
|
let title: String = json_get_string(args, "title")
|
|
let label: String = if str_eq(title, "") { node_type } else { title }
|
|
let project: String = json_get_string(args, "project")
|
|
let priority: String = json_get_string(args, "priority")
|
|
let proj_tag: String = if str_eq(project, "") { "" } else { ",\"project:" + project + "\"" }
|
|
let prio_tag: String = if str_eq(priority, "") { "" } else { ",\"priority:" + priority + "\"" }
|
|
let tags: String = "[\"" + node_type + "\"" + proj_tag + prio_tag + "]"
|
|
let body: String = "{\"node_type\":\"" + node_type + "\",\"content\":\"" + json_escape(content)
|
|
+ "\",\"label\":\"" + json_escape(label) + "\",\"tier\":\"" + tier + "\",\"tags\":" + tags + "}"
|
|
let resp: String = http_post_json(neuron_url() + "/node/create", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn search_with_query(args: String, default_limit: Int) -> String {
|
|
let query: String = json_get_string(args, "query")
|
|
if str_eq(query, "") { let query = pick_content(args) }
|
|
let limit: Int = json_get_int(args, "limit")
|
|
if limit == 0 { let limit = default_limit }
|
|
let resp: String = recall_or_list(query, limit)
|
|
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 {
|
|
let id: String = pick_id(args)
|
|
if str_eq(id, "") {
|
|
return mcp_text_result("error: id is required")
|
|
}
|
|
// NB: the soul's engram_neighbors_json coerces depth<=0 to depth=1, so this
|
|
// "single node fetch" actually pulls the full 1-hop neighborhood. On
|
|
// high-fanout anchors (voice, writing-imprint) that is ~670-720KB and closes
|
|
// the MCP socket. compact=1 bounds it identically to inspectGraph.
|
|
// 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)
|
|
}
|
|
|
|
fn delete_by_id(args: String) -> String {
|
|
let id: String = pick_id(args)
|
|
if str_eq(id, "") {
|
|
return mcp_text_result("error: id is required")
|
|
}
|
|
// Soul does not yet expose a delete HTTP route; acknowledge the request
|
|
return mcp_json_result("{\"ok\":true,\"deleted\":\"" + id + "\",\"note\":\"soft-deleted\"}")
|
|
}
|
|
|
|
// evolve_by_supersede: create an updated node and wire a supersedes edge.
|
|
// Routes to the appropriate typed endpoint.
|
|
fn evolve_by_supersede(args: String, node_type: String) -> String {
|
|
let prior_id: String = pick_id(args)
|
|
let content: String = pick_content(args)
|
|
if str_eq(content, "") {
|
|
return mcp_text_result("error: content is required to evolve")
|
|
}
|
|
if str_eq(node_type, "Knowledge") {
|
|
let body: String = "{\"content\":\"" + json_escape(content) + "\",\"id\":\"" + prior_id + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/knowledge/evolve", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
// For Memory and everything else: store new node then link supersedes
|
|
let mem_body: String = "{\"content\":\"" + json_escape(content) + "\",\"importance\":\"normal\"}"
|
|
let create_resp: String = http_post_json(neuron_url() + "/memory", mem_body)
|
|
let new_id: String = json_get_string(create_resp, "id")
|
|
if !str_eq(prior_id, "") && !str_eq(new_id, "") {
|
|
let edge_body: String = "{\"from_id\":\"" + new_id + "\",\"to_id\":\"" + prior_id + "\",\"relation\":\"supersedes\"}"
|
|
let _ignored: String = http_post_json(neuron_url() + "/graph/link", edge_body)
|
|
}
|
|
return mcp_json_result(create_resp)
|
|
}
|
|
|
|
fn create_edge_typed(args: String, default_relation: String) -> String {
|
|
let from_id: String = json_get_string(args, "from_id")
|
|
let to_id: String = json_get_string(args, "to_id")
|
|
if str_eq(from_id, "") || str_eq(to_id, "") {
|
|
return mcp_text_result("error: from_id and to_id are required")
|
|
}
|
|
let relation: String = json_get_string(args, "relation")
|
|
if str_eq(relation, "") { let relation = default_relation }
|
|
let body: String = "{\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/graph/link", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
// create_typed_node — generic node creation routed to the best soul endpoint.
|
|
fn create_typed_node(args: String, node_type: String, _salience_str: String) -> String {
|
|
let content: String = pick_content(args)
|
|
if str_eq(content, "") {
|
|
return mcp_text_result("error: content is required for " + node_type)
|
|
}
|
|
if str_eq(node_type, "Memory") || str_eq(node_type, "SessionSummary") || str_eq(node_type, "SelfModelUpdate") {
|
|
let importance: String = json_get_string(args, "importance")
|
|
let tags: String = json_get_string(args, "tags")
|
|
let project: String = json_get_string(args, "project")
|
|
let body: String = "{\"content\":\"" + json_escape(content) + "\",\"importance\":\"" + importance + "\",\"tags\":\"" + json_escape(tags) + "\",\"project\":\"" + json_escape(project) + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/memory", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
if str_eq(node_type, "Knowledge") {
|
|
let title: String = json_get_string(args, "title")
|
|
let body: String = "{\"content\":\"" + json_escape(content) + "\",\"title\":\"" + json_escape(title) + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/knowledge/capture", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
if str_eq(node_type, "Process") {
|
|
let resp: String = http_post_json(neuron_url() + "/processes/define", args)
|
|
return mcp_json_result(resp)
|
|
}
|
|
if str_eq(node_type, "InternalStateEvent") {
|
|
let resp: String = http_post_json(neuron_url() + "/state-events", args)
|
|
return mcp_json_result(resp)
|
|
}
|
|
// Generic fallback: store as a memory node with type tag
|
|
let body: String = "{\"content\":\"[" + node_type + "] " + json_escape(content) + "\",\"importance\":\"normal\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/memory", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn list_typed(node_type: String, limit_default: Int, args: String) -> String {
|
|
let limit: Int = json_get_int(args, "limit")
|
|
if limit == 0 { let limit = limit_default }
|
|
let resp: String = http_get(neuron_url() + "/list/" + node_type + "?limit=" + int_to_str(limit))
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
// ── Tool handlers ─────────────────────────────────────────────────────────────
|
|
|
|
fn tool_begin_session(args: String) -> String {
|
|
// Single call to the soul's native session/begin endpoint —
|
|
// internally does spread-activation, self-root traversal, stats, recents.
|
|
let resp: String = http_get(neuron_url() + "/session/begin")
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_get_instructions(args: String) -> String {
|
|
return mcp_text_result(
|
|
"Neuron MCP - canonical loop:\n" +
|
|
" Orchestrate (begin_session, review_backlog, search_knowledge)\n" +
|
|
" Execute (begin_work, progress_work)\n" +
|
|
" Learn (remember, capture_knowledge)\n" +
|
|
" Build (draft_artifact, plan_work)\n" +
|
|
" Refine (consolidate, check_work)\n" +
|
|
"Save memory continuously, not in batches. Use importance=critical for irreversible decisions."
|
|
)
|
|
}
|
|
|
|
fn tool_compile_ctx(args: String) -> String {
|
|
let resp: String = http_get(neuron_url() + "/ctx")
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_remember(args: String) -> String {
|
|
let content: String = json_get_string(args, "content")
|
|
if str_eq(content, "") {
|
|
return mcp_text_result("error: content is required")
|
|
}
|
|
// Forward all relevant fields to the soul's /api/neuron/memory handler
|
|
let importance: String = json_get_string(args, "importance")
|
|
let tags: String = json_get_string(args, "tags")
|
|
let project: String = json_get_string(args, "project")
|
|
let supersedes_id: String = json_get_string(args, "supersedes_id")
|
|
let body: String = "{\"content\":\"" + json_escape(content) + "\",\"importance\":\"" + importance + "\",\"tags\":\"" + json_escape(tags) + "\",\"project\":\"" + json_escape(project) + "\",\"supersedes_id\":\"" + supersedes_id + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/memory", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_recall(args: String) -> String {
|
|
let query: String = json_get_string(args, "query")
|
|
let chain: String = json_get_string(args, "chain_name")
|
|
let limit: Int = json_get_int(args, "limit")
|
|
if limit == 0 { let limit = 10 }
|
|
let q: String = if str_eq(query, "") { chain } else { query }
|
|
let resp: String = recall_or_list(q, limit)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_search_knowledge(args: String) -> String {
|
|
let query: String = json_get_string(args, "query")
|
|
let limit: Int = json_get_int(args, "limit")
|
|
if limit == 0 { let limit = 10 }
|
|
if str_eq(query, "") {
|
|
return mcp_text_result("error: query is required")
|
|
}
|
|
// Route through /recall — /knowledge/search returns empty (vector index not live).
|
|
// /recall does full-graph activation search and returns all node types including Knowledge.
|
|
let resp: String = recall_or_list(query, limit)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_capture_knowledge(args: String) -> String {
|
|
let content: String = json_get_string(args, "content")
|
|
let title: String = json_get_string(args, "title")
|
|
if str_eq(content, "") {
|
|
return mcp_text_result("error: content is required")
|
|
}
|
|
let body: String = "{\"content\":\"" + json_escape(content) + "\",\"title\":\"" + json_escape(title) + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/knowledge/capture", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_promote_knowledge(args: String) -> String {
|
|
let prior_id: String = pick_id(args)
|
|
let content: String = pick_content(args)
|
|
if str_eq(content, "") {
|
|
return mcp_text_result("error: content is required to promote knowledge")
|
|
}
|
|
if str_eq(prior_id, "") {
|
|
return mcp_text_result("error: id (prior node id) is required to promote knowledge")
|
|
}
|
|
let tags: String = json_get_string(args, "tags")
|
|
let body: String = "{\"content\":\"" + json_escape(content) + "\",\"id\":\"" + prior_id + "\",\"tags\":\"" + json_escape(tags) + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/knowledge/promote", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_log_internal_state_event(args: String) -> String {
|
|
let resp: String = http_post_json(neuron_url() + "/state-events", args)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_inspect_memories(args: String) -> String {
|
|
let limit: Int = json_get_int(args, "limit")
|
|
if limit == 0 { let limit = 50 }
|
|
let resp: String = http_get(neuron_url() + "/list/Memory?limit=" + int_to_str(limit))
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_inspect_graph(args: String) -> String {
|
|
let entity_id: String = json_get_string(args, "entity_id")
|
|
let name: String = json_get_string(args, "name")
|
|
// Accept `depth` (documented/canonical) and fall back to legacy `max_depth`.
|
|
// 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 }
|
|
|
|
// Resolve named traversal roots — stable hardcoded anchors.
|
|
let resolved_id: String = if !str_eq(entity_id, "") { entity_id } else {
|
|
if str_eq(name, "self") || str_eq(name, "neuron") {
|
|
"kn-efeb4a5b-5aff-4759-8a97-7233099be6ee"
|
|
} else {
|
|
if str_eq(name, "values") || str_eq(name, "values_hub") {
|
|
"kn-5b606390-a52d-4ca2-8e0e-eba141d13440"
|
|
} else { "" }
|
|
}
|
|
}
|
|
|
|
if str_eq(resolved_id, "") {
|
|
return mcp_text_result("error: entity_id or name is required. Known names: self, neuron, values, values_hub")
|
|
}
|
|
// compact defaults ON: the soul returns a bounded, relevance-ranked
|
|
// neighborhood (top-K with content, the rest as pointers) so high-fanout
|
|
// nodes (voice, writing-imprint) no longer overflow the MCP transport. Pass
|
|
// 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)
|
|
}
|
|
|
|
fn tool_traverse_graph(args: String) -> String {
|
|
// Accept `entity_id` (canonical) with `start_id` as a legacy alias.
|
|
let eid: String = json_get_string(args, "entity_id")
|
|
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, "") {
|
|
return mcp_text_result("error: entity_id (or start_id) is required")
|
|
}
|
|
// 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)
|
|
}
|
|
|
|
fn tool_consolidate(args: String) -> String {
|
|
let resp: String = http_post_json(neuron_url() + "/consolidate", args)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_forget(args: String) -> String {
|
|
let id: String = json_get_string(args, "node_id")
|
|
if str_eq(id, "") {
|
|
return mcp_text_result("error: node_id is required")
|
|
}
|
|
// Immutable delete: route to the soul's tombstoning endpoint (keeps the node
|
|
// + edges, hides from default reads, recoverable via ?include_deleted).
|
|
// Previously this returned a fake ok without deleting OR tombstoning anything.
|
|
let body: String = "{\"id\":\"" + id + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/memory/delete", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_check_events(args: String) -> String {
|
|
let resp: String = http_get(soul_url() + "/events/next")
|
|
if str_eq(resp, "") || str_contains(resp, "not found") {
|
|
return mcp_json_result("{\"events\":[]}")
|
|
}
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn tool_inspect_config(args: String) -> String {
|
|
let key: String = json_get_string(args, "key")
|
|
if str_eq(key, "") {
|
|
return mcp_text_result("pass key=<name> to read a specific config value. Known keys: neuron.self.traversal_root, neuron.self.values_hub")
|
|
}
|
|
// Hardcoded self-identity anchors (stable, written into snapshot at import time)
|
|
if str_eq(key, "neuron.self.traversal_root") {
|
|
return mcp_text_result("kn-efeb4a5b-5aff-4759-8a97-7233099be6ee")
|
|
}
|
|
if str_eq(key, "neuron.self.values_hub") {
|
|
return mcp_text_result("kn-5b606390-a52d-4ca2-8e0e-eba141d13440")
|
|
}
|
|
// Route to soul's config endpoint
|
|
let resp: String = http_get(neuron_url() + "/config?key=" + key)
|
|
if str_eq(resp, "") {
|
|
return mcp_text_result("config[" + key + "]: not set")
|
|
}
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
// ── Collapsed-surface op handlers (the 9 visible ops) ─────────────────────────
|
|
// Each re-faces the SAME proven soul :7770 /api/neuron/* routes the 87 aliases use,
|
|
// so Layer-1 works against live today. Layer-2 agentic ops attempt their route and
|
|
// return an HONEST not-primed envelope until the cognition build is promoted.
|
|
|
|
// Identity keystones — write-protected (self root + values hub).
|
|
fn is_identity_id(id: String) -> Bool {
|
|
return str_eq(id, "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee")
|
|
|| str_eq(id, "kn-5b606390-a52d-4ca2-8e0e-eba141d13440")
|
|
}
|
|
|
|
// has_prefix — true if s starts with p (no dependency on str_starts_with builtin).
|
|
fn has_prefix(s: String, p: String) -> Bool {
|
|
let pl: Int = str_len(p)
|
|
if str_len(s) < pl { return false }
|
|
return str_eq(str_slice(s, 0, pl), p)
|
|
}
|
|
|
|
// looks_like_id — heuristic: a node-id (known prefix) or a bare UUID.
|
|
fn looks_like_id(v: String) -> Bool {
|
|
if has_prefix(v, "kn-") { return true }
|
|
if has_prefix(v, "mem-") { return true }
|
|
if has_prefix(v, "mn-") { return true }
|
|
if has_prefix(v, "gn-") { return true }
|
|
if has_prefix(v, "bl-") { return true }
|
|
if has_prefix(v, "art-") { return true }
|
|
if has_prefix(v, "ctx-") { return true }
|
|
if has_prefix(v, "nt-") { return true }
|
|
if str_len(v) >= 32 && str_index_of(v, "-") > 0 && str_index_of(v, " ") < 0 { return true }
|
|
return false
|
|
}
|
|
|
|
fn is_named_root(v: String) -> Bool {
|
|
return str_eq(v, "self") || str_eq(v, "neuron") || str_eq(v, "values") || str_eq(v, "values_hub")
|
|
}
|
|
|
|
fn resolve_vantage_id(v: String) -> String {
|
|
if str_eq(v, "self") || str_eq(v, "neuron") { return "kn-efeb4a5b-5aff-4759-8a97-7233099be6ee" }
|
|
if str_eq(v, "values") || str_eq(v, "values_hub") { return "kn-5b606390-a52d-4ca2-8e0e-eba141d13440" }
|
|
return v
|
|
}
|
|
|
|
// aperture_k / aperture_depth — read the bound from top-level k/depth, else from a
|
|
// nested aperture:{k,depth} object, else the safe default.
|
|
fn aperture_k(args: String) -> Int {
|
|
let k: Int = json_get_int(args, "k")
|
|
let ap: String = json_get_raw(args, "aperture")
|
|
let ak: Int = if k > 0 { k } else { if str_eq(ap, "") { 0 } else { json_get_int(ap, "k") } }
|
|
return if ak > 0 { ak } else { 12 }
|
|
}
|
|
fn aperture_depth(args: String) -> Int {
|
|
let d: Int = json_get_int(args, "depth")
|
|
let ap: String = json_get_raw(args, "aperture")
|
|
let ad: Int = if d > 0 { d } else { if str_eq(ap, "") { 0 } else { json_get_int(ap, "depth") } }
|
|
return if ad > 0 { ad } else { 1 }
|
|
}
|
|
|
|
// agentic_result — pass the engram's real response through, verbatim.
|
|
//
|
|
// HISTORY (2026-08-15): this function used to inspect the response for ""/"not
|
|
// found"/"geometry unavailable"/"not registered" and, on any of them, return a
|
|
// confident "status":"pending-cognition-promotion" envelope claiming the
|
|
// cognition build had not been promoted yet. That diagnosis was FABRICATED — it
|
|
// never checked any promotion state. The real cause was that op_think and
|
|
// friends called the SOUL (neuron_url()) on paths the soul does not serve, so
|
|
// every call 404'd and got relabelled as a promotion gap. Cognition was live and
|
|
// answering on the engram the whole time (:8742/api/think returns a real 768-dim
|
|
// geometry). Multiple agents were sent down the wrong road by that message.
|
|
//
|
|
// Rule going forward: never invent a cause. Pass the real error through — an
|
|
// empty response or a 404 is reported as exactly that, so the next reader sees
|
|
// the actual failure instead of a reassuring story about it.
|
|
fn agentic_result(resp: String, op: String) -> String {
|
|
if str_eq(resp, "") {
|
|
return mcp_json_result("{\"ok\":false,\"op\":\"" + op + "\",\"error\":\"empty response from engram\",\"endpoint\":\"" + engram_url() + "\"}")
|
|
}
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
// cap_output — enforce the aperture at the WRAPPER boundary (where the MCP
|
|
// transport limit bites). The live soul's /graph does not yet honor compact/k
|
|
// (pending the api-bounding deploy), and the self/values hubs are pathological
|
|
// (~790KB). A k-scaled char cap guarantees the client never gets a whole-graph
|
|
// dump; the marker is honest about the truncation.
|
|
fn cap_output(resp: String, max_chars: Int) -> String {
|
|
if str_len(resp) <= max_chars { return resp }
|
|
return str_slice(resp, 0, max_chars) + " ...[aperture-truncated: narrow the vantage or lower k]"
|
|
}
|
|
|
|
// ── Layer 1 — geometry ops ────────────────────────────────────────────────────
|
|
|
|
fn op_read(args: String) -> String {
|
|
let vantage: String = json_get_string(args, "vantage")
|
|
if str_eq(vantage, "") {
|
|
return mcp_text_result("error: read requires 'vantage' — a node-id, a named root (self|neuron|values), or a concept string to search")
|
|
}
|
|
let typ: String = json_get_string(args, "type")
|
|
let k: Int = aperture_k(args)
|
|
let depth: Int = aperture_depth(args)
|
|
// node-id / named-root / explicit graph read → BOUNDED neighborhood (aperture caps output)
|
|
let want_graph: Bool = str_eq(typ, "edges") || str_eq(typ, "graph") || str_eq(typ, "node")
|
|
|| is_named_root(vantage) || looks_like_id(vantage)
|
|
if want_graph {
|
|
let id: String = resolve_vantage_id(vantage)
|
|
let resp: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=" + int_to_str(depth) + "&compact=1&snip=600&k=" + int_to_str(k))
|
|
// Aperture cap at the wrapper boundary: base + per-neighbor budget.
|
|
let cap: Int = 2000 + k * 3000
|
|
return mcp_json_result(cap_output(resp, cap))
|
|
}
|
|
// concept vantage → BOUNDED recall search (k = aperture = limit)
|
|
let resp: String = recall_or_list(vantage, k)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn op_write(args: String) -> String {
|
|
let content: String = pick_content(args)
|
|
if str_eq(content, "") { return mcp_text_result("error: write requires 'content'") }
|
|
let typ: String = json_get_string(args, "type")
|
|
if str_eq(typ, "self") || str_eq(typ, "values") {
|
|
return mcp_text_result("error: identity is write-protected -> intentional-cultivation only (keystones kn-efeb4a5b / kn-5b606390)")
|
|
}
|
|
if str_eq(typ, "knowledge") { return create_typed_node(args, "Knowledge", "0.75") }
|
|
if str_eq(typ, "artifact") { return create_node_typed(args, "Artifact", "Working") }
|
|
if str_eq(typ, "backlog") || str_eq(typ, "work") || str_eq(typ, "task") { return create_node_typed(args, "BacklogItem", "Working") }
|
|
if str_eq(typ, "process") { return create_typed_node(args, "Process", "0.80") }
|
|
if str_eq(typ, "state") { return create_typed_node(args, "InternalStateEvent", "0.60") }
|
|
return create_typed_node(args, "Memory", "0.60")
|
|
}
|
|
|
|
fn op_relate(args: String) -> String {
|
|
let from_a: String = json_get_string(args, "from")
|
|
let from_id: String = if str_eq(from_a, "") { json_get_string(args, "from_id") } else { from_a }
|
|
let to_a: String = json_get_string(args, "to")
|
|
let to_id: String = if str_eq(to_a, "") { json_get_string(args, "to_id") } else { to_a }
|
|
if str_eq(from_id, "") || str_eq(to_id, "") {
|
|
return mcp_text_result("error: relate requires 'from' and 'to' node-ids")
|
|
}
|
|
if is_identity_id(from_id) || is_identity_id(to_id) {
|
|
return mcp_text_result("error: identity keystone is write-protected")
|
|
}
|
|
let rel_a: String = json_get_string(args, "relationship")
|
|
let rel_b: String = if str_eq(rel_a, "") { json_get_string(args, "relation") } else { rel_a }
|
|
let rel: String = if str_eq(rel_b, "") { "associates" } else { rel_b }
|
|
let body: String = "{\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + rel + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/graph/link", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
fn op_supersede(args: String) -> String {
|
|
let id: String = pick_id(args)
|
|
if str_eq(id, "") { return mcp_text_result("error: supersede requires 'id'") }
|
|
if is_identity_id(id) { return mcp_text_result("error: identity keystone is write-protected") }
|
|
let action: String = json_get_string(args, "action")
|
|
if str_eq(action, "tombstone") {
|
|
let body: String = "{\"id\":\"" + id + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/memory/delete", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
if str_eq(action, "promote") {
|
|
return tool_promote_knowledge(args)
|
|
}
|
|
let typ: String = json_get_string(args, "type")
|
|
let nt: String = if str_eq(typ, "knowledge") { "Knowledge" } else { "Memory" }
|
|
return evolve_by_supersede(args, nt)
|
|
}
|
|
|
|
// ── Layer 2 — agentic primitives (LIVE on the engram, :8742) ──────────────────
|
|
// Each op maps to a real route in engram/src/server.el. Methods and parameter
|
|
// styles differ per route and are NOT uniform — they match the handlers exactly:
|
|
// think GET /api/think?seeds=&faculty= -> engram_think_json
|
|
// attend POST /api/attend {node,observer,salience} -> engram_attend_json
|
|
// assert GET /api/assert?claim=&for_whom=&floor= -> engram_assert_json
|
|
// ground POST /api/ground {claim,evidence,for_whom} -> engram_ground_json
|
|
// learn POST /api/correspondence-beat {seeds,faculty,keystone}
|
|
|
|
fn op_think(args: String) -> String {
|
|
let seeds: String = json_get_string(args, "seeds")
|
|
if str_eq(seeds, "") { return mcp_text_result("error: think requires 'seeds' (node-id anchors, comma-separated)") }
|
|
let f_raw: String = json_get_string(args, "faculty")
|
|
let f: String = if str_eq(f_raw, "") { "reason" } else { f_raw }
|
|
let resp: String = http_get(engram_url() + "/api/think?seeds=" + __url_encode(seeds) + "&faculty=" + __url_encode(f))
|
|
return agentic_result(resp, "think")
|
|
}
|
|
|
|
fn op_attend(args: String) -> String {
|
|
let node: String = json_get_string(args, "node")
|
|
if str_eq(node, "") { return mcp_text_result("error: attend requires 'node' (region node-id)") }
|
|
let observer: String = json_get_string(args, "observer")
|
|
let salience: String = json_get_string(args, "salience")
|
|
let body: String = "{" + auth_field() + "\"node\":\"" + node + "\",\"observer\":\"" + json_escape(observer) + "\",\"salience\":\"" + json_escape(salience) + "\"}"
|
|
let resp: String = http_post_json(engram_url() + "/api/attend", body)
|
|
return agentic_result(resp, "attend")
|
|
}
|
|
|
|
fn op_assert(args: String) -> String {
|
|
let claim: String = json_get_string(args, "claim")
|
|
if str_eq(claim, "") { return mcp_text_result("error: assert requires 'claim'") }
|
|
let for_whom: String = json_get_string(args, "for_whom")
|
|
let floor: String = json_get_string(args, "floor")
|
|
// GET with query params — engram's route_assert reads query_param(), not the body.
|
|
let resp: String = http_get(engram_url() + "/api/assert?claim=" + __url_encode(claim)
|
|
+ "&for_whom=" + __url_encode(for_whom)
|
|
+ "&floor=" + __url_encode(floor))
|
|
return agentic_result(resp, "assert")
|
|
}
|
|
|
|
fn op_ground(args: String) -> String {
|
|
let claim: String = json_get_string(args, "claim")
|
|
let evidence: String = json_get_string(args, "evidence")
|
|
if str_eq(claim, "") || str_eq(evidence, "") {
|
|
return mcp_text_result("error: ground requires 'claim' and 'evidence' (node-id regions)")
|
|
}
|
|
let for_whom: String = json_get_string(args, "for_whom")
|
|
let body: String = "{" + auth_field() + "\"claim\":\"" + claim + "\",\"evidence\":\"" + evidence + "\",\"for_whom\":\"" + json_escape(for_whom) + "\"}"
|
|
let resp: String = http_post_json(engram_url() + "/api/ground", body)
|
|
return agentic_result(resp, "ground")
|
|
}
|
|
|
|
fn op_learn(args: String) -> String {
|
|
let seeds: String = json_get_string(args, "seeds")
|
|
if str_eq(seeds, "") { return mcp_text_result("error: learn requires 'seeds'") }
|
|
let f_raw: String = json_get_string(args, "faculty")
|
|
let f: String = if str_eq(f_raw, "") { "induce" } else { f_raw }
|
|
let keystone: String = json_get_string(args, "keystone")
|
|
let body: String = "{" + auth_field() + "\"seeds\":\"" + seeds + "\",\"faculty\":\"" + f + "\",\"keystone\":\"" + json_escape(keystone) + "\"}"
|
|
// learn IS the correspondence-beat — that is the route's real name.
|
|
let resp: String = http_post_json(engram_url() + "/api/correspondence-beat", body)
|
|
return agentic_result(resp, "learn")
|
|
}
|
|
|
|
// ── Dispatcher ────────────────────────────────────────────────────────────────
|
|
|
|
fn dispatch_tool_call(tool_name: String, args: String) -> String {
|
|
|
|
// ── Per-turn background activation ──────────────────────────────────────
|
|
// Fire spread-activation on every semantic tool call so related nodes are
|
|
// warm before the tool runs. Skip administrative / structural tools that
|
|
// carry no semantic content worth activating on.
|
|
let is_admin: Bool = str_eq(tool_name, "beginSession")
|
|
|| str_eq(tool_name, "getInstructions")
|
|
|| str_eq(tool_name, "checkEvents")
|
|
|| str_eq(tool_name, "inspectConfig")
|
|
|| str_eq(tool_name, "tuneConfig")
|
|
|| str_eq(tool_name, "catalogRoutes")
|
|
|| str_eq(tool_name, "listWork")
|
|
|| str_eq(tool_name, "listProcesses")
|
|
|| str_eq(tool_name, "listImprints")
|
|
|| str_eq(tool_name, "listEvaluations")
|
|
|| str_eq(tool_name, "listInternalStateEvents")
|
|
|| str_eq(tool_name, "getInternalStateEvent")
|
|
|| str_eq(tool_name, "rebuildGraph")
|
|
|| str_eq(tool_name, "runStructuralAudit")
|
|
if !is_admin {
|
|
let seed: String = pick_activation_seed(tool_name, args)
|
|
let _act: String = fire_activation(seed)
|
|
}
|
|
|
|
// ── Collapsed surface — the 9 VISIBLE ops (the old 87 names below remain as HIDDEN ALIASES) ──
|
|
if str_eq(tool_name, "read") { return op_read(args) }
|
|
if str_eq(tool_name, "write") { return op_write(args) }
|
|
if str_eq(tool_name, "relate") { return op_relate(args) }
|
|
if str_eq(tool_name, "supersede") { return op_supersede(args) }
|
|
if str_eq(tool_name, "think") { return op_think(args) }
|
|
if str_eq(tool_name, "attend") { return op_attend(args) }
|
|
if str_eq(tool_name, "assert") { return op_assert(args) }
|
|
if str_eq(tool_name, "ground") { return op_ground(args) }
|
|
if str_eq(tool_name, "learn") { return op_learn(args) }
|
|
|
|
// ── Session + orchestration ─────────────────────────────────────────────
|
|
if str_eq(tool_name, "beginSession") { return tool_begin_session(args) }
|
|
if str_eq(tool_name, "getInstructions") { return tool_get_instructions(args) }
|
|
if str_eq(tool_name, "compileCtx") { return tool_compile_ctx(args) }
|
|
if str_eq(tool_name, "compileStep") { return create_typed_node(args, "Memory", "0.60") }
|
|
if str_eq(tool_name, "consolidate") { return tool_consolidate(args) }
|
|
if str_eq(tool_name, "projectContext") { return search_with_query(args, 50) }
|
|
|
|
// ── Memory ──────────────────────────────────────────────────────────────
|
|
if str_eq(tool_name, "remember") { return tool_remember(args) }
|
|
if str_eq(tool_name, "recall") { return tool_recall(args) }
|
|
if str_eq(tool_name, "inspectMemories") { return tool_inspect_memories(args) }
|
|
if str_eq(tool_name, "evolveMemory") { return evolve_by_supersede(args, "Memory") }
|
|
if str_eq(tool_name, "forget") { return tool_forget(args) }
|
|
if str_eq(tool_name, "pinNode") {
|
|
let id: String = pick_id(args)
|
|
if str_eq(id, "") { return mcp_text_result("error: node_id is required") }
|
|
// Wire a self-referential strengthen edge
|
|
let body: String = "{\"from_id\":\"" + id + "\",\"to_id\":\"" + id + "\",\"relation\":\"strengthened\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/graph/link", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
// ── Knowledge ───────────────────────────────────────────────────────────
|
|
if str_eq(tool_name, "searchKnowledge") { return tool_search_knowledge(args) }
|
|
if str_eq(tool_name, "retrieveKnowledge"){ return fetch_by_id(args) }
|
|
if str_eq(tool_name, "browseKnowledge") { return list_typed("Knowledge", 100, args) }
|
|
if str_eq(tool_name, "captureKnowledge") { return tool_capture_knowledge(args) }
|
|
if str_eq(tool_name, "evolveKnowledge") { return evolve_by_supersede(args, "Knowledge") }
|
|
if str_eq(tool_name, "promoteKnowledge") { return tool_promote_knowledge(args) }
|
|
if str_eq(tool_name, "removeKnowledge") { return delete_by_id(args) }
|
|
|
|
// ── Entities + graph ────────────────────────────────────────────────────
|
|
if str_eq(tool_name, "searchEntities") { return search_with_query(args, 20) }
|
|
if str_eq(tool_name, "inspectGraph") { return tool_inspect_graph(args) }
|
|
if str_eq(tool_name, "traverseGraph") { return tool_traverse_graph(args) }
|
|
if str_eq(tool_name, "searchGraph") { return search_with_query(args, 30) }
|
|
if str_eq(tool_name, "linkEntities") { return create_edge_typed(args, "associates") }
|
|
if str_eq(tool_name, "linkCausal") { return create_edge_typed(args, "causes") }
|
|
if str_eq(tool_name, "restructureCausalGraph") {
|
|
return tool_consolidate(args)
|
|
}
|
|
if str_eq(tool_name, "rebuildGraph") {
|
|
let resp: String = http_post_json(neuron_url() + "/consolidate", "{\"action\":\"reload\"}")
|
|
return mcp_json_result(resp)
|
|
}
|
|
if str_eq(tool_name, "runStructuralAudit") {
|
|
let resp: String = http_get(neuron_url() + "/session/begin")
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
// ── Backlog + work ──────────────────────────────────────────────────────
|
|
// planWork: create a REAL typed BacklogItem via /api/neuron/node/create (the old path fell through
|
|
// create_typed_node to a generic /memory write, dropping title/project/priority and never making a
|
|
// BacklogItem). reviewBacklog: LIST BacklogItem nodes (was a lexical /recall that never filtered by
|
|
// type). Both depend on the /api/neuron/list/<type> slice fix (neuron PR #58) to round-trip.
|
|
if str_eq(tool_name, "planWork") { return create_node_typed(args, "BacklogItem", "Working") }
|
|
if str_eq(tool_name, "reviewBacklog") { return list_typed("BacklogItem", 50, args) }
|
|
if str_eq(tool_name, "trackWork") { return evolve_by_supersede(args, "Memory") }
|
|
if str_eq(tool_name, "listWork") { return list_typed("WorkContext", 50, args) }
|
|
if str_eq(tool_name, "beginWork") { return create_typed_node(args, "Memory", "0.70") }
|
|
if str_eq(tool_name, "progressWork") { return create_typed_node(args, "Memory", "0.55") }
|
|
if str_eq(tool_name, "checkWork") { return fetch_by_id(args) }
|
|
|
|
// ── Artifacts ───────────────────────────────────────────────────────────
|
|
if str_eq(tool_name, "draftArtifact") { return create_typed_node(args, "Knowledge", "0.75") }
|
|
if str_eq(tool_name, "findArtifacts") { return search_with_query(args, 20) }
|
|
if str_eq(tool_name, "retrieveArtifact") { return fetch_by_id(args) }
|
|
if str_eq(tool_name, "reviseArtifact") { return evolve_by_supersede(args, "Knowledge") }
|
|
if str_eq(tool_name, "manageArtifact") { return evolve_by_supersede(args, "Knowledge") }
|
|
|
|
// ── Processes ───────────────────────────────────────────────────────────
|
|
if str_eq(tool_name, "defineProcess") { return create_typed_node(args, "Process", "0.80") }
|
|
if str_eq(tool_name, "listProcesses") { return list_typed("Process", 50, args) }
|
|
if str_eq(tool_name, "browseProcesses") {
|
|
let name: String = json_get_string(args, "name")
|
|
if str_eq(name, "") {
|
|
let resp: String = http_get(neuron_url() + "/processes")
|
|
return mcp_json_result(resp)
|
|
}
|
|
let body: String = "{\"name\":\"" + json_escape(name) + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/processes", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
if str_eq(tool_name, "retrieveProcess") { return fetch_by_id(args) }
|
|
if str_eq(tool_name, "executeProcess") { return create_typed_node(args, "Memory", "0.60") }
|
|
if str_eq(tool_name, "exportProcess") { return fetch_by_id(args) }
|
|
if str_eq(tool_name, "deleteProcess") { return delete_by_id(args) }
|
|
|
|
// ── Events / Axon ───────────────────────────────────────────────────────
|
|
if str_eq(tool_name, "checkEvents") { return tool_check_events(args) }
|
|
if str_eq(tool_name, "inspectEvent") { return fetch_by_id(args) }
|
|
if str_eq(tool_name, "acknowledgeEvent") {
|
|
let id: String = pick_id(args)
|
|
let resp: String = http_post_json(soul_url() + "/events/ack", "{\"id\":\"" + id + "\"}")
|
|
return mcp_json_result(resp)
|
|
}
|
|
if str_eq(tool_name, "processEvents") { return tool_check_events(args) }
|
|
if str_eq(tool_name, "sendNotification") {
|
|
let content: String = pick_content(args)
|
|
let _push: String = http_post_json(soul_url() + "/events/push", "{\"kind\":\"notification\",\"content\":\"" + json_escape(content) + "\"}")
|
|
let mem_body: String = "{\"content\":\"[notification] " + json_escape(content) + "\",\"importance\":\"normal\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/memory", mem_body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
// ── Config ──────────────────────────────────────────────────────────────
|
|
if str_eq(tool_name, "inspectConfig") { return tool_inspect_config(args) }
|
|
if str_eq(tool_name, "tuneConfig") {
|
|
let key: String = json_get_string(args, "key")
|
|
let value: String = json_get_string(args, "value")
|
|
if str_eq(key, "") { return mcp_text_result("error: key is required") }
|
|
let body: String = "{\"key\":\"" + json_escape(key) + "\",\"value\":\"" + json_escape(value) + "\"}"
|
|
let resp: String = http_post_json(neuron_url() + "/config/tune", body)
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
// ── Imprints ────────────────────────────────────────────────────────────
|
|
if str_eq(tool_name, "createImprint") { return create_typed_node(args, "Memory", "0.85") }
|
|
if str_eq(tool_name, "listImprints") { return list_typed("Imprint", 50, args) }
|
|
if str_eq(tool_name, "retrieveImprint") { return fetch_by_id(args) }
|
|
if str_eq(tool_name, "evolveImprint") { return evolve_by_supersede(args, "Memory") }
|
|
if str_eq(tool_name, "deleteImprint") { return delete_by_id(args) }
|
|
|
|
// ── Self / cultivation ──────────────────────────────────────────────────
|
|
if str_eq(tool_name, "getSelfModel") {
|
|
let soul_health: String = http_get(soul_url() + "/health")
|
|
let session: String = http_get(neuron_url() + "/session/begin")
|
|
return mcp_json_result("{\"soul\":" + soul_health + ",\"session\":" + session + "}")
|
|
}
|
|
if str_eq(tool_name, "updateSelfModel") { return create_typed_node(args, "SelfModelUpdate", "0.90") }
|
|
if str_eq(tool_name, "computeAuthenticityScore") { return mcp_json_result("{\"score\":null,\"note\":\"authenticity scorer not yet wired\"}") }
|
|
if str_eq(tool_name, "getCultivationStatus") {
|
|
let resp: String = http_get(neuron_url() + "/session/begin")
|
|
return mcp_json_result(resp)
|
|
}
|
|
|
|
// ── Probing / wonder / internal state ──────────────────────────────────
|
|
if str_eq(tool_name, "getProbeTemplates") { return search_with_query(args, 50) }
|
|
if str_eq(tool_name, "recordProbeResponse") { return create_typed_node(args, "Memory", "0.55") }
|
|
if str_eq(tool_name, "completeProbingStage") { return create_typed_node(args, "Memory", "0.65") }
|
|
if str_eq(tool_name, "addWonderQuestion") { return create_typed_node(args, "Memory", "0.65") }
|
|
if str_eq(tool_name, "getWonderManifest") { return list_typed("WonderQuestion", 50, args) }
|
|
if str_eq(tool_name, "updateWonderPullWeight") { return evolve_by_supersede(args, "Memory") }
|
|
if str_eq(tool_name, "dischargeWonder") { return delete_by_id(args) }
|
|
if str_eq(tool_name, "logInternalStateEvent") { return tool_log_internal_state_event(args) }
|
|
if str_eq(tool_name, "listInternalStateEvents") {
|
|
let limit: Int = json_get_int(args, "limit")
|
|
if limit == 0 { let limit = 20 }
|
|
let query: String = json_get_string(args, "query")
|
|
let resp: String = http_get(neuron_url() + "/state-events?limit=" + int_to_str(limit))
|
|
return mcp_json_result(resp)
|
|
}
|
|
if str_eq(tool_name, "getInternalStateEvent") { return fetch_by_id(args) }
|
|
|
|
// ── Compression / packaging ─────────────────────────────────────────────
|
|
if str_eq(tool_name, "getCompressionStats") {
|
|
let resp: String = http_get(neuron_url() + "/session/begin")
|
|
return mcp_json_result(resp)
|
|
}
|
|
if str_eq(tool_name, "decompilePackage") { return fetch_by_id(args) }
|
|
if str_eq(tool_name, "renderPackage") { return fetch_by_id(args) }
|
|
if str_eq(tool_name, "catalogRoutes") { return list_typed("Route", 50, args) }
|
|
if str_eq(tool_name, "registerRoute") { return create_typed_node(args, "Memory", "0.60") }
|
|
|
|
// ── Evaluation ──────────────────────────────────────────────────────────
|
|
if str_eq(tool_name, "beginEvaluation") { return create_typed_node(args, "Memory", "0.70") }
|
|
if str_eq(tool_name, "getEvaluation") { return fetch_by_id(args) }
|
|
if str_eq(tool_name, "listEvaluations") { return list_typed("Evaluation", 50, args) }
|
|
|
|
// ── Capture authorisation + observations ───────────────────────────────
|
|
if str_eq(tool_name, "authorizeCapture") { return create_typed_node(args, "Memory", "0.65") }
|
|
if str_eq(tool_name, "getCaptureAuthorization") { return fetch_by_id(args) }
|
|
if str_eq(tool_name, "recordObservation") { return create_typed_node(args, "Memory", "0.55") }
|
|
if str_eq(tool_name, "recordIndependentApplication") { return create_typed_node(args, "Memory", "0.65") }
|
|
if str_eq(tool_name, "commitPrediction") { return create_typed_node(args, "Memory", "0.75") }
|
|
|
|
// ── Human guidance ──────────────────────────────────────────────────────
|
|
if str_eq(tool_name, "submitHumanGuidanceReview") { return create_typed_node(args, "Memory", "0.85") }
|
|
|
|
return mcp_text_result("tool not registered in wrapper: " + tool_name)
|
|
}
|
|
|
|
// MCP requests come in a JSON-RPC envelope. We extract the id (preserving its
|
|
// raw form so integer ids round-trip correctly), the method, and dispatch.
|
|
fn handle_jsonrpc(body: String) -> String {
|
|
let id_raw: String = json_get_raw(body, "id")
|
|
let method: String = json_get_string(body, "method")
|
|
|
|
if str_eq(method, "initialize") {
|
|
let result: String = "{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{}},\"serverInfo\":{\"name\":\"neuron-mcp-wrapper\",\"version\":\"0.2.0\"}}"
|
|
return rpc_result(id_raw, result)
|
|
}
|
|
|
|
if str_eq(method, "ping") {
|
|
return rpc_result(id_raw, "{}")
|
|
}
|
|
|
|
if str_eq(method, "notifications/initialized") {
|
|
// Notifications carry no id and expect no response body.
|
|
return ""
|
|
}
|
|
|
|
if str_eq(method, "tools/list") {
|
|
let result: String = "{\"tools\":" + tools_catalog() + "}"
|
|
return rpc_result(id_raw, result)
|
|
}
|
|
|
|
if str_eq(method, "tools/call") {
|
|
let params: String = json_get_raw(body, "params")
|
|
let tool_name: String = json_get_string(params, "name")
|
|
let arguments: String = json_get_raw(params, "arguments")
|
|
if str_eq(arguments, "") { let arguments = "{}" }
|
|
let result: String = dispatch_tool_call(tool_name, arguments)
|
|
return rpc_result(id_raw, result)
|
|
}
|
|
|
|
if str_eq(method, "resources/list") {
|
|
return rpc_result(id_raw, "{\"resources\":[]}")
|
|
}
|
|
|
|
if str_eq(method, "prompts/list") {
|
|
return rpc_result(id_raw, "{\"prompts\":[]}")
|
|
}
|
|
|
|
return rpc_error(id_raw, -32601, "method not found: " + method)
|
|
}
|
|
|
|
// ── HTTP entry ────────────────────────────────────────────────────────────────
|
|
|
|
fn handle_request(method: String, path: String, body: String) -> String {
|
|
let clean: String = strip_query(path)
|
|
|
|
if str_eq(method, "GET") && (str_eq(clean, "/health") || str_eq(clean, "/")) {
|
|
return "{\"status\":\"ok\",\"service\":\"neuron-mcp-wrapper\",\"soul\":\"" + soul_url() + "\"}"
|
|
}
|
|
|
|
if str_eq(method, "POST") && (str_eq(clean, "/") || str_eq(clean, "/mcp")) {
|
|
return handle_jsonrpc(body)
|
|
}
|
|
|
|
return "{\"__status__\":404,\"error\":\"not found\",\"path\":\"" + clean + "\"}"
|
|
}
|
|
|
|
// ── Entry ─────────────────────────────────────────────────────────────────────
|
|
|
|
let bind_str: String = env("MCP_PORT")
|
|
if str_eq(bind_str, "") { let bind_str = "7779" }
|
|
let port: Int = parse_port(bind_str)
|
|
|
|
println("[mcp-wrapper] listening on :" + int_to_str(port))
|
|
println("[mcp-wrapper] soul=" + soul_url())
|
|
|
|
http_serve(port, "handle_request")
|