ADK-agents/mcp_server/server.py
2026-07-29 17:21:34 +08:00

409 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Dev Agent MCP ServerHTTP 模式)
将 Dev Agent 暴露为 MCP 服务器CodeBuddy 通过 HTTP POST MCP 调用
直接用 FastAPI 处理 MCP JSON-RPC 消息,不依赖 mcp SDK 的 HTTP transport
避免各种版本兼容问题。
"""
import os
import sys
import asyncio
import json
import uuid
from dotenv import load_dotenv
# 确保项目根目录在 path 里
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
from mcp.server import Server
from mcp.types import Tool, TextContent
from fastapi import FastAPI, Request, Response
import uvicorn
from .task_manager import TaskManager
from .agent_runner import AgentRunner
# 加载 .env
load_dotenv(os.path.join(PROJECT_ROOT, "my_agent", ".env"))
# 配置
HOST = os.getenv("MCP_SERVER_HOST", "0.0.0.0")
PORT = int(os.getenv("MCP_SERVER_PORT", "8001"))
DATA_DIR = os.getenv("MCP_DATA_DIR", os.path.join(PROJECT_ROOT, "data"))
MCP_PATH = "/mcp" # MCP 端点路径
# 初始化组件
task_manager = TaskManager(store_dir=os.path.join(DATA_DIR, "tasks"))
agent_runner = AgentRunner()
mcp_server = Server("dev-agent-mcp-server")
# --- MCP 工具定义 ---
@mcp_server.list_tools()
async def list_tools():
return [
Tool(
name="submit_task",
description=(
"提交一个开发任务给 Dev Agent 执行。任务将异步执行,"
"提交后返回 task_id用 get_task_status 查询进度。"
),
inputSchema={
"type": "object",
"properties": {
"description": {
"type": "string",
"description": "任务的详细描述,要做什么开发工作",
},
"project_path": {
"type": "string",
"description": "项目的本地路径agent 将在此目录下工作",
},
"requirements": {
"type": "string",
"description": "(可选)额外的要求或约束条件",
},
},
"required": ["description", "project_path"],
},
),
Tool(
name="get_task_status",
description="查询任务的当前状态pending/running/completed/failed/cancelled",
inputSchema={
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "任务 ID",
},
},
"required": ["task_id"],
},
),
Tool(
name="get_task_result",
description="获取任务的执行结果(完成后调用)",
inputSchema={
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "任务 ID",
},
},
"required": ["task_id"],
},
),
Tool(
name="get_task_log",
description="获取任务的执行日志",
inputSchema={
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "任务 ID",
},
},
"required": ["task_id"],
},
),
Tool(
name="cancel_task",
description="取消一个正在执行或等待中的任务",
inputSchema={
"type": "object",
"properties": {
"task_id": {
"type": "string",
"description": "任务 ID",
},
},
"required": ["task_id"],
},
),
Tool(
name="list_tasks",
description="列出所有任务,可按状态过滤",
inputSchema={
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "可选按状态过滤pending/running/completed/failed/cancelled",
},
"limit": {
"type": "integer",
"description": "(可选)返回数量限制,默认 20",
"default": 20,
},
},
},
),
]
# --- MCP 工具实现 ---
@mcp_server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "submit_task":
return await _submit_task(arguments)
elif name == "get_task_status":
return _get_task_status(arguments)
elif name == "get_task_result":
return _get_task_result(arguments)
elif name == "get_task_log":
return _get_task_log(arguments)
elif name == "cancel_task":
return await _cancel_task(arguments)
elif name == "list_tasks":
return _list_tasks(arguments)
else:
return [TextContent(type="text", text=f"错误:未知工具 {name}")]
async def _submit_task(args: dict):
description = args.get("description", "")
project_path = args.get("project_path", "")
requirements = args.get("requirements", "")
if not description:
return [TextContent(type="text", text="错误description 不能为空")]
if not project_path:
return [TextContent(type="text", text="错误project_path 不能为空")]
if not os.path.isdir(project_path):
return [TextContent(type="text", text=f"错误:项目路径不存在 {project_path}")]
task = await task_manager.submit_task(
description=description,
project_path=project_path,
requirements=requirements,
)
return [TextContent(
type="text",
text=(
f"任务已提交\n"
f"任务ID: {task['id']}\n"
f"状态: {task['status']}\n"
f"描述: {description[:100]}\n"
f"项目: {project_path}\n"
f"\n"
f"请使用 get_task_status 查询进度。"
),
)]
def _get_task_status(args: dict):
task_id = args.get("task_id", "")
task = task_manager.get_task(task_id)
if not task:
return [TextContent(type="text", text=f"错误:任务不存在 {task_id}")]
return [TextContent(
type="text",
text=(
f"任务状态\n"
f"任务ID: {task['id']}\n"
f"状态: {task['status']}\n"
f"描述: {task['description'][:100]}\n"
f"创建时间: {_format_time(task.get('created_at'))}\n"
f"更新时间: {_format_time(task.get('updated_at'))}\n"
),
)]
def _get_task_result(args: dict):
task_id = args.get("task_id", "")
task = task_manager.get_task(task_id)
if not task:
return [TextContent(type="text", text=f"错误:任务不存在 {task_id}")]
result = task.get("result")
status = task["status"]
if status in ("pending", "running"):
return [TextContent(
type="text",
text=(
f"任务尚未完成(状态:{status}"
f"请稍后再试或使用 get_task_status 查询进度。"
),
)]
if not result:
return [TextContent(type="text", text=f"任务结果为空,状态:{status}")]
if isinstance(result, dict):
summary = result.get("summary", str(result))
tool_count = result.get("tool_calls_count", 0)
full = result.get("full_response", "")
return [TextContent(
type="text",
text=(
f"任务结果({status}\n"
f"{'='*40}\n"
f"{summary}\n"
f"{'='*40}\n"
f"工具调用次数: {tool_count}\n"
f"\n"
f"--- 完整回复 ---\n"
f"{full[:5000]}"
f"\n{'...' if len(full) > 5000 else ''}"
),
)]
return [TextContent(type="text", text=str(result))]
def _get_task_log(args: dict):
task_id = args.get("task_id", "")
task = task_manager.get_task(task_id)
if not task:
return [TextContent(type="text", text=f"错误:任务不存在 {task_id}")]
logs = task.get("logs", [])
if not logs:
return [TextContent(type="text", text="暂无日志")]
lines = []
for log in logs[-50:]:
lines.append(f"[{log['time']}] {log['message']}")
return [TextContent(type="text", text="\n".join(lines))]
async def _cancel_task(args: dict):
task_id = args.get("task_id", "")
success = await task_manager.cancel_task(task_id)
if success:
return [TextContent(type="text", text=f"任务 {task_id} 已取消")]
else:
return [TextContent(type="text", text=f"取消失败:任务不存在或已结束")]
def _list_tasks(args: dict):
status = args.get("status")
limit = int(args.get("limit", 20))
tasks = task_manager.list_tasks(status=status)
tasks = tasks[:limit]
if not tasks:
return [TextContent(type="text", text="没有找到任务")]
lines = [f"任务列表(共 {len(tasks)} 个):"]
for t in tasks:
lines.append(
f" [{t['status']}] {t['id']} - {t['description'][:50]} "
f"({_format_time(t.get('created_at'))})"
)
return [TextContent(type="text", text="\n".join(lines))]
def _format_time(ts: float = None) -> str:
import time
if not ts:
return "-"
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))
# --- HTTP Server ---
def create_fastapi_app() -> FastAPI:
"""创建 FastAPI 应用,处理 MCP JSON-RPC 请求"""
fastapi_app = FastAPI(title="Dev Agent MCP Server")
# 存储 sessionsession_id -> (read_stream, write_stream, session_task)
sessions = {}
async def _get_or_create_session(session_id: str):
"""获取或创建 MCP session简单的内存会话管理"""
if session_id not in sessions:
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from mcp.server.session import ServerSession
read_stream_writer, read_stream = MemoryObjectSendStream(100), MemoryObjectReceiveStream(100)
write_stream, write_stream_reader = MemoryObjectSendStream(100), MemoryObjectReceiveStream(100)
session = ServerSession(read_stream, write_stream)
task = asyncio.create_task(
mcp_server.run(
read_stream, write_stream,
mcp_server.create_initialization_options(),
)
)
sessions[session_id] = (read_stream_writer, write_stream_reader, task, session)
return sessions[session_id]
@fastapi_app.post(MCP_PATH)
async def handle_mcp(request: Request):
"""处理 MCP JSON-RPC 请求"""
body = await request.json()
# 简单处理:单条请求(非批量)
# 从 header 获取 session_id没有就创建新的
session_id = request.headers.get("mcp-session-id") or str(uuid.uuid4())
read_stream_writer, write_stream_reader, task, session = await _get_or_create_session(session_id)
# 把请求写入 read_stream
await read_stream_writer.send(body)
# 等待响应(简单地从 write_stream 读一条)
response = await write_stream_reader.receive()
# 返回响应
return Response(
content=json.dumps(response),
media_type="application/json",
headers={"mcp-session-id": session_id},
)
@fastapi_app.get("/health")
async def health():
return {"status": "ok", "server": "dev-agent-mcp-server"}
return fastapi_app
async def _task_executor(task: dict) -> dict:
"""任务执行器,交给 TaskManager 调用"""
return await agent_runner.run_task(task)
async def main():
"""启动 MCP Server"""
print("=" * 50)
print("Dev Agent MCP Server 启动中...")
print(f" 监听地址: {HOST}:{PORT}")
print(f" 数据目录: {DATA_DIR}")
print(f" MCP 端点: http://{HOST}:{PORT}{MCP_PATH}")
print(f" 健康检查: http://{HOST}:{PORT}/health")
print("=" * 50)
# 启动任务管理器
await task_manager.start(executor=_task_executor)
# 启动 HTTP server
fastapi_app = create_fastapi_app()
config = uvicorn.Config(fastapi_app, host=HOST, port=PORT, log_level="info")
server = uvicorn.Server(config)
try:
await server.serve()
finally:
await task_manager.stop()
if __name__ == "__main__":
asyncio.run(main())