Skip to content

Commit b01b218

Browse files
author
Joseph Ibrahim
committed
scout live-wire (mcp_server dispatch + CLAUDE.md verify-before-write rule)
1 parent 45b18d5 commit b01b218

2 files changed

Lines changed: 94 additions & 0 deletions

File tree

‎CLAUDE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ Track across the session:
581581
12. **Never trust LLM boundary flags** — blast radius inferred from dependency graph (R7)
582582
13. **PDG cooks don't block FastMCP** — `pdg.PyEventHandler` bridge keeps server responsive (R8)
583583
14. **Evolved memory syncs viewport** — LOP nodes referencing USD force-cooked via LopNetwork walk (R10)
584+
15. **Verify before you write an unfamiliar API** — before emitting any `hou.*` / `pdg.*` / `pxr.*` call or VEX function you are not certain exists in H21.0.671, call the `synapse_scout` tool first. It returns real reference snippets AND, per dotted symbol in the query, whether it exists verbatim in the canonical corpus (`found_in_corpus=false` ⇒ phantom — do **not** emit it). This is the runtime form of the "`dir()` is a hard gate" rule and the front-line defense against SYNAPSE's #1 failure class (phantom APIs: `hou.pdg.*`, `hou.secure`, `hou.lopNetworks()`, `hou.updateGraphTick()`). Scout reads the repo `rag/` corpus (materialized lazily on first call), not the thin `G:\` store. Pure-Python, zero-`hou`; wired in `mcp_server.py` via the cognitive Dispatcher. CRUCIBLE still verifies after the fact — scout shrinks how often it has to.
584585

585586
---
586587

‎mcp_server.py‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,86 @@ async def _inspector_call_tool(arguments: dict) -> list:
663663
return [TextContent(type="text", text=_dumps_str(result))]
664664

665665

666+
# ---------------------------------------------------------------------------
667+
# Scout tool -- federated H21 docs-RAG + VEX retrieval, pure-Python, zero-hou.
668+
#
669+
# Like the Inspector (above) this routes through the cognitive Dispatcher, but
670+
# it needs NO Houdini round-trip: scout reads a LOCAL corpus and grounds dotted
671+
# API symbols against it. On first use we materialize that corpus from the
672+
# CANONICAL repo rag/ tree (build-if-absent) and bind scout to it, so the live
673+
# tool never serves an empty store nor the thin G:\ SideFXLabs-only one.
674+
# ---------------------------------------------------------------------------
675+
from pathlib import Path as _Path
676+
from synapse.cognitive.tools.scout import (
677+
synapse_scout as _scout_tool,
678+
SYNAPSE_SCOUT_SCHEMA as _SCOUT_SCHEMA,
679+
)
680+
from synapse.cognitive.tools import scout as _scout_module
681+
from synapse.cognitive.tools import scout_ingest as _scout_ingest
682+
683+
_SCOUT_TOOL_NAME = _SCOUT_SCHEMA["name"] # "synapse_scout"
684+
_SCOUT_TOOL_DESC = _SCOUT_SCHEMA["description"]
685+
_SCOUT_TOOL_SCHEMA = _SCOUT_SCHEMA["input_schema"]
686+
687+
_scout_dispatcher: _Dispatcher | None = None
688+
689+
690+
def _get_scout_dispatcher() -> _Dispatcher:
691+
"""Build the scout Dispatcher once. On first build, materialize the scout
692+
corpus from the canonical repo rag/ (build-if-absent) and point scout at
693+
that store so it never goes live empty and never reads the thin G:\\ one."""
694+
global _scout_dispatcher
695+
if _scout_dispatcher is not None:
696+
return _scout_dispatcher
697+
try:
698+
info = _scout_ingest.ensure_corpus()
699+
store_root = _Path(info["store_root"])
700+
_scout_module.RAG_ROOT = store_root
701+
_scout_module.VEX_ROOT = store_root
702+
# Drop caches keyed on any previous root (defensive; usually fresh proc).
703+
for _cache in (_scout_module._CORPUS, _scout_module._FTS,
704+
_scout_module._DENSE, _scout_module._SYMS):
705+
_cache.clear()
706+
except Exception:
707+
logger.exception(
708+
"scout corpus materialization failed; synapse_scout will surface "
709+
"a ScoutError on call rather than serving an empty store"
710+
)
711+
_scout_dispatcher = _Dispatcher(
712+
is_testing=True,
713+
tools={_SCOUT_TOOL_NAME: _scout_tool},
714+
schemas={_SCOUT_TOOL_NAME: _SCOUT_SCHEMA},
715+
)
716+
return _scout_dispatcher
717+
718+
719+
async def _scout_call_tool(arguments: dict) -> list:
720+
"""Handle synapse_scout via the cognitive Dispatcher. Pure-Python, so it
721+
runs off the event loop via asyncio.to_thread; the Dispatcher wraps any
722+
ScoutError into the AgentToolError envelope (never raises)."""
723+
kwargs: dict = {"query": arguments.get("query", "")}
724+
for opt in ("domain", "k", "max_chars", "where"):
725+
if arguments.get(opt) is not None:
726+
kwargs[opt] = arguments[opt]
727+
try:
728+
dispatcher = _get_scout_dispatcher()
729+
result = await asyncio.to_thread(
730+
dispatcher.execute, _SCOUT_TOOL_NAME, kwargs,
731+
)
732+
except Exception as e:
733+
logger.exception("Unexpected error dispatching synapse_scout")
734+
return [TextContent(type="text", text=_dumps_str({
735+
"error": type(e).__name__, "message": str(e),
736+
}))]
737+
738+
if isinstance(result, _AgentToolError):
739+
return [TextContent(type="text", text=_dumps_str({
740+
"error": result.error_type, "message": result.error_message,
741+
}))]
742+
743+
return [TextContent(type="text", text=_dumps_str(result))]
744+
745+
666746
@server.list_tools()
667747
async def list_tools():
668748
"""Register all Synapse MCP tools.
@@ -697,6 +777,15 @@ async def list_tools():
697777
inputSchema=_INSPECTOR_TOOL_SCHEMA,
698778
))
699779

780+
# Scout tool -- local federated docs-RAG + VEX retrieval with phantom-API
781+
# grounding. Dispatched via a dedicated branch in call_tool() (pure-Python,
782+
# no Houdini round-trip).
783+
tools.append(Tool(
784+
name=_SCOUT_TOOL_NAME,
785+
description=_SCOUT_TOOL_DESC,
786+
inputSchema=_SCOUT_TOOL_SCHEMA,
787+
))
788+
700789
return tools
701790

702791

@@ -723,6 +812,10 @@ async def call_tool(name: str, arguments: dict):
723812
if name == _INSPECTOR_TOOL_NAME:
724813
return await _inspector_call_tool(arguments)
725814

815+
# Scout tool -- pure-Python federated retrieval + phantom-API grounding.
816+
if name == _SCOUT_TOOL_NAME:
817+
return await _scout_call_tool(arguments)
818+
726819
if name not in TOOL_DISPATCH:
727820
return [TextContent(type="text", text=f"I don't recognize the tool '{name}' \u2014 check the available tools list")]
728821

0 commit comments

Comments
 (0)