feat(engine): 添加MCP连接管理和工具集成功能
- 集成MCP连接管理器,支持MCP服务器连接 - 添加多种内置工具:ClarifyTool、CronTool、DelegateTool、ExecuteCodeTool、 PatchFileTool、ProcessTool、SendMessageTool、SpawnTool、TerminalTool、 TodoTool、WebFetchTool、WebSearchTool、WriteFileTool等 - 实现工具注册和装配功能 - 添加技能选择上下文参数 - 支持思考模式控制参数thinking_enabled feat(coordinator): 重构任务执行计划器参数命名 - 将learning_candidate_enabled重命名为allow_candidate_generation - 更新TeamGraphScheduler中的参数传递 - 修改LocalAgentRunner中的相关参数处理 - 更新README文档中的相应描述 refactor(context): 标准化工具调用参数格式 - 添加_json导入用于参数序列化 - 实现_provider_tool_calls方法标准化OpenAI兼容的工具调用载荷 - 修复工具调用中参数非字符串类型的序列化问题 refactor(session): 优化消息历史记录过滤逻辑 - 修改get_messages_as_conversation为基于运行状态过滤消息 - 排除未完成、失败或错误结束的运行记录 - 改进对话历史的可见性控制机制 fix(store): 修复FTS索引重建逻辑 - 添加异常处理防止FTS索引创建失败 - 实现_rebuild_fts_index方法重新构建全文搜索索引 - 优化索引触发器和表的维护流程
This commit is contained in:
192
app-instance/backend/beaver/interfaces/mcp/tools_server.py
Normal file
192
app-instance/backend/beaver/interfaces/mcp/tools_server.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""Beaver local tools as real stdio MCP servers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import mcp.types as types
|
||||
from mcp.server.lowlevel import Server
|
||||
from mcp.server.lowlevel.server import NotificationOptions
|
||||
from mcp.server.models import InitializationOptions
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
from beaver.engine.session import SessionManager
|
||||
from beaver.memory.curated.store import MemoryStore
|
||||
from beaver.services.cron_service import CronService
|
||||
from beaver.skills import SkillsLoader
|
||||
from beaver.skills.drafts import DraftService
|
||||
from beaver.skills.specs import SkillSpecStore
|
||||
from beaver.tools.base import BaseTool, ObjectBackedTool, ToolContext
|
||||
from beaver.tools.builtins import (
|
||||
ClarifyTool,
|
||||
CronTool,
|
||||
DelegateTool,
|
||||
ExecuteCodeTool,
|
||||
ListDirectoryTool,
|
||||
MemoryTool,
|
||||
PatchFileTool,
|
||||
ProcessTool,
|
||||
ReadFileTool,
|
||||
SearchFilesTool,
|
||||
SendMessageTool,
|
||||
SkillManageTool,
|
||||
SkillViewTool,
|
||||
SkillsListTool,
|
||||
SpawnTool,
|
||||
TerminalTool,
|
||||
TodoTool,
|
||||
WebFetchTool,
|
||||
WebSearchTool,
|
||||
WriteFileTool,
|
||||
)
|
||||
|
||||
|
||||
LOCAL_TOOL_CATEGORIES = {
|
||||
"filesystem": "Beaver Local Filesystem Tools",
|
||||
"runtime": "Beaver Local Runtime Tools",
|
||||
"memory": "Beaver Local Memory Tools",
|
||||
"skills": "Beaver Local Skills Tools",
|
||||
"coordination": "Beaver Local Coordination Tools",
|
||||
"scheduler": "Beaver Local Scheduler Tools",
|
||||
"web": "Beaver Local Web Tools",
|
||||
}
|
||||
|
||||
|
||||
def _workspace_path(value: str | None = None) -> Path:
|
||||
raw = value or os.getenv("BEAVER_WORKSPACE") or os.getenv("NANOBOT_WORKSPACE")
|
||||
if raw:
|
||||
return Path(raw).expanduser().resolve()
|
||||
return Path.cwd()
|
||||
|
||||
|
||||
def _json_content(value: str) -> dict[str, Any]:
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
return parsed if isinstance(parsed, dict) else {"success": True, "result": parsed}
|
||||
except json.JSONDecodeError:
|
||||
return {"success": True, "content": value}
|
||||
|
||||
|
||||
def _category_tools(category: str, workspace: Path) -> tuple[list[BaseTool], ToolContext]:
|
||||
skill_store = SkillSpecStore(workspace)
|
||||
skills_loader = SkillsLoader(workspace, skill_store=skill_store)
|
||||
draft_service = DraftService(skill_store)
|
||||
services = {
|
||||
"skills_loader": skills_loader,
|
||||
"draft_service": draft_service,
|
||||
}
|
||||
context = ToolContext(workspace=str(workspace), services=services)
|
||||
|
||||
if category == "filesystem":
|
||||
tools: list[BaseTool] = [
|
||||
ObjectBackedTool(ListDirectoryTool()),
|
||||
ObjectBackedTool(ReadFileTool()),
|
||||
ObjectBackedTool(SearchFilesTool()),
|
||||
ObjectBackedTool(WriteFileTool()),
|
||||
ObjectBackedTool(PatchFileTool()),
|
||||
]
|
||||
elif category == "runtime":
|
||||
tools = [
|
||||
ObjectBackedTool(TerminalTool()),
|
||||
ObjectBackedTool(ProcessTool()),
|
||||
ObjectBackedTool(ExecuteCodeTool()),
|
||||
]
|
||||
elif category == "memory":
|
||||
session_manager = SessionManager(workspace)
|
||||
memory_store = MemoryStore(workspace / "memory" / "curated")
|
||||
memory_store.load_from_disk()
|
||||
tools = [
|
||||
ObjectBackedTool(MemoryTool(store=memory_store)),
|
||||
ObjectBackedTool(__import__("beaver.tools.builtins.session_search", fromlist=["SessionSearchTool"]).SessionSearchTool(db=session_manager)),
|
||||
]
|
||||
elif category == "skills":
|
||||
tools = [
|
||||
ObjectBackedTool(SkillViewTool(loader=skills_loader)),
|
||||
SkillsListTool(),
|
||||
SkillManageTool(),
|
||||
]
|
||||
elif category == "coordination":
|
||||
tools = [
|
||||
ObjectBackedTool(TodoTool()),
|
||||
ObjectBackedTool(ClarifyTool()),
|
||||
ObjectBackedTool(DelegateTool()),
|
||||
ObjectBackedTool(SpawnTool()),
|
||||
ObjectBackedTool(SendMessageTool()),
|
||||
]
|
||||
elif category == "scheduler":
|
||||
services["cron_service"] = CronService(workspace / "cron" / "jobs.json")
|
||||
tools = [CronTool()]
|
||||
elif category == "web":
|
||||
tools = [
|
||||
ObjectBackedTool(WebFetchTool()),
|
||||
ObjectBackedTool(WebSearchTool()),
|
||||
]
|
||||
else:
|
||||
raise ValueError(f"Unknown local tool category: {category}")
|
||||
return tools, context
|
||||
|
||||
|
||||
def create_tools_server(*, category: str, workspace: str | None = None) -> Server:
|
||||
workspace_path = _workspace_path(workspace)
|
||||
tools, context = _category_tools(category, workspace_path)
|
||||
tool_map = {tool.spec.name: tool for tool in tools}
|
||||
server = Server(LOCAL_TOOL_CATEGORIES.get(category, f"Beaver Local {category} Tools"))
|
||||
|
||||
@server.list_tools()
|
||||
async def list_tools() -> list[types.Tool]:
|
||||
return [
|
||||
types.Tool(
|
||||
name=tool.spec.name,
|
||||
description=tool.spec.description,
|
||||
inputSchema=tool.spec.input_schema,
|
||||
)
|
||||
for tool in tools
|
||||
]
|
||||
|
||||
@server.call_tool(validate_input=True)
|
||||
async def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
||||
tool = tool_map.get(name)
|
||||
if tool is None:
|
||||
return {"success": False, "error": f"Unknown tool: {name}"}
|
||||
result = await tool.invoke(arguments or {}, context)
|
||||
if result.raw_output is not None and isinstance(result.raw_output, dict):
|
||||
return result.raw_output
|
||||
payload = _json_content(result.content)
|
||||
if "success" not in payload:
|
||||
payload["success"] = bool(result.success)
|
||||
if result.error and "error" not in payload:
|
||||
payload["error"] = result.error
|
||||
return payload
|
||||
|
||||
return server
|
||||
|
||||
|
||||
async def _run_stdio(category: str, workspace: str | None) -> None:
|
||||
server = create_tools_server(category=category, workspace=workspace)
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
InitializationOptions(
|
||||
server_name=LOCAL_TOOL_CATEGORIES.get(category, f"beaver-{category}"),
|
||||
server_version="0.1.0",
|
||||
capabilities=server.get_capabilities(notification_options=NotificationOptions(), experimental_capabilities={}),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Run a Beaver local tool category as a stdio MCP server.")
|
||||
parser.add_argument("--category", choices=sorted(LOCAL_TOOL_CATEGORIES), required=True)
|
||||
parser.add_argument("--workspace", default=None)
|
||||
args = parser.parse_args()
|
||||
asyncio.run(_run_stdio(args.category, args.workspace))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user