Files
DemonGatanjieu-Anomalous_Mo…/api/mcp_directory.py
T
DemonGatanjieuandClaude Opus 5.5 59916d1ea3 Offer an on-demand tool set: a few tools, and the directory for the rest
A third choice for "Tools for the AI" in the Gates window. With "On demand",
AI apps are offered only action_catalog.CORE plus two tools of
mcp_directory.py:
- CORE: begin_task, get_situation, run_workflow, run_status, report, ask_user,
  get_answers.
- tool_help: the description and arguments of up to three tools named in the
  directory.
- use_tool: calls any tool of the directory.

tools/list drops to 11 tools and about 9,000 characters (daily 22,200, full
40,700). With this set the directory begin_task brings names every tool and
says how to reach the ones not listed.

use_tool has no run of its own. mcp_server._call makes the call the tool it
names before anything else, so the user's gates, the timeline's step, the run
watch and the answer are all that tool's; the timeline never shows use_tool.
An unknown name, the card's own tools or wrong arguments come back as tool
errors that point to tool_help.

The default stays Full. Switching back to Daily or Full in the Gates window
undoes this without a revert, and this commit stands alone if it has to go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 16:11:41 +08:00

102 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 offered 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.
With the "lazy" tool set (ai_gates.tool_set) AI apps are offered only action_catalog.CORE and
two tools of this module: tool_help gives the description and arguments of tools named in the
directory, use_tool calls one. use_tool has no run of its own: mcp_server.py makes the call the
tool it names before anything else, so gates, steps and answers are that tool's.
"""
from . import action_catalog
from .mcp_tools import ToolError, _schema, retext
DIRECTORY_TOOLS = ("tool_help", "use_tool")
MAX_HELP = 3 # tool specs in one answer: three stay under what Antigravity files away
LAZY_NOTE = " Not in your tool list: tool_help(names) gives its arguments, use_tool(name, arguments) calls it."
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."
def directory(specs, lazy=False):
"""{heading: "tool (title), ..."} for the tools offered, `specs` {name: spec}; `lazy`: most are
reached through use_tool."""
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})")
return {"note": NOTE + (LAZY_NOTE if lazy else ""),
**{heading: ", ".join(sections[stage]) for stage, heading in SECTIONS if stage in sections}}
def attach(result, name, specs, lazy=False):
"""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
retext(result, {**data, "directory": directory(specs, lazy)})
return result
def help_of(tools, names):
"""tool_help: what tools `names` do and take, from the server's `tools` {name: tool}."""
if not isinstance(names, list) or not 0 < len(names) <= MAX_HELP:
raise ToolError(f"names lists 1 to {MAX_HELP} tool names from the directory.")
found, missing = [], []
for name in names:
tool = tools.get(name) if isinstance(name, str) and name not in DIRECTORY_TOOLS else None
if tool:
schema = tool["spec"]["inputSchema"]
found.append({"name": name, "description": tool["spec"].get("description", ""),
"arguments": schema.get("properties", {}), "required": schema.get("required", [])})
else:
missing.append(str(name)[:60])
return {"tools": found, **({"not_found": missing} if missing else {}),
"next": "use_tool(name, arguments) calls one, with arguments as listed."}
def refused(text):
"""A use_tool call that names no tool it can make."""
return {"content": [{"type": "text", "text": text}], "isError": True}
def hinted(result, name):
"""A failed use_tool call, with where its arguments are told."""
if isinstance(result, dict) and result.get("isError") and result.get("content"):
first = result["content"][0]
result["content"][0] = {**first, "text": f"{first.get('text', '')} tool_help(names=[\"{name}\"]) shows its arguments."}
return result
def directory_tools(registry):
"""tool_help and use_tool, `registry()` giving the server's tools."""
def tool_help(names):
return help_of(registry(), names)
return {
"tool_help": {"run": tool_help, "spec": {
"name": "tool_help", "title": "What a tool takes",
"description": f"The description and arguments of up to {MAX_HELP} tools named in the directory, to call "
"them with use_tool.",
"inputSchema": _schema({"names": {"type": "array", "items": {"type": "string"}, "maxItems": MAX_HELP}}, ["names"]),
"annotations": {"title": "What a tool takes", "readOnlyHint": True, "destructiveHint": False,
"idempotentHint": True, "openWorldHint": False},
}},
"use_tool": {"spec": {
"name": "use_tool", "title": "Use a tool",
"description": "Calls a tool of the directory that is not in your tool list: name, and its arguments as "
"tool_help gives them. It is that tool's call in every way (its answer, the user's gates).",
"inputSchema": _schema({"name": {"type": "string"}, "arguments": {"type": "object"}}, ["name"]),
"annotations": {"title": "Use a tool", "readOnlyHint": False, "destructiveHint": True,
"idempotentHint": False, "openWorldHint": False},
}},
}