first commit
This commit is contained in:
7
py/Dockerfile
Normal file
7
py/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM python:3.10-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY py/requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
COPY py/ ./
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
23
py/docker-compose.yaml
Normal file
23
py/docker-compose.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
fastapi:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- '8123:8123'
|
||||||
|
networks:
|
||||||
|
- 1panel-network
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: '10m'
|
||||||
|
max-file: '3'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
1panel-network:
|
||||||
|
external: true
|
||||||
5
py/requirements.txt
Normal file
5
py/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi==0.110.0
|
||||||
|
uvicorn==0.27.1
|
||||||
|
websockets==12.0
|
||||||
|
mcp==1.26.0
|
||||||
|
httpx==0.27.0
|
||||||
113
py/server.py
Normal file
113
py/server.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import random
|
||||||
|
import asyncio
|
||||||
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from starlette.routing import Route, Mount
|
||||||
|
from mcp.server import Server
|
||||||
|
from mcp.server.sse import SseServerTransport
|
||||||
|
from mcp.types import Tool, TextContent
|
||||||
|
|
||||||
|
connected_clients: list[WebSocket] = []
|
||||||
|
|
||||||
|
mcp_server = Server("cherry-studio-park-mcp")
|
||||||
|
|
||||||
|
# 1. MCP SSE 传输配置
|
||||||
|
# Cherry Studio 连接 /sse,消息路由 /messages/
|
||||||
|
sse = SseServerTransport("/messages/")
|
||||||
|
|
||||||
|
async def mcp_endpoint(request: Request):
|
||||||
|
# 标准 SSE 握手,连接 MCP Server
|
||||||
|
async with sse.connect_sse(request.scope, request.receive, request._send) as (read_stream, write_stream):
|
||||||
|
await mcp_server.run(read_stream, write_stream, mcp_server.create_initialization_options())
|
||||||
|
|
||||||
|
# 2. 核心逻辑:发送告警到前端
|
||||||
|
async def send_alert(floor: str):
|
||||||
|
lights = random.sample(["f9-sphere1", "f9-sphere2", "f9-sphere3", "f9-sphere4"], k=random.randint(1, 2))
|
||||||
|
msg = {
|
||||||
|
"action": "show_alert",
|
||||||
|
"floor": floor,
|
||||||
|
"lights": lights
|
||||||
|
}
|
||||||
|
# 发送给所有连接的 Vue 客户端
|
||||||
|
for client in list(connected_clients):
|
||||||
|
try:
|
||||||
|
await client.send_json(msg)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return msg
|
||||||
|
|
||||||
|
# 3. MCP 工具定义
|
||||||
|
@mcp_server.list_tools()
|
||||||
|
async def list_tools():
|
||||||
|
return [
|
||||||
|
Tool(
|
||||||
|
name="query_crowded_floor",
|
||||||
|
description="查询哪一层出现拥挤情况,返回楼层编号",
|
||||||
|
inputSchema={"type": "object", "properties": {}, "required": []}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="show_alert_on_floor",
|
||||||
|
description="在三维平台展示告警效果,跳转到指定楼层并显示拥挤情况",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"floor": {"type": "string", "description": "楼层编号,如 F9、F8、B1 等"}
|
||||||
|
},
|
||||||
|
"required": ["floor"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
@mcp_server.call_tool()
|
||||||
|
async def call_tool(name: str, arguments: dict):
|
||||||
|
try:
|
||||||
|
if name == "query_crowded_floor":
|
||||||
|
return [TextContent(type="text", text="第九层")]
|
||||||
|
|
||||||
|
if name == "show_alert_on_floor":
|
||||||
|
floor = arguments.get("floor", "F9")
|
||||||
|
msg = await send_alert(floor)
|
||||||
|
return [TextContent(type="text", text=f"已在 {floor} 层触发告警,点亮了 {msg['lights']}")]
|
||||||
|
|
||||||
|
return [TextContent(type="text", text="未知工具")]
|
||||||
|
except Exception as e:
|
||||||
|
return [TextContent(type="text", text=f"执行失败: {str(e)}")]
|
||||||
|
|
||||||
|
# 4. FastAPI 路由注册
|
||||||
|
app = FastAPI(title="园区3D控制MCP服务")
|
||||||
|
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
||||||
|
|
||||||
|
# 注册 SSE 端点 (Cherry Studio 连接此地址)
|
||||||
|
app.add_route("/sse", mcp_endpoint, methods=["GET"])
|
||||||
|
# 别名,防止 Cherry Studio 尝试 /mcp/sse
|
||||||
|
app.add_route("/mcp/sse", mcp_endpoint, methods=["GET"])
|
||||||
|
# 挂载消息处理路由
|
||||||
|
app.mount("/messages/", sse.handle_post_message)
|
||||||
|
app.mount("/mcp/messages/", sse.handle_post_message)
|
||||||
|
|
||||||
|
# WebSocket 端点 (Vue 前端连接此地址)
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
connected_clients.append(websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await websocket.receive_text()
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
if websocket in connected_clients:
|
||||||
|
connected_clients.remove(websocket)
|
||||||
|
|
||||||
|
# HTTP 测试接口
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {"message": "MCP SSE 服务运行中", "ws_clients": len(connected_clients)}
|
||||||
|
|
||||||
|
@app.get("/trigger_alert/{floor}")
|
||||||
|
async def http_trigger(floor: str):
|
||||||
|
"""供网页直接测试触发"""
|
||||||
|
msg = await send_alert(floor)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
Reference in New Issue
Block a user