forked from Rockachopa/Timmy-time-dashboard
Implement MCP system, Event Bus, and Sub-Agents
## 1. MCP (Model Context Protocol) Implementation ### Registry (src/mcp/registry.py) - Tool registration with JSON schemas - Dynamic tool discovery - Health tracking per tool - Metrics collection (latency, error rates) - @register_tool decorator for easy registration ### Server (src/mcp/server.py) - MCPServer class implementing MCP protocol - MCPHTTPServer for FastAPI integration - Standard endpoints: list_tools, call_tool, get_schema ### Schemas (src/mcp/schemas/base.py) - create_tool_schema() helper - Common parameter types - Standard return types ### Bootstrap (src/mcp/bootstrap.py) - Automatic tool module loading - Status reporting ## 2. MCP-Compliant Tools (src/tools/) | Tool | Purpose | Category | |------|---------|----------| | web_search | DuckDuckGo search | research | | read_file | File reading | files | | write_file | File writing (confirmation) | files | | list_directory | Directory listing | files | | python | Python code execution | code | | memory_search | Vector memory search | memory | All tools have proper schemas, error handling, and MCP registration. ## 3. Event Bus (src/events/bus.py) - Async publish/subscribe pattern - Pattern matching with wildcards (agent.task.*) - Event history tracking - Concurrent handler execution - Module-level singleton for system-wide use ## 4. Sub-Agents (src/agents/) All agents inherit from BaseAgent with: - Agno Agent integration - MCP tool registry access - Event bus connectivity - Structured logging ### Agent Roster | Agent | Role | Tools | Purpose | |-------|------|-------|---------| | Seer | Research | web_search, read_file, memory_search | Information gathering | | Forge | Code | python, write_file, read_file | Code generation | | Quill | Writing | write_file, read_file, memory_search | Content creation | | Echo | Memory | memory_search, read_file, write_file | Context retrieval | | Helm | Routing | memory_search | Task routing decisions | | Timmy | Orchestrator | All tools | Coordination & user interface | ### Timmy Orchestrator - Analyzes user requests - Routes to appropriate sub-agent - Handles direct queries - Manages swarm coordination - create_timmy_swarm() factory function ## 5. Integration All components wired together: - Tools auto-register on import - Agents connect to event bus - MCP server provides HTTP API - Ready for dashboard integration ## Tests - All 973 existing tests pass - New components tested manually - Import verification successful Next steps: Cascade Router, Self-Upgrade Loop, Dashboard integration
This commit is contained in:
17
src/mcp/__init__.py
Normal file
17
src/mcp/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""MCP (Model Context Protocol) package.
|
||||
|
||||
Provides tool registry, server, and schema management.
|
||||
"""
|
||||
|
||||
from mcp.registry import tool_registry, register_tool
|
||||
from mcp.server import mcp_server, MCPServer, MCPHTTPServer
|
||||
from mcp.schemas.base import create_tool_schema
|
||||
|
||||
__all__ = [
|
||||
"tool_registry",
|
||||
"register_tool",
|
||||
"mcp_server",
|
||||
"MCPServer",
|
||||
"MCPHTTPServer",
|
||||
"create_tool_schema",
|
||||
]
|
||||
71
src/mcp/bootstrap.py
Normal file
71
src/mcp/bootstrap.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Bootstrap the MCP system by loading all tools.
|
||||
|
||||
This module is responsible for:
|
||||
1. Loading all tool modules from src/tools/
|
||||
2. Registering them with the tool registry
|
||||
3. Verifying tool health
|
||||
4. Reporting status
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from mcp.registry import tool_registry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Tool modules to load
|
||||
TOOL_MODULES = [
|
||||
"tools.web_search",
|
||||
"tools.file_ops",
|
||||
"tools.code_exec",
|
||||
"tools.memory_tool",
|
||||
]
|
||||
|
||||
|
||||
def bootstrap_mcp() -> dict:
|
||||
"""Initialize the MCP system by loading all tools.
|
||||
|
||||
Returns:
|
||||
Status dict with loaded tools and any errors
|
||||
"""
|
||||
loaded = []
|
||||
errors = []
|
||||
|
||||
for module_name in TOOL_MODULES:
|
||||
try:
|
||||
# Import the module (this triggers @register_tool decorators)
|
||||
importlib.import_module(module_name)
|
||||
loaded.append(module_name)
|
||||
logger.info("Loaded tool module: %s", module_name)
|
||||
except Exception as exc:
|
||||
errors.append({"module": module_name, "error": str(exc)})
|
||||
logger.error("Failed to load tool module %s: %s", module_name, exc)
|
||||
|
||||
# Get registry status
|
||||
registry_status = tool_registry.to_dict()
|
||||
|
||||
status = {
|
||||
"loaded_modules": loaded,
|
||||
"errors": errors,
|
||||
"total_tools": len(registry_status.get("tools", [])),
|
||||
"tools_by_category": registry_status.get("categories", {}),
|
||||
"tool_names": tool_registry.list_tools(),
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"MCP Bootstrap complete: %d tools loaded from %d modules",
|
||||
status["total_tools"],
|
||||
len(loaded)
|
||||
)
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def get_tool_status() -> dict:
|
||||
"""Get current status of all tools."""
|
||||
return {
|
||||
"tools": tool_registry.to_dict(),
|
||||
"metrics": tool_registry.get_metrics(),
|
||||
}
|
||||
340
src/mcp/registry.py
Normal file
340
src/mcp/registry.py
Normal file
@@ -0,0 +1,340 @@
|
||||
"""MCP Tool Registry — Dynamic tool discovery and management.
|
||||
|
||||
The registry maintains a catalog of all available tools, their schemas,
|
||||
and health status. Tools can be registered dynamically at runtime.
|
||||
|
||||
Usage:
|
||||
from mcp.registry import tool_registry
|
||||
|
||||
# Register a tool
|
||||
tool_registry.register("web_search", web_search_schema, web_search_func)
|
||||
|
||||
# Discover tools
|
||||
tools = tool_registry.discover(capabilities=["search"])
|
||||
|
||||
# Execute a tool
|
||||
result = tool_registry.execute("web_search", {"query": "Bitcoin"})
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from mcp.schemas.base import create_tool_schema
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolRecord:
|
||||
"""A registered tool with metadata."""
|
||||
name: str
|
||||
schema: dict
|
||||
handler: Callable
|
||||
category: str = "general"
|
||||
health_status: str = "unknown" # healthy, degraded, unhealthy
|
||||
last_execution: Optional[float] = None
|
||||
execution_count: int = 0
|
||||
error_count: int = 0
|
||||
avg_latency_ms: float = 0.0
|
||||
added_at: float = field(default_factory=time.time)
|
||||
requires_confirmation: bool = False
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""Central registry for all MCP tools."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._tools: dict[str, ToolRecord] = {}
|
||||
self._categories: dict[str, list[str]] = {}
|
||||
logger.info("ToolRegistry initialized")
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
schema: dict,
|
||||
handler: Callable,
|
||||
category: str = "general",
|
||||
requires_confirmation: bool = False,
|
||||
) -> ToolRecord:
|
||||
"""Register a new tool.
|
||||
|
||||
Args:
|
||||
name: Unique tool name
|
||||
schema: JSON schema describing inputs/outputs
|
||||
handler: Function to execute
|
||||
category: Tool category for organization
|
||||
requires_confirmation: If True, user must approve before execution
|
||||
|
||||
Returns:
|
||||
The registered ToolRecord
|
||||
"""
|
||||
if name in self._tools:
|
||||
logger.warning("Tool '%s' already registered, replacing", name)
|
||||
|
||||
record = ToolRecord(
|
||||
name=name,
|
||||
schema=schema,
|
||||
handler=handler,
|
||||
category=category,
|
||||
requires_confirmation=requires_confirmation,
|
||||
)
|
||||
|
||||
self._tools[name] = record
|
||||
|
||||
# Add to category
|
||||
if category not in self._categories:
|
||||
self._categories[category] = []
|
||||
if name not in self._categories[category]:
|
||||
self._categories[category].append(name)
|
||||
|
||||
logger.info("Registered tool: %s (category: %s)", name, category)
|
||||
return record
|
||||
|
||||
def unregister(self, name: str) -> bool:
|
||||
"""Remove a tool from the registry."""
|
||||
if name not in self._tools:
|
||||
return False
|
||||
|
||||
record = self._tools.pop(name)
|
||||
|
||||
# Remove from category
|
||||
if record.category in self._categories:
|
||||
if name in self._categories[record.category]:
|
||||
self._categories[record.category].remove(name)
|
||||
|
||||
logger.info("Unregistered tool: %s", name)
|
||||
return True
|
||||
|
||||
def get(self, name: str) -> Optional[ToolRecord]:
|
||||
"""Get a tool record by name."""
|
||||
return self._tools.get(name)
|
||||
|
||||
def get_handler(self, name: str) -> Optional[Callable]:
|
||||
"""Get just the handler function for a tool."""
|
||||
record = self._tools.get(name)
|
||||
return record.handler if record else None
|
||||
|
||||
def get_schema(self, name: str) -> Optional[dict]:
|
||||
"""Get the JSON schema for a tool."""
|
||||
record = self._tools.get(name)
|
||||
return record.schema if record else None
|
||||
|
||||
def list_tools(self, category: Optional[str] = None) -> list[str]:
|
||||
"""List all tool names, optionally filtered by category."""
|
||||
if category:
|
||||
return self._categories.get(category, [])
|
||||
return list(self._tools.keys())
|
||||
|
||||
def list_categories(self) -> list[str]:
|
||||
"""List all tool categories."""
|
||||
return list(self._categories.keys())
|
||||
|
||||
def discover(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
healthy_only: bool = True,
|
||||
) -> list[ToolRecord]:
|
||||
"""Discover tools matching criteria.
|
||||
|
||||
Args:
|
||||
query: Search in tool names and descriptions
|
||||
category: Filter by category
|
||||
healthy_only: Only return healthy tools
|
||||
|
||||
Returns:
|
||||
List of matching ToolRecords
|
||||
"""
|
||||
results = []
|
||||
|
||||
for name, record in self._tools.items():
|
||||
# Category filter
|
||||
if category and record.category != category:
|
||||
continue
|
||||
|
||||
# Health filter
|
||||
if healthy_only and record.health_status == "unhealthy":
|
||||
continue
|
||||
|
||||
# Query filter
|
||||
if query:
|
||||
query_lower = query.lower()
|
||||
name_match = query_lower in name.lower()
|
||||
desc = record.schema.get("description", "")
|
||||
desc_match = query_lower in desc.lower()
|
||||
if not (name_match or desc_match):
|
||||
continue
|
||||
|
||||
results.append(record)
|
||||
|
||||
return results
|
||||
|
||||
async def execute(self, name: str, params: dict) -> Any:
|
||||
"""Execute a tool by name with given parameters.
|
||||
|
||||
Args:
|
||||
name: Tool name
|
||||
params: Parameters to pass to the tool
|
||||
|
||||
Returns:
|
||||
Tool execution result
|
||||
|
||||
Raises:
|
||||
ValueError: If tool not found
|
||||
RuntimeError: If tool execution fails
|
||||
"""
|
||||
record = self._tools.get(name)
|
||||
if not record:
|
||||
raise ValueError(f"Tool '{name}' not found in registry")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Check if handler is async
|
||||
if inspect.iscoroutinefunction(record.handler):
|
||||
result = await record.handler(**params)
|
||||
else:
|
||||
result = record.handler(**params)
|
||||
|
||||
# Update metrics
|
||||
latency_ms = (time.time() - start_time) * 1000
|
||||
record.last_execution = time.time()
|
||||
record.execution_count += 1
|
||||
|
||||
# Update rolling average latency
|
||||
if record.execution_count == 1:
|
||||
record.avg_latency_ms = latency_ms
|
||||
else:
|
||||
record.avg_latency_ms = (
|
||||
record.avg_latency_ms * 0.9 + latency_ms * 0.1
|
||||
)
|
||||
|
||||
# Mark healthy on success
|
||||
record.health_status = "healthy"
|
||||
|
||||
logger.debug("Tool '%s' executed in %.2fms", name, latency_ms)
|
||||
return result
|
||||
|
||||
except Exception as exc:
|
||||
record.error_count += 1
|
||||
record.execution_count += 1
|
||||
|
||||
# Degrade health on repeated errors
|
||||
error_rate = record.error_count / record.execution_count
|
||||
if error_rate > 0.5:
|
||||
record.health_status = "unhealthy"
|
||||
logger.error("Tool '%s' marked unhealthy (error rate: %.1f%%)",
|
||||
name, error_rate * 100)
|
||||
elif error_rate > 0.2:
|
||||
record.health_status = "degraded"
|
||||
logger.warning("Tool '%s' degraded (error rate: %.1f%%)",
|
||||
name, error_rate * 100)
|
||||
|
||||
raise RuntimeError(f"Tool '{name}' execution failed: {exc}") from exc
|
||||
|
||||
def check_health(self, name: str) -> str:
|
||||
"""Check health status of a tool."""
|
||||
record = self._tools.get(name)
|
||||
if not record:
|
||||
return "not_found"
|
||||
return record.health_status
|
||||
|
||||
def get_metrics(self, name: Optional[str] = None) -> dict:
|
||||
"""Get metrics for a tool or all tools."""
|
||||
if name:
|
||||
record = self._tools.get(name)
|
||||
if not record:
|
||||
return {}
|
||||
return {
|
||||
"name": record.name,
|
||||
"category": record.category,
|
||||
"health": record.health_status,
|
||||
"executions": record.execution_count,
|
||||
"errors": record.error_count,
|
||||
"avg_latency_ms": round(record.avg_latency_ms, 2),
|
||||
}
|
||||
|
||||
# Return metrics for all tools
|
||||
return {
|
||||
name: self.get_metrics(name)
|
||||
for name in self._tools.keys()
|
||||
}
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Export registry as dictionary (for API/dashboard)."""
|
||||
return {
|
||||
"tools": [
|
||||
{
|
||||
"name": r.name,
|
||||
"schema": r.schema,
|
||||
"category": r.category,
|
||||
"health": r.health_status,
|
||||
"requires_confirmation": r.requires_confirmation,
|
||||
}
|
||||
for r in self._tools.values()
|
||||
],
|
||||
"categories": self._categories,
|
||||
"total_tools": len(self._tools),
|
||||
}
|
||||
|
||||
|
||||
# Module-level singleton
|
||||
tool_registry = ToolRegistry()
|
||||
|
||||
|
||||
def register_tool(
|
||||
name: Optional[str] = None,
|
||||
category: str = "general",
|
||||
schema: Optional[dict] = None,
|
||||
requires_confirmation: bool = False,
|
||||
):
|
||||
"""Decorator for registering a function as an MCP tool.
|
||||
|
||||
Usage:
|
||||
@register_tool(name="web_search", category="research")
|
||||
def web_search(query: str, max_results: int = 5) -> str:
|
||||
...
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
tool_name = name or func.__name__
|
||||
|
||||
# Auto-generate schema if not provided
|
||||
if schema is None:
|
||||
# Try to infer from type hints
|
||||
sig = inspect.signature(func)
|
||||
params = {}
|
||||
required = []
|
||||
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param.default == inspect.Parameter.empty:
|
||||
required.append(param_name)
|
||||
params[param_name] = {"type": "string"}
|
||||
else:
|
||||
params[param_name] = {
|
||||
"type": "string",
|
||||
"default": str(param.default),
|
||||
}
|
||||
|
||||
tool_schema = create_tool_schema(
|
||||
name=tool_name,
|
||||
description=func.__doc__ or f"Execute {tool_name}",
|
||||
parameters=params,
|
||||
required=required,
|
||||
)
|
||||
else:
|
||||
tool_schema = schema
|
||||
|
||||
tool_registry.register(
|
||||
name=tool_name,
|
||||
schema=tool_schema,
|
||||
handler=func,
|
||||
category=category,
|
||||
requires_confirmation=requires_confirmation,
|
||||
)
|
||||
|
||||
return func
|
||||
return decorator
|
||||
52
src/mcp/schemas/base.py
Normal file
52
src/mcp/schemas/base.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Base schemas for MCP (Model Context Protocol) tools.
|
||||
|
||||
All tools must provide a JSON schema describing their interface.
|
||||
This enables dynamic discovery and type-safe invocation.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def create_tool_schema(
|
||||
name: str,
|
||||
description: str,
|
||||
parameters: dict[str, Any],
|
||||
required: list[str] | None = None,
|
||||
returns: dict[str, Any] | None = None,
|
||||
) -> dict:
|
||||
"""Create a standard MCP tool schema.
|
||||
|
||||
Args:
|
||||
name: Tool name (must be unique)
|
||||
description: Human-readable description
|
||||
parameters: JSON schema for input parameters
|
||||
required: List of required parameter names
|
||||
returns: JSON schema for return value
|
||||
|
||||
Returns:
|
||||
Complete tool schema dict
|
||||
"""
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": parameters,
|
||||
"required": required or [],
|
||||
},
|
||||
"returns": returns or {"type": "string"},
|
||||
}
|
||||
|
||||
|
||||
# Common parameter schemas
|
||||
PARAM_STRING = {"type": "string"}
|
||||
PARAM_INTEGER = {"type": "integer"}
|
||||
PARAM_BOOLEAN = {"type": "boolean"}
|
||||
PARAM_ARRAY_STRINGS = {"type": "array", "items": {"type": "string"}}
|
||||
PARAM_OBJECT = {"type": "object"}
|
||||
|
||||
# Common return schemas
|
||||
RETURN_STRING = {"type": "string"}
|
||||
RETURN_OBJECT = {"type": "object"}
|
||||
RETURN_ARRAY = {"type": "array"}
|
||||
RETURN_BOOLEAN = {"type": "boolean"}
|
||||
210
src/mcp/server.py
Normal file
210
src/mcp/server.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""MCP (Model Context Protocol) Server.
|
||||
|
||||
Implements the MCP protocol for tool discovery and execution.
|
||||
Agents communicate with this server to discover and invoke tools.
|
||||
|
||||
The server can run:
|
||||
1. In-process (direct method calls) — fastest, for local agents
|
||||
2. HTTP API — for external clients
|
||||
3. Stdio — for subprocess-based agents
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from mcp.registry import tool_registry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MCPServer:
|
||||
"""Model Context Protocol server for tool management.
|
||||
|
||||
Provides standard MCP endpoints:
|
||||
- list_tools: Discover available tools
|
||||
- call_tool: Execute a tool
|
||||
- get_schema: Get tool input/output schemas
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.registry = tool_registry
|
||||
logger.info("MCP Server initialized")
|
||||
|
||||
def list_tools(
|
||||
self,
|
||||
category: Optional[str] = None,
|
||||
query: Optional[str] = None,
|
||||
) -> list[dict]:
|
||||
"""List available tools.
|
||||
|
||||
MCP Protocol: tools/list
|
||||
"""
|
||||
tools = self.registry.discover(
|
||||
query=query,
|
||||
category=category,
|
||||
healthy_only=True,
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"name": t.name,
|
||||
"description": t.schema.get("description", ""),
|
||||
"parameters": t.schema.get("parameters", {}),
|
||||
"category": t.category,
|
||||
}
|
||||
for t in tools
|
||||
]
|
||||
|
||||
async def call_tool(self, name: str, arguments: dict) -> dict:
|
||||
"""Execute a tool with given arguments.
|
||||
|
||||
MCP Protocol: tools/call
|
||||
|
||||
Args:
|
||||
name: Tool name
|
||||
arguments: Tool parameters
|
||||
|
||||
Returns:
|
||||
Result dict with content or error
|
||||
"""
|
||||
try:
|
||||
result = await self.registry.execute(name, arguments)
|
||||
return {
|
||||
"content": [
|
||||
{"type": "text", "text": str(result)}
|
||||
],
|
||||
"isError": False,
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.error("Tool execution failed: %s", exc)
|
||||
return {
|
||||
"content": [
|
||||
{"type": "text", "text": f"Error: {exc}"}
|
||||
],
|
||||
"isError": True,
|
||||
}
|
||||
|
||||
def get_schema(self, name: str) -> Optional[dict]:
|
||||
"""Get the JSON schema for a tool.
|
||||
|
||||
MCP Protocol: tools/schema
|
||||
"""
|
||||
return self.registry.get_schema(name)
|
||||
|
||||
def get_tool_info(self, name: str) -> Optional[dict]:
|
||||
"""Get detailed info about a tool including health metrics."""
|
||||
record = self.registry.get(name)
|
||||
if not record:
|
||||
return None
|
||||
|
||||
return {
|
||||
"name": record.name,
|
||||
"schema": record.schema,
|
||||
"category": record.category,
|
||||
"health": record.health_status,
|
||||
"metrics": {
|
||||
"executions": record.execution_count,
|
||||
"errors": record.error_count,
|
||||
"avg_latency_ms": round(record.avg_latency_ms, 2),
|
||||
},
|
||||
"requires_confirmation": record.requires_confirmation,
|
||||
}
|
||||
|
||||
def health_check(self) -> dict:
|
||||
"""Server health status."""
|
||||
tools = self.registry.list_tools()
|
||||
healthy = sum(
|
||||
1 for t in tools
|
||||
if self.registry.check_health(t) == "healthy"
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"total_tools": len(tools),
|
||||
"healthy_tools": healthy,
|
||||
"degraded_tools": sum(
|
||||
1 for t in tools
|
||||
if self.registry.check_health(t) == "degraded"
|
||||
),
|
||||
"unhealthy_tools": sum(
|
||||
1 for t in tools
|
||||
if self.registry.check_health(t) == "unhealthy"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class MCPHTTPServer:
|
||||
"""HTTP API wrapper for MCP Server."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.mcp = MCPServer()
|
||||
|
||||
def get_routes(self) -> dict:
|
||||
"""Get FastAPI route handlers."""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/mcp", tags=["mcp"])
|
||||
|
||||
class ToolCallRequest(BaseModel):
|
||||
name: str
|
||||
arguments: dict = {}
|
||||
|
||||
@router.get("/tools")
|
||||
async def list_tools(
|
||||
category: Optional[str] = None,
|
||||
query: Optional[str] = None,
|
||||
):
|
||||
"""List available tools."""
|
||||
return {"tools": self.mcp.list_tools(category, query)}
|
||||
|
||||
@router.post("/tools/call")
|
||||
async def call_tool(request: ToolCallRequest):
|
||||
"""Execute a tool."""
|
||||
result = await self.mcp.call_tool(request.name, request.arguments)
|
||||
return result
|
||||
|
||||
@router.get("/tools/{name}")
|
||||
async def get_tool(name: str):
|
||||
"""Get tool info."""
|
||||
info = self.mcp.get_tool_info(name)
|
||||
if not info:
|
||||
raise HTTPException(404, f"Tool '{name}' not found")
|
||||
return info
|
||||
|
||||
@router.get("/tools/{name}/schema")
|
||||
async def get_schema(name: str):
|
||||
"""Get tool schema."""
|
||||
schema = self.mcp.get_schema(name)
|
||||
if not schema:
|
||||
raise HTTPException(404, f"Tool '{name}' not found")
|
||||
return schema
|
||||
|
||||
@router.get("/health")
|
||||
async def health():
|
||||
"""Server health check."""
|
||||
return self.mcp.health_check()
|
||||
|
||||
return router
|
||||
|
||||
|
||||
# Module-level singleton
|
||||
mcp_server = MCPServer()
|
||||
|
||||
|
||||
# Convenience functions for agents
|
||||
def discover_tools(query: Optional[str] = None) -> list[dict]:
|
||||
"""Quick tool discovery."""
|
||||
return mcp_server.list_tools(query=query)
|
||||
|
||||
|
||||
async def use_tool(name: str, **kwargs) -> str:
|
||||
"""Execute a tool and return result text."""
|
||||
result = await mcp_server.call_tool(name, kwargs)
|
||||
|
||||
if result.get("isError"):
|
||||
raise RuntimeError(result["content"][0]["text"])
|
||||
|
||||
return result["content"][0]["text"]
|
||||
Reference in New Issue
Block a user