mcp_pipe.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. """
  2. mcp_pipe.py — 小智AI MCP 接入点桥接(v8 in-process)
  3. 不用 subprocess。直接 import audio_server 的工具,
  4. 在主进程里用 websockets.sync 跟 xiaozhi.me 通信。
  5. 完全绕开 Windows + Python 3.14 的 stdio pipe 怪行为。
  6. """
  7. from __future__ import annotations
  8. import json
  9. import logging
  10. import os
  11. import sys
  12. import time
  13. import websockets.sync.client as ws_sync
  14. import audio_server
  15. logger = logging.getLogger("MCP_PIPE")
  16. HERE = os.path.dirname(os.path.abspath(__file__))
  17. RECONNECT_DELAY = 5 # 断开后等 N 秒重连
  18. def run_session(endpoint: str, raw_log) -> bool:
  19. """单次 WS 会话,返回 True=正常退出(KeyboardInterrupt),False=断线需要重连"""
  20. try:
  21. ws = ws_sync.connect(endpoint, max_size=4 * 1024 * 1024)
  22. except Exception as e:
  23. logger.warning("WS connect failed: %s, will retry in %ds", e, RECONNECT_DELAY)
  24. return False
  25. logger.info("WS connected")
  26. logger.info("READY: ready to receive xiaozhi MCP requests")
  27. try:
  28. while True:
  29. try:
  30. raw = ws.recv(timeout=300)
  31. except TimeoutError:
  32. logger.info("WS recv timeout 5min, reconnecting...")
  33. return False
  34. except Exception as e:
  35. logger.warning("WS recv failed: %s", e)
  36. return False
  37. if not raw:
  38. logger.warning("WS recv empty (EOF)")
  39. return False
  40. if isinstance(raw, bytes):
  41. raw = raw.decode("utf-8", errors="replace")
  42. logger.info("RAW: %s", raw[:300])
  43. raw_log.write(f"[<-] {raw[:300]}\n")
  44. raw_log.flush()
  45. try:
  46. req = json.loads(raw)
  47. except Exception as e:
  48. logger.warning("parse err: %s", e)
  49. continue
  50. method = req.get("method")
  51. rid = req.get("id")
  52. logger.info("<- method=%s id=%s", method, rid)
  53. if rid is None and method and method.startswith("notifications/"):
  54. logger.info(" (notification, no response)")
  55. continue
  56. resp = audio_server.handle_request(req)
  57. if resp is None:
  58. continue
  59. line = (json.dumps(resp, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8")
  60. logger.info("-> response for %s id=%s (%d bytes)", method, rid, len(line))
  61. raw_log.write(f"[->] {line.decode('utf-8', errors='replace')[:300]}\n")
  62. raw_log.flush()
  63. try:
  64. ws.send(line)
  65. except Exception as e:
  66. logger.warning("WS send failed (connection likely closed): %s", e)
  67. return False
  68. except KeyboardInterrupt:
  69. return True
  70. except Exception as e:
  71. logger.warning("Unexpected error in session: %s", e)
  72. return False
  73. finally:
  74. try:
  75. ws.close()
  76. except Exception:
  77. pass
  78. return False
  79. def main() -> int:
  80. for k in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY"):
  81. os.environ.pop(k, None)
  82. endpoint = os.environ.get("MCP_ENDPOINT")
  83. if not endpoint:
  84. print("ERROR: MCP_ENDPOINT 必填")
  85. return 2
  86. logging.basicConfig(
  87. level=os.environ.get("MCP_LOG_LEVEL", "INFO"),
  88. format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
  89. )
  90. raw_log_path = os.path.join(HERE, "mcp-pipe-utf8.log")
  91. raw_log = open(raw_log_path, "a", encoding="utf-8")
  92. raw_log.write(f"=== mcp_pipe started (auto-reconnect) ===\n")
  93. raw_log.flush()
  94. logger.info("AudioBook MCP server connecting to %s", endpoint)
  95. logger.info("API base = %s", audio_server.API_BASE)
  96. while True:
  97. should_exit = run_session(endpoint, raw_log)
  98. if should_exit:
  99. logger.info("KeyboardInterrupt, exiting")
  100. break
  101. logger.info("Disconnected, reconnecting in %ds...", RECONNECT_DELAY)
  102. time.sleep(RECONNECT_DELAY)
  103. return 0
  104. if __name__ == "__main__":
  105. sys.exit(main())