mcp_server_http.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. """
  2. mcp_server_http.py — 小智 MCP 接入点(streamable-http 模式)
  3. 为什么需要这个文件:
  4. mcp_server_local.py 用 WebSocket(ws://),
  5. 小智读长 audiobook 内容时 WebSocket 经常断流,体验差。
  6. 这里改成 streamable-http(HTTP + JSON / SSE),传输长内容更稳定。
  7. 复用:
  8. audio_server.handle_request(req) 是纯函数,直接调它。
  9. 启动:
  10. .venv\Scripts\python.exe mcp_server_http.py
  11. 监听 0.0.0.0:8766(HTTP)
  12. """
  13. from __future__ import annotations
  14. import json
  15. import logging
  16. import os
  17. import uvicorn
  18. from starlette.applications import Starlette
  19. from starlette.middleware.cors import CORSMiddleware
  20. from starlette.requests import Request
  21. from starlette.responses import JSONResponse, Response
  22. from starlette.routing import Route
  23. import audio_server
  24. # 复用 audio_server 的设置
  25. API_BASE = audio_server.API_BASE
  26. API_TIMEOUT = audio_server.API_TIMEOUT
  27. logger = logging.getLogger("MCP_HTTP")
  28. async def mcp_endpoint(request: Request) -> Response:
  29. """
  30. MCP streamable-http 端点。
  31. 接收 JSON-RPC 请求,返回 JSON-RPC 响应。
  32. 注意:streamable-http 协议允许两种传输:
  33. - application/json(普通 POST)
  34. - text/event-stream(SSE,用于流式响应)
  35. 长内容场景下,建议一次性 POST 返回完整 JSON,
  36. 避免 WebSocket 长连接断开问题。
  37. """
  38. try:
  39. body = await request.json()
  40. except Exception as e:
  41. return JSONResponse(
  42. {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": f"parse error: {e}"}},
  43. status_code=400,
  44. )
  45. logger.info("<- %s", json.dumps(body, ensure_ascii=False)[:300])
  46. # 复用 audio_server 的 JSON-RPC 处理
  47. resp = audio_server.handle_request(body)
  48. if resp is None:
  49. # notification 类型,不需要响应
  50. return Response(status_code=204)
  51. logger.info("-> %s", json.dumps(resp, ensure_ascii=False)[:300])
  52. # 返回 application/json 响应(不用 SSE,避免长内容分片问题)
  53. return JSONResponse(
  54. resp,
  55. headers={
  56. "Cache-Control": "no-cache",
  57. "X-MCP-Transport": "streamable-http",
  58. },
  59. )
  60. async def health_check(request: Request) -> Response:
  61. """健康检查端点"""
  62. return JSONResponse({
  63. "status": "ok",
  64. "transport": "streamable-http",
  65. "api_base": API_BASE,
  66. })
  67. # Starlette app
  68. app = Starlette(
  69. debug=False,
  70. routes=[
  71. Route("/mcp", mcp_endpoint, methods=["POST"]),
  72. Route("/health", health_check, methods=["GET"]),
  73. ],
  74. )
  75. # CORS(小智可能从不同源访问)
  76. app.add_middleware(
  77. CORSMiddleware,
  78. allow_origins=["*"],
  79. allow_credentials=True,
  80. allow_methods=["*"],
  81. allow_headers=["*"],
  82. )
  83. def main():
  84. port = int(os.environ.get("MCP_HTTP_PORT", "8766"))
  85. logging.basicConfig(
  86. level="INFO",
  87. format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
  88. )
  89. logger.info(f"MCP HTTP Server 启动在 http://0.0.0.0:{port}/mcp")
  90. logger.info(f"API base = {API_BASE}")
  91. logger.info(f"transport = streamable-http (POST /mcp)")
  92. uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
  93. if __name__ == "__main__":
  94. main()