forked from Rockachopa/Timmy-time-dashboard
235 lines
7.5 KiB
Python
235 lines
7.5 KiB
Python
"""Register OpenFang Hands as MCP tools in Timmy's tool registry.
|
|
|
|
Each OpenFang Hand becomes a callable MCP tool that personas can use
|
|
during task execution. The mapping ensures the right personas get
|
|
access to the right hands:
|
|
|
|
Mace (Security) → collector (OSINT), browser
|
|
Seer (Analytics) → predictor, researcher
|
|
Echo (Research) → researcher, browser, collector
|
|
Helm (DevOps) → browser
|
|
Lead hand → available to all personas via direct request
|
|
|
|
Call ``register_openfang_tools()`` during app startup (after config
|
|
is loaded) to populate the tool registry.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from infrastructure.openfang.client import OPENFANG_HANDS, openfang_client
|
|
|
|
try:
|
|
from mcp.schemas.base import create_tool_schema
|
|
except ImportError:
|
|
|
|
def create_tool_schema(**kwargs):
|
|
return kwargs
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ── Tool schemas ─────────────────────────────────────────────────────────────
|
|
|
|
_HAND_SCHEMAS: dict[str, dict] = {
|
|
"browser": create_tool_schema(
|
|
name="openfang_browser",
|
|
description=(
|
|
"Web automation via OpenFang's Browser hand. "
|
|
"Navigates URLs, extracts content, fills forms. "
|
|
"Includes mandatory purchase confirmation gates."
|
|
),
|
|
parameters={
|
|
"url": {"type": "string", "description": "URL to navigate to"},
|
|
"instruction": {
|
|
"type": "string",
|
|
"description": "What to do on the page",
|
|
},
|
|
},
|
|
required=["url"],
|
|
),
|
|
"collector": create_tool_schema(
|
|
name="openfang_collector",
|
|
description=(
|
|
"OSINT intelligence and continuous monitoring via OpenFang's "
|
|
"Collector hand. Gathers public information on targets."
|
|
),
|
|
parameters={
|
|
"target": {
|
|
"type": "string",
|
|
"description": "Target to investigate (domain, org, person)",
|
|
},
|
|
"depth": {
|
|
"type": "string",
|
|
"description": "Collection depth: shallow | standard | deep",
|
|
"default": "shallow",
|
|
},
|
|
},
|
|
required=["target"],
|
|
),
|
|
"predictor": create_tool_schema(
|
|
name="openfang_predictor",
|
|
description=(
|
|
"Superforecasting with calibrated reasoning via OpenFang's "
|
|
"Predictor hand. Produces probability estimates with reasoning."
|
|
),
|
|
parameters={
|
|
"question": {
|
|
"type": "string",
|
|
"description": "Forecasting question to evaluate",
|
|
},
|
|
"horizon": {
|
|
"type": "string",
|
|
"description": "Time horizon: 1d | 1w | 1m | 3m | 1y",
|
|
"default": "1w",
|
|
},
|
|
},
|
|
required=["question"],
|
|
),
|
|
"lead": create_tool_schema(
|
|
name="openfang_lead",
|
|
description=(
|
|
"Prospect discovery and ICP-based qualification via OpenFang's "
|
|
"Lead hand. Finds and scores potential leads."
|
|
),
|
|
parameters={
|
|
"icp": {
|
|
"type": "string",
|
|
"description": "Ideal Customer Profile description",
|
|
},
|
|
"max_results": {
|
|
"type": "integer",
|
|
"description": "Maximum leads to return",
|
|
"default": 10,
|
|
},
|
|
},
|
|
required=["icp"],
|
|
),
|
|
"twitter": create_tool_schema(
|
|
name="openfang_twitter",
|
|
description=(
|
|
"Social account management via OpenFang's Twitter hand. "
|
|
"Includes approval gates for sensitive actions."
|
|
),
|
|
parameters={
|
|
"action": {
|
|
"type": "string",
|
|
"description": "Action: post | reply | search | analyze",
|
|
},
|
|
"content": {
|
|
"type": "string",
|
|
"description": "Content for the action",
|
|
},
|
|
},
|
|
required=["action", "content"],
|
|
),
|
|
"researcher": create_tool_schema(
|
|
name="openfang_researcher",
|
|
description=(
|
|
"Deep autonomous research with source verification via "
|
|
"OpenFang's Researcher hand. Produces cited reports."
|
|
),
|
|
parameters={
|
|
"topic": {
|
|
"type": "string",
|
|
"description": "Research topic or question",
|
|
},
|
|
"depth": {
|
|
"type": "string",
|
|
"description": "Research depth: quick | standard | deep",
|
|
"default": "standard",
|
|
},
|
|
},
|
|
required=["topic"],
|
|
),
|
|
"clip": create_tool_schema(
|
|
name="openfang_clip",
|
|
description=(
|
|
"Video processing and social media publishing via OpenFang's "
|
|
"Clip hand. Edits, captions, and publishes video content."
|
|
),
|
|
parameters={
|
|
"source": {
|
|
"type": "string",
|
|
"description": "Source video path or URL",
|
|
},
|
|
"instruction": {
|
|
"type": "string",
|
|
"description": "What to do with the video",
|
|
},
|
|
},
|
|
required=["source"],
|
|
),
|
|
}
|
|
|
|
# Map personas to the OpenFang hands they should have access to
|
|
PERSONA_HAND_MAP: dict[str, list[str]] = {
|
|
"echo": ["researcher", "browser", "collector"],
|
|
"seer": ["predictor", "researcher"],
|
|
"mace": ["collector", "browser"],
|
|
"helm": ["browser"],
|
|
"forge": ["browser", "researcher"],
|
|
"quill": ["researcher"],
|
|
"pixel": ["clip", "browser"],
|
|
"lyra": [],
|
|
"reel": ["clip"],
|
|
}
|
|
|
|
|
|
def _make_hand_handler(hand_name: str):
|
|
"""Create an async handler that delegates to the OpenFang client."""
|
|
|
|
async def handler(**kwargs: Any) -> str:
|
|
result = await openfang_client.execute_hand(hand_name, kwargs)
|
|
if result.success:
|
|
return result.output
|
|
return f"[OpenFang {hand_name} error] {result.error}"
|
|
|
|
handler.__name__ = f"openfang_{hand_name}"
|
|
handler.__doc__ = _HAND_SCHEMAS.get(hand_name, {}).get(
|
|
"description", f"OpenFang {hand_name} hand"
|
|
)
|
|
return handler
|
|
|
|
|
|
def register_openfang_tools() -> int:
|
|
"""Register all OpenFang Hands as MCP tools.
|
|
|
|
Returns the number of tools registered.
|
|
"""
|
|
try:
|
|
from mcp.registry import tool_registry
|
|
except ImportError:
|
|
logger.warning("MCP registry not available — skipping OpenFang tool registration")
|
|
return 0
|
|
|
|
count = 0
|
|
for hand_name in OPENFANG_HANDS:
|
|
schema = _HAND_SCHEMAS.get(hand_name)
|
|
if not schema:
|
|
logger.warning("No schema for OpenFang hand: %s", hand_name)
|
|
continue
|
|
|
|
tool_name = f"openfang_{hand_name}"
|
|
handler = _make_hand_handler(hand_name)
|
|
|
|
tool_registry.register(
|
|
name=tool_name,
|
|
schema=schema,
|
|
handler=handler,
|
|
category="openfang",
|
|
tags=["openfang", hand_name, "vendor"],
|
|
source_module="infrastructure.openfang.tools",
|
|
requires_confirmation=(hand_name in ("twitter",)),
|
|
)
|
|
count += 1
|
|
|
|
logger.info("Registered %d OpenFang tools in MCP registry", count)
|
|
return count
|
|
|
|
|
|
def get_hands_for_persona(persona_id: str) -> list[str]:
|
|
"""Return the OpenFang tool names available to a persona."""
|
|
hand_names = PERSONA_HAND_MAP.get(persona_id, [])
|
|
return [f"openfang_{h}" for h in hand_names]
|