forked from zhanggangyong/agent_management
328 lines
9.5 KiB
Python
328 lines
9.5 KiB
Python
"""
|
|
MCP HTTP/SSE 服务器 - 支持远程调用
|
|
实现 MCP 协议的 HTTP 和 SSE 传输方式,供 Cursor 等客户端远程调用
|
|
"""
|
|
import json
|
|
import os
|
|
import uuid
|
|
from typing import Dict, Any, Optional, AsyncGenerator
|
|
from fastapi import FastAPI, Request, HTTPException, Header
|
|
from fastapi.responses import StreamingResponse, JSONResponse
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel
|
|
from loguru import logger
|
|
|
|
# 导入 MCP 服务器和工具函数
|
|
from .mcp_server import (
|
|
server as mcp_server,
|
|
search_facebook,
|
|
initialize_agent
|
|
)
|
|
|
|
# 工具映射
|
|
TOOL_MAP = {
|
|
'search_facebook': search_facebook,
|
|
}
|
|
|
|
|
|
# 创建 FastAPI 应用
|
|
app = FastAPI(
|
|
title="MCP HTTP/SSE Server - Facebook搜索Agent",
|
|
description="MCP 协议的 HTTP 和 SSE 传输实现",
|
|
version="1.0.0"
|
|
)
|
|
|
|
# 配置 CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Session 管理
|
|
sessions: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
class MCPRequest(BaseModel):
|
|
"""MCP JSON-RPC 请求"""
|
|
jsonrpc: str = "2.0"
|
|
id: Optional[str] = None
|
|
method: str
|
|
params: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
class MCPResponse(BaseModel):
|
|
"""MCP JSON-RPC 响应"""
|
|
jsonrpc: str = "2.0"
|
|
id: Optional[str] = None
|
|
result: Optional[Any] = None
|
|
error: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
async def handle_mcp_request(request_data: Dict[str, Any], session_id: Optional[str] = None) -> Dict[str, Any]:
|
|
"""处理 MCP 请求"""
|
|
method = request_data.get("method")
|
|
params = request_data.get("params", {})
|
|
request_id = request_data.get("id")
|
|
|
|
try:
|
|
if method == "initialize":
|
|
# 初始化会话
|
|
if not session_id:
|
|
session_id = str(uuid.uuid4())
|
|
sessions[session_id] = {
|
|
"initialized": True,
|
|
"capabilities": {}
|
|
}
|
|
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": request_id,
|
|
"result": {
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {
|
|
"tools": {},
|
|
"resources": {}
|
|
},
|
|
"serverInfo": {
|
|
"name": "Facebook搜索Agent",
|
|
"version": "1.0.0"
|
|
}
|
|
}
|
|
}
|
|
|
|
elif method == "tools/list":
|
|
# 列出所有工具
|
|
tools = [
|
|
{
|
|
"name": "search_facebook",
|
|
"description": "搜索Facebook内容,返回相关帖子和AI生成的总结。支持搜索关键词,返回帖子标题、链接、摘要、作者、点赞数等信息,并提供AI生成的总结。",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "搜索关键词,例如:technology news、travel tips、food recipes等"
|
|
},
|
|
"limit": {
|
|
"type": "integer",
|
|
"description": "返回结果数量,默认5,最大20",
|
|
"default": 5,
|
|
"minimum": 1,
|
|
"maximum": 20
|
|
}
|
|
},
|
|
"required": ["query"]
|
|
}
|
|
}
|
|
]
|
|
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": request_id,
|
|
"result": {
|
|
"tools": tools
|
|
}
|
|
}
|
|
|
|
elif method == "tools/call":
|
|
# 调用工具
|
|
tool_name = params.get("name")
|
|
arguments = params.get("arguments", {})
|
|
|
|
if tool_name not in TOOL_MAP:
|
|
raise ValueError(f"Tool '{tool_name}' not found")
|
|
|
|
# 获取工具函数
|
|
tool_func = TOOL_MAP[tool_name]
|
|
|
|
# 调用工具(异步)
|
|
result = await tool_func(**arguments)
|
|
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": request_id,
|
|
"result": {
|
|
"content": [
|
|
{
|
|
"type": "text",
|
|
"text": str(result)
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
elif method == "ping":
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": request_id,
|
|
"result": {}
|
|
}
|
|
|
|
else:
|
|
raise ValueError(f"Unknown method: {method}")
|
|
|
|
except Exception as e:
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": request_id,
|
|
"error": {
|
|
"code": -32603,
|
|
"message": str(e)
|
|
}
|
|
}
|
|
|
|
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
"""应用启动时初始化"""
|
|
try:
|
|
# 初始化Agent
|
|
initialize_agent()
|
|
|
|
logger.info("=" * 60)
|
|
logger.info("Facebook搜索Agent MCP HTTP服务器启动")
|
|
logger.info("=" * 60)
|
|
logger.info("MCP服务器已就绪,支持HTTP/SSE传输")
|
|
|
|
except Exception as e:
|
|
logger.error(f"启动失败: {e}")
|
|
raise
|
|
|
|
|
|
@app.post("/mcp")
|
|
async def mcp_http_endpoint(request: Request):
|
|
"""MCP HTTP 端点 - Streamable HTTP"""
|
|
try:
|
|
body = await request.json()
|
|
session_id = request.headers.get("x-mcp-session-id")
|
|
|
|
response = await handle_mcp_request(body, session_id)
|
|
|
|
# 如果创建了新会话,返回 session ID
|
|
if "result" in response and isinstance(response["result"], dict):
|
|
if "sessionId" not in response["result"] and session_id:
|
|
response["result"]["sessionId"] = session_id
|
|
|
|
return JSONResponse(
|
|
content=response,
|
|
headers={"x-mcp-session-id": session_id or ""}
|
|
)
|
|
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={
|
|
"jsonrpc": "2.0",
|
|
"error": {
|
|
"code": -32700,
|
|
"message": f"Parse error: {str(e)}"
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
@app.get("/mcp/sse")
|
|
async def mcp_sse_endpoint(request: Request):
|
|
"""MCP SSE 端点 - Server-Sent Events"""
|
|
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
|
|
|
|
async def event_stream() -> AsyncGenerator[str, None]:
|
|
# 发送初始连接消息
|
|
yield f"data: {json.dumps({'type': 'connection', 'sessionId': session_id})}\n\n"
|
|
|
|
# 发送工具列表
|
|
tools = list(TOOL_MAP.keys())
|
|
yield f"data: {json.dumps({'type': 'tools', 'tools': tools})}\n\n"
|
|
|
|
# 保持连接
|
|
import asyncio
|
|
while True:
|
|
await asyncio.sleep(30) # 心跳
|
|
yield f"data: {json.dumps({'type': 'ping'})}\n\n"
|
|
|
|
return StreamingResponse(
|
|
event_stream(),
|
|
media_type="text/event-stream",
|
|
headers={
|
|
"Cache-Control": "no-cache",
|
|
"Connection": "keep-alive",
|
|
"x-mcp-session-id": session_id
|
|
}
|
|
)
|
|
|
|
|
|
@app.post("/mcp/sse")
|
|
async def mcp_sse_post(request: Request):
|
|
"""MCP SSE POST 端点 - 处理 SSE 请求"""
|
|
try:
|
|
body = await request.json()
|
|
session_id = request.headers.get("x-mcp-session-id") or str(uuid.uuid4())
|
|
|
|
async def response_stream() -> AsyncGenerator[str, None]:
|
|
response = await handle_mcp_request(body, session_id)
|
|
yield f"data: {json.dumps(response)}\n\n"
|
|
|
|
return StreamingResponse(
|
|
response_stream(),
|
|
media_type="text/event-stream",
|
|
headers={
|
|
"Cache-Control": "no-cache",
|
|
"Connection": "keep-alive",
|
|
"x-mcp-session-id": session_id
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={
|
|
"jsonrpc": "2.0",
|
|
"error": {
|
|
"code": -32700,
|
|
"message": f"Parse error: {str(e)}"
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
"""健康检查"""
|
|
return {
|
|
"status": "healthy",
|
|
"service": "MCP HTTP/SSE Server - Facebook搜索Agent",
|
|
"tools_count": len(TOOL_MAP)
|
|
}
|
|
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
"""根端点"""
|
|
return {
|
|
"service": "MCP HTTP/SSE Server - Facebook搜索Agent",
|
|
"version": "1.0.0",
|
|
"endpoints": {
|
|
"mcp_http": "/mcp",
|
|
"mcp_sse": "/mcp/sse",
|
|
"health": "/health"
|
|
},
|
|
"tools": list(TOOL_MAP.keys())
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
host = os.getenv("MCP_HOST", "0.0.0.0")
|
|
port = int(os.getenv("MCP_PORT", "8001"))
|
|
|
|
print(f"🚀 MCP HTTP/SSE Server 启动中...")
|
|
print(f"📡 HTTP 端点: http://{host}:{port}/mcp")
|
|
print(f"📡 SSE 端点: http://{host}:{port}/mcp/sse")
|
|
print(f"📚 健康检查: http://{host}:{port}/health")
|
|
|
|
uvicorn.run(app, host=host, port=port)
|
|
|