butler/mcp/server_stdio.py
Rama main · réplica de lectura del repositorio original.
"""Expose a subset of Butler tools as an MCP Server (stdio, dev use)."""
from __future__ import annotations
import json
import logging
from typing import Any, cast
logger = logging.getLogger(__name__)
_EXPOSED = (
"read_file",
"list_directory",
"search_files",
"session_todos_list",
)
def run_stdio_server() -> int:
try:
from mcp.server.fastmcp import FastMCP
except ImportError:
logger.error("Install MCP SDK: pip install butler-system[mcp]")
return 1
mcp = FastMCP("butler")
@mcp.tool(name="read_file", description="Read a file from the active Butler workspace") # type: ignore[untyped-decorator]
def mcp_read_file(path: str, offset: int = 1, limit: int = 500) -> str:
return _dispatch_builtin("read_file", {"path": path, "offset": offset, "limit": limit})
@mcp.tool(name="list_directory", description="List directory entries") # type: ignore[untyped-decorator]
def mcp_list_directory(path: str = ".") -> str:
return _dispatch_builtin("list_directory", {"path": path})
@mcp.tool(name="search_files", description="Ripgrep search in workspace") # type: ignore[untyped-decorator]
def mcp_search_files(pattern: str, path: str = ".", glob: str = "") -> str:
args: dict[str, Any] = {"pattern": pattern, "path": path}
if glob:
args["glob"] = glob
return _dispatch_builtin("search_files", args)
@mcp.tool(name="session_todos_list", description="List session-scoped todos") # type: ignore[untyped-decorator]
def mcp_session_todos_list() -> str:
return _dispatch_builtin("session_todos_list", {})
mcp.run(transport="stdio")
return 0
def _dispatch_builtin(name: str, args: dict[str, Any]) -> str:
if name not in _EXPOSED:
return json.dumps({"ok": False, "error": f"tool not exposed: {name}"})
from butler.tools.registry import dispatch_tool
return cast(str, dispatch_tool(name, args))