Files
DemonGatanjieu-Anomalous_Mo…/api/mcp_directory.py
T
DemonGatanjieuandClaude Opus 5.5 46eca1b594 Run a picture's own workflow, keep a task on one page, read a big canvas whole
- run_workflow takes a gallery picture as its workflow (mcp_fill.opening
  says what a name opens, for open_workflow too); a picture's workflow
  opens with an id of its own, and the tab's mark goes into the workflow
  before it loads, so ComfyUI finishing the tab does not drop it.
- With ComfyUI open in two browser tabs a task's actions were split
  between them (one opened the task's tab, the other could not find it):
  the page that did the last action gets the next first now, another
  only when it is gone (mcp_bridge.py, pages keep an id of their own).
- describe_canvas on a big canvas: titles off, then nodes as a line each
  with their settings, so every value stays readable.
- search_models names the models that did not fit; begin_task's tool
  directory goes by name alone when the answer would be too long.
- ask_user refuses a "test" question with made-up options too.
- Exam round recorded: question 2 with nothing filed away, a big canvas
  read right in 3 calls, question 3 in 12 calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 01:16:17 +08:00

118 lines
5.3 KiB
Python

"""A directory of the tools by what an AI wants to do, so it goes straight to the right one, as
ARCHITECTURE.md does for the code: each tool under its stage of the work (action_catalog.TOOLS),
with its title, in the catalog's order. It is built from the registry, so a new tool shows up by
itself (tests/test_mcp_directory.py checks that every stage has a heading). mcp_server.py puts it
into begin_task's answer, and into get_situation's while there is no task yet.
AI apps keep the tool list they fetched (Antigravity files it away and fetches it again only when
it reconnects), so after Anomalous is updated an app can go on with the old one: new tools missing,
gone ones still called. listed() keeps which list each app fetched last (a fingerprint, in
anomalous/ai_tool_lists.json); a call from an app whose list is not today's tells the AI, and notes
it on the task (task["stale_tools"]) so the workbench tells the user how to reconnect, until the app
fetches the list again.
"""
import hashlib
import json
import os
import time
from . import action_catalog, ai_tasks, mcp_fit
from .mcp_tools import retext
from .path_utils import atomic_write_json
SECTIONS = ( # stage -> heading, in the order work usually goes
("run", "Run a workflow"), ("canvas", "The workflow on the canvas"), ("model", "Models"), ("prompt", "Prompts"),
("combo", "Combos"), ("picture", "Pictures"), ("voice", "Voices"), ("library", "The library"),
(None, "The task and the user"),
)
NOTE = "The tools by what you want to do: go straight to the one you need instead of looking around first."
STALE_NOTE = ("Your app holds an older list of Anomalous's tools (Anomalous was updated since it fetched it): some may "
"be missing or gone. The call went through, but tell the user in report to reconnect the app to Anomalous "
"(Antigravity: restart it, or switch the anomalous MCP server off and on) and to go on in a new chat.")
_lists = None # app -> fingerprint of the tool list it fetched last
def directory(specs, titles=True):
"""{heading: "tool (title), ..."} for the tools of `specs` {name: spec}; the names alone without `titles`."""
sections = {}
for name, entry in action_catalog.TOOLS.items():
if name in specs:
sections.setdefault(entry.stage, []).append(f"{name} ({specs[name].get('title') or name})" if titles else name)
return {"note": NOTE, **{heading: ", ".join(sections[stage]) for stage, heading in SECTIONS if stage in sections}}
def attach(result, name, specs):
"""Puts the directory into begin_task's answer, and into get_situation's while there is no task."""
data = result.get("structuredContent") if isinstance(result, dict) and not result.get("isError") else None
if not isinstance(data, dict) or not (name == "begin_task" or (name == "get_situation" and not data.get("task"))):
return result
answer = {**data, "directory": directory(specs)}
if mcp_fit.size(answer) > mcp_fit.BUDGET: # a long message of the user's with it: the tools by name, to stay readable
answer["directory"] = directory(specs, titles=False)
retext(result, answer)
return result
def fingerprint(tools):
return hashlib.sha256(json.dumps(tools, sort_keys=True, ensure_ascii=False).encode("utf-8")).hexdigest()[:16]
def _path():
return os.path.join(os.path.dirname(ai_tasks.tasks_dir()), "ai_tool_lists.json")
def _known():
global _lists
if _lists is None:
try:
with open(_path(), encoding="utf-8") as source:
stored = json.load(source)
except (OSError, ValueError):
stored = {}
_lists = stored if isinstance(stored, dict) else {}
return _lists
def listed(client, tools):
"""The app `client` fetched `tools`: kept, and the notes on its tasks go."""
with ai_tasks._lock:
known = _known()
if known.get(client) != fingerprint(tools):
known[client] = fingerprint(tools)
atomic_write_json(_path(), known)
changed = [task for task in ai_tasks._load().values() if task.get("stale_tools") and task.get("client") == client]
for task in changed:
task.pop("stale_tools")
ai_tasks._save(task)
for task in changed:
ai_tasks._notify(task["id"])
def stale(client, tools):
"""Whether the app `client` fetched another list than `tools` last (one never seen fetching: no)."""
with ai_tasks._lock:
known = _known().get(client)
return bool(known) and known != fingerprint(tools)
def tell_stale(result, task_id):
"""Says so in the call's answer, and notes it on the task for the workbench."""
if isinstance(result, dict):
data = result.get("structuredContent")
first = (result.get("content") or [None])[0]
if isinstance(data, dict) and not result.get("isError"):
retext(result, {**data, "tool_list": STALE_NOTE})
elif isinstance(first, dict) and first.get("type") == "text":
result["content"][0] = {**first, "text": f"{first.get('text', '')} {STALE_NOTE}"}
if not task_id:
return result
with ai_tasks._lock:
task = ai_tasks._load().get(task_id)
if task is None or task.get("stale_tools"):
return result
task["stale_tools"] = time.time()
ai_tasks._save(task)
ai_tasks._notify(task_id)
return result