From 27e31431a0805b15f5bbefbdd053a7fdeb99d8fc Mon Sep 17 00:00:00 2001 From: guo zebin Date: Tue, 6 Jan 2026 20:01:08 +0800 Subject: [PATCH] =?UTF-8?q?fay=E6=94=AF=E6=8C=81mcp=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E8=BF=9E=E6=8E=A5=EF=BC=8C=E5=AF=B9=E5=A4=96=E6=9A=B4?= =?UTF-8?q?=E5=B9=BF=E6=92=AD=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cache_data/tmpl6qo3gps.wav | Bin 0 -> 34860 bytes core/wsa_server.py | 603 +++++++++++++++++++------------------ fay_booter.py | 43 +++ faymcp/mcp_server.py | 189 ++++++++++++ 4 files changed, 534 insertions(+), 301 deletions(-) create mode 100644 cache_data/tmpl6qo3gps.wav create mode 100644 faymcp/mcp_server.py diff --git a/cache_data/tmpl6qo3gps.wav b/cache_data/tmpl6qo3gps.wav new file mode 100644 index 0000000000000000000000000000000000000000..cc45b23eba362e9558efa3e77e787e1ef448a647 GIT binary patch literal 34860 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

hAiy_ zfB9w5qVQIEAj#?1-`t04o6!gJh*DQD!Y=&gE7TjjhKr>JE6jT=u*pEL9|3(^MjOP@ zkFh$TnaaTZsgNthdYSx+wo6;7Ad}w#&xSxhS~y$L*G&nW`O<}x?(+0%I|e80zdg3k|=7b5t#^9ez+lU@nDpCZm2t>H_6AzT@FbWkCy6gi_UkzmN}Q7`{9Y?*AW01EXjF literal 0 HcmV?d00001 diff --git a/core/wsa_server.py b/core/wsa_server.py index 99f0da5..f2d5feb 100644 --- a/core/wsa_server.py +++ b/core/wsa_server.py @@ -1,42 +1,42 @@ -from asyncio import AbstractEventLoop - -import websockets -import asyncio -import json -from abc import abstractmethod -from websockets.legacy.server import Serve - -from utils import util -from scheduler.thread_manager import MyThread - -class MyServer: - def __init__(self, host='0.0.0.0', port=10000): - self.lock = asyncio.Lock() - self.__host = host # ip - self.__port = port # 端口号 - self.__listCmd = [] # 要发送的信息的列表 - self.__clients = list() - self.__server: Serve = None - self.__event_loop: AbstractEventLoop = None - self.__running = True - self.__pending = None - self.isConnect = False - self.TIMEOUT = 3 # 设置任何超时时间为 3 秒 - self.__tasks = {} # 记录任务和开始时间的字典 - - # 接收处理 - async def __consumer_handler(self, websocket, path): - username = None - output_setting = None - try: - async for message in websocket: - await asyncio.sleep(0.01) - try: - data = json.loads(message) - username = data.get("Username") - output_setting = data.get("Output") - except json.JSONDecodeError: - pass # Ignore invalid JSON messages +from asyncio import AbstractEventLoop + +import websockets +import asyncio +import json +from abc import abstractmethod +from websockets.legacy.server import Serve + +from utils import util +from scheduler.thread_manager import MyThread + +class MyServer: + def __init__(self, host='0.0.0.0', port=10000): + self.lock = asyncio.Lock() + self.__host = host # ip + self.__port = port # 端口号 + self.__listCmd = [] # 要发送的信息的列表 + self.__clients = list() + self.__server: Serve = None + self.__event_loop: AbstractEventLoop = None + self.__running = True + self.__pending = None + self.isConnect = False + self.TIMEOUT = 3 # 设置任何超时时间为 3 秒 + self.__tasks = {} # 记录任务和开始时间的字典 + + # 接收处理 + async def __consumer_handler(self, websocket, path): + username = None + output_setting = None + try: + async for message in websocket: + await asyncio.sleep(0.01) + try: + data = json.loads(message) + username = data.get("Username") + output_setting = data.get("Output") + except json.JSONDecodeError: + pass # Ignore invalid JSON messages if username is not None or output_setting is not None: remote_address = websocket.remote_address unique_id = f"{remote_address[0]}:{remote_address[1]}" @@ -47,270 +47,271 @@ class MyServer: self.__clients[i]["username"] = username if output_setting is not None: self.__clients[i]["output"] = output_setting - await self.__consumer(message) - except websockets.exceptions.ConnectionClosedError as e: - # 从客户端列表中移除已断开的连接 - await self.remove_client(websocket) - util.printInfo(1, "User" if username is None else username, f"WebSocket 连接关闭: {e}") - - def get_client_output(self, username): - clients_with_username = [c for c in self.__clients if c.get("username") == username] - if not clients_with_username: - return False - for client in clients_with_username: - # 获取output设置,支持布尔值、字符串布尔值、数字等多种格式 - output = client.get("output", True) # 默认为True,表示需要音频 - - # 处理不同类型的输入 - if isinstance(output, bool): - if output: # 如果是True - return True - elif isinstance(output, str): - if output.lower() == 'true': # 字符串"true" - return True - elif isinstance(output, (int, float)): - if output != 0 and output != '0': # 0以外的数字 - return True - - return False - - # 发送处理 - async def __producer_handler(self, websocket, path): - while self.__running: - await asyncio.sleep(0.01) - if len(self.__listCmd) > 0: - message = await self.__producer() - if message: - username = json.loads(message).get("Username") - if username is None: - # 群发消息 - async with self.lock: - wsclients = [c["websocket"] for c in self.__clients] - tasks = [self.send_message_with_timeout(client, message, username, timeout=3) for client in wsclients] - await asyncio.gather(*tasks) - else: - # 向指定用户发送消息 - async with self.lock: - target_clients = [c["websocket"] for c in self.__clients if c.get("username") == username] - tasks = [self.send_message_with_timeout(client, message, username, timeout=3) for client in target_clients] - await asyncio.gather(*tasks) - - # 发送消息(设置超时) - async def send_message_with_timeout(self, client, message, username, timeout=3): - try: - await asyncio.wait_for(self.send_message(client, message, username), timeout=timeout) - except asyncio.TimeoutError: - util.printInfo(1, "User" if username is None else username, f"发送消息超时: 用户名 {username}") - except websockets.exceptions.ConnectionClosed as e: - # 从客户端列表中移除已断开的连接 - await self.remove_client(client) - util.printInfo(1, "User" if username is None else username, f"WebSocket 连接关闭: {e}") - - # 发送消息 - async def send_message(self, client, message, username): - try: - await client.send(message) - except websockets.exceptions.ConnectionClosed as e: - # 从客户端列表中移除已断开的连接 - await self.remove_client(client) - util.printInfo(1, "User" if username is None else username, f"WebSocket 连接关闭: {e}") - - - async def __handler(self, websocket, path): - self.isConnect = True - util.log(1,"websocket连接上:{}".format(self.__port)) - self.on_connect_handler() - remote_address = websocket.remote_address - unique_id = f"{remote_address[0]}:{remote_address[1]}" - async with self.lock: - self.__clients.append({"id" : unique_id, "websocket" : websocket, "username" : "User"}) - consumer_task = asyncio.create_task(self.__consumer_handler(websocket, path))#接收 - producer_task = asyncio.create_task(self.__producer_handler(websocket, path))#发送 - done, self.__pending = await asyncio.wait([consumer_task, producer_task], return_when=asyncio.FIRST_COMPLETED) - - for task in self.__pending: - task.cancel() - try: - await task - except asyncio.CancelledError: - pass - - # 从客户端列表中移除已断开的连接 - await self.remove_client(websocket) - util.log(1, "websocket连接断开:{}".format(unique_id)) - - async def __consumer(self, message): - self.on_revice_handler(message) - - async def __producer(self): - if len(self.__listCmd) > 0: - message = self.on_send_handler(self.__listCmd.pop(0)) - return message - else: - return None - - async def remove_client(self, websocket): - async with self.lock: - self.__clients = [c for c in self.__clients if c["websocket"] != websocket] - if len(self.__clients) == 0: - self.isConnect = False - self.on_close_handler() - - def is_connected(self, username): - if username is None: - username = "User" - if len(self.__clients) == 0: - return False - clients = [c for c in self.__clients if c["username"] == username] - if len(clients) > 0: - return True - return False - - - #Edit by xszyou on 20230113:通过继承此类来实现服务端的接收后处理逻辑 - @abstractmethod - def on_revice_handler(self, message): - pass - - #Edit by xszyou on 20230114:通过继承此类来实现服务端的连接处理逻辑 - @abstractmethod - def on_connect_handler(self): - pass - - #Edit by xszyou on 20230804:通过继承此类来实现服务端的发送前的处理逻辑 - @abstractmethod - def on_send_handler(self, message): - return message - - #Edit by xszyou on 20230816:通过继承此类来实现服务端的断开后的处理逻辑 - @abstractmethod - def on_close_handler(self): - pass - - # 创建server - def __connect(self): - self.__event_loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.__event_loop) - self.__isExecute = True - if self.__server: - util.log(1, 'server already exist') - return - self.__server = websockets.serve(self.__handler, self.__host, self.__port) - asyncio.get_event_loop().run_until_complete(self.__server) - asyncio.get_event_loop().run_forever() - - # 往要发送的命令列表中,添加命令 + await self.__consumer(message) + except websockets.exceptions.ConnectionClosedError as e: + # 从客户端列表中移除已断开的连接 + await self.remove_client(websocket) + util.printInfo(1, "User" if username is None else username, f"WebSocket 连接关闭: {e}") + + def get_client_output(self, username): + clients_with_username = [c for c in self.__clients if c.get("username") == username] + if not clients_with_username: + return False + for client in clients_with_username: + # 获取output设置,支持布尔值、字符串布尔值、数字等多种格式 + output = client.get("output", True) # 默认为True,表示需要音频 + + # 处理不同类型的输入 + if isinstance(output, bool): + if output: # 如果是True + return True + elif isinstance(output, str): + if output.lower() == 'true': # 字符串"true" + return True + elif isinstance(output, (int, float)): + if output != 0 and output != '0': # 0以外的数字 + return True + + return False + + # 发送处理 + async def __producer_handler(self, websocket, path): + while self.__running: + await asyncio.sleep(0.01) + if len(self.__listCmd) > 0: + message = await self.__producer() + if message: + username = json.loads(message).get("Username") + if username is None: + # 群发消息 + async with self.lock: + wsclients = [c["websocket"] for c in self.__clients] + tasks = [self.send_message_with_timeout(client, message, username, timeout=3) for client in wsclients] + await asyncio.gather(*tasks) + else: + # 向指定用户发送消息 + async with self.lock: + target_clients = [c["websocket"] for c in self.__clients if c.get("username") == username] + tasks = [self.send_message_with_timeout(client, message, username, timeout=3) for client in target_clients] + await asyncio.gather(*tasks) + + # 发送消息(设置超时) + async def send_message_with_timeout(self, client, message, username, timeout=3): + try: + await asyncio.wait_for(self.send_message(client, message, username), timeout=timeout) + except asyncio.TimeoutError: + util.printInfo(1, "User" if username is None else username, f"发送消息超时: 用户名 {username}") + except websockets.exceptions.ConnectionClosed as e: + # 从客户端列表中移除已断开的连接 + await self.remove_client(client) + util.printInfo(1, "User" if username is None else username, f"WebSocket 连接关闭: {e}") + + # 发送消息 + async def send_message(self, client, message, username): + try: + await client.send(message) + except websockets.exceptions.ConnectionClosed as e: + # 从客户端列表中移除已断开的连接 + await self.remove_client(client) + util.printInfo(1, "User" if username is None else username, f"WebSocket 连接关闭: {e}") + + + async def __handler(self, websocket, path): + self.isConnect = True + util.log(1,"websocket连接上:{}".format(self.__port)) + self.on_connect_handler() + remote_address = websocket.remote_address + unique_id = f"{remote_address[0]}:{remote_address[1]}" + async with self.lock: + self.__clients.append({"id" : unique_id, "websocket" : websocket, "username" : "User"}) + consumer_task = asyncio.create_task(self.__consumer_handler(websocket, path))#接收 + producer_task = asyncio.create_task(self.__producer_handler(websocket, path))#发送 + done, self.__pending = await asyncio.wait([consumer_task, producer_task], return_when=asyncio.FIRST_COMPLETED) + + for task in self.__pending: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + # 从客户端列表中移除已断开的连接 + await self.remove_client(websocket) + util.log(1, "websocket连接断开:{}".format(unique_id)) + + async def __consumer(self, message): + self.on_revice_handler(message) + + async def __producer(self): + if len(self.__listCmd) > 0: + message = self.on_send_handler(self.__listCmd.pop(0)) + return message + else: + return None + + async def remove_client(self, websocket): + async with self.lock: + self.__clients = [c for c in self.__clients if c["websocket"] != websocket] + if len(self.__clients) == 0: + self.isConnect = False + self.on_close_handler() + + def is_connected(self, username): + if username is None: + username = "User" + if len(self.__clients) == 0: + return False + clients = [c for c in self.__clients if c["username"] == username] + if len(clients) > 0: + return True + return False + + + #Edit by xszyou on 20230113:通过继承此类来实现服务端的接收后处理逻辑 + @abstractmethod + def on_revice_handler(self, message): + pass + + #Edit by xszyou on 20230114:通过继承此类来实现服务端的连接处理逻辑 + @abstractmethod + def on_connect_handler(self): + pass + + #Edit by xszyou on 20230804:通过继承此类来实现服务端的发送前的处理逻辑 + @abstractmethod + def on_send_handler(self, message): + return message + + #Edit by xszyou on 20230816:通过继承此类来实现服务端的断开后的处理逻辑 + @abstractmethod + def on_close_handler(self): + pass + + # 创建server + def __connect(self): + self.__event_loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.__event_loop) + self.__isExecute = True + if self.__server: + util.log(1, 'server already exist') + return + self.__server = websockets.serve(self.__handler, self.__host, self.__port) + asyncio.get_event_loop().run_until_complete(self.__server) + asyncio.get_event_loop().run_forever() + + # 往要发送的命令列表中,添加命令 def add_cmd(self, content): if not self.__running: return - jsonStr = json.dumps(content) + # keep unicode (emoji/中文) intact for websocket consumers + jsonStr = json.dumps(content, ensure_ascii=False) self.__listCmd.append(jsonStr) - # util.log('命令 {}'.format(content)) - - # 开启服务 - def start_server(self): - MyThread(target=self.__connect).start() - - # 关闭服务 - def stop_server(self): - self.__running = False - self.isConnect = False - if self.__server is None: - return - self.__server.close() - self.__server = None - self.__clients = [] - util.log(1, "WebSocket server stopped.") - - -#ui端server -class WebServer(MyServer): - def __init__(self, host='0.0.0.0', port=10003): - super().__init__(host, port) - - def on_revice_handler(self, message): - pass - - def on_connect_handler(self): - self.add_cmd({"panelMsg": "使用提示:Fay可以独立使用,启动数字人将自动对接。"}) - - def on_send_handler(self, message): - return message - - def on_close_handler(self): - pass - -#数字人端server -class HumanServer(MyServer): - def __init__(self, host='0.0.0.0', port=10002): - super().__init__(host, port) - - def on_revice_handler(self, message): - pass - - def on_connect_handler(self): - web_server_instance = get_web_instance() - web_server_instance.add_cmd({"is_connect": self.isConnect}) - - - def on_send_handler(self, message): - # util.log(1, '向human发送 {}'.format(message)) - if not self.isConnect: - return None - return message - - def on_close_handler(self): - web_server_instance = get_web_instance() - web_server_instance.add_cmd({"is_connect": self.isConnect}) - - - -#测试 -class TestServer(MyServer): - def __init__(self, host='0.0.0.0', port=10000): - super().__init__(host, port) - - def on_revice_handler(self, message): - print(message) - - def on_connect_handler(self): - print("连接上了") - - def on_send_handler(self, message): - return message - - def on_close_handler(self): - pass - - - -#单例 - -__instance: MyServer = None -__web_instance: MyServer = None - - -def new_instance(host='0.0.0.0', port=10002) -> MyServer: - global __instance - if __instance is None: - __instance = HumanServer(host, port) - return __instance - - -def new_web_instance(host='0.0.0.0', port=10003) -> MyServer: - global __web_instance - if __web_instance is None: - __web_instance = WebServer(host, port) - return __web_instance - - -def get_instance() -> MyServer: - return __instance - - -def get_web_instance() -> MyServer: - return __web_instance - -if __name__ == '__main__': - testServer = TestServer(host='0.0.0.0', port=10000) + # util.log('命令 {}'.format(content)) + + # 开启服务 + def start_server(self): + MyThread(target=self.__connect).start() + + # 关闭服务 + def stop_server(self): + self.__running = False + self.isConnect = False + if self.__server is None: + return + self.__server.close() + self.__server = None + self.__clients = [] + util.log(1, "WebSocket server stopped.") + + +#ui端server +class WebServer(MyServer): + def __init__(self, host='0.0.0.0', port=10003): + super().__init__(host, port) + + def on_revice_handler(self, message): + pass + + def on_connect_handler(self): + self.add_cmd({"panelMsg": "使用提示:Fay可以独立使用,启动数字人将自动对接。"}) + + def on_send_handler(self, message): + return message + + def on_close_handler(self): + pass + +#数字人端server +class HumanServer(MyServer): + def __init__(self, host='0.0.0.0', port=10002): + super().__init__(host, port) + + def on_revice_handler(self, message): + pass + + def on_connect_handler(self): + web_server_instance = get_web_instance() + web_server_instance.add_cmd({"is_connect": self.isConnect}) + + + def on_send_handler(self, message): + # util.log(1, '向human发送 {}'.format(message)) + if not self.isConnect: + return None + return message + + def on_close_handler(self): + web_server_instance = get_web_instance() + web_server_instance.add_cmd({"is_connect": self.isConnect}) + + + +#测试 +class TestServer(MyServer): + def __init__(self, host='0.0.0.0', port=10000): + super().__init__(host, port) + + def on_revice_handler(self, message): + print(message) + + def on_connect_handler(self): + print("连接上了") + + def on_send_handler(self, message): + return message + + def on_close_handler(self): + pass + + + +#单例 + +__instance: MyServer = None +__web_instance: MyServer = None + + +def new_instance(host='0.0.0.0', port=10002) -> MyServer: + global __instance + if __instance is None: + __instance = HumanServer(host, port) + return __instance + + +def new_web_instance(host='0.0.0.0', port=10003) -> MyServer: + global __web_instance + if __web_instance is None: + __web_instance = WebServer(host, port) + return __web_instance + + +def get_instance() -> MyServer: + return __instance + + +def get_web_instance() -> MyServer: + return __web_instance + +if __name__ == '__main__': + testServer = TestServer(host='0.0.0.0', port=10000) testServer.start_server() diff --git a/fay_booter.py b/fay_booter.py index 0ea0543..ca5df93 100644 --- a/fay_booter.py +++ b/fay_booter.py @@ -1,5 +1,6 @@ #核心启动模块 import time +import os import re import pyaudio import socket @@ -21,6 +22,10 @@ deviceSocketServer = None DeviceInputListenerDict = {} ngrok = None socket_service_instance = None +mcp_sse_server = None +mcp_sse_thread = None +# 是否启用内置 MCP SSE 服务器(默认关闭,需显式开启以避免端口/代理问题) +mcp_sse_enabled = True # 延迟导入fay_core def get_fay_core(): @@ -287,9 +292,25 @@ def stop(): global ngrok global socket_service_instance global deviceSocketServer + global mcp_sse_server + global mcp_sse_thread util.log(1, '正在关闭服务...') __running = False + + # 关闭 MCP SSE 服务 + try: + if mcp_sse_server is not None: + util.log(1, '正在关闭MCP SSE服务器...') + try: + mcp_sse_server.should_exit = True + except Exception: + pass + if mcp_sse_thread is not None and mcp_sse_thread.is_alive(): + mcp_sse_thread.join(timeout=2) + util.log(1, 'MCP SSE服务器已关闭') + except Exception as e: + util.log(1, f'MCP SSE服务器关闭异常: {e}') # 断开所有MCP服务连接 util.log(1, '正在断开所有MCP服务连接...') @@ -338,6 +359,8 @@ def start(): global recorderListener global __running global socket_service_instance + global mcp_sse_server + global mcp_sse_thread util.log(1, '开启服务...') __running = True @@ -375,6 +398,26 @@ def start(): #启动自动播报服务 util.log(1,'启动自动播报服务...') MyThread(target=start_auto_play_service).start() + + # 启动 MCP SSE 服务(需显式开启) + if mcp_sse_enabled: + try: + from faymcp import mcp_server as fay_mcp_server + import uvicorn + util.log(1, f"MCP SSE服务器启动中: http://{fay_mcp_server.HOST}:{fay_mcp_server.PORT}{fay_mcp_server.SSE_PATH}") + config = uvicorn.Config( + app=fay_mcp_server.app, + host=fay_mcp_server.HOST, + port=fay_mcp_server.PORT, + log_level="info" + ) + mcp_sse_server = uvicorn.Server(config) + mcp_sse_thread = MyThread(target=mcp_sse_server.run, daemon=True) + mcp_sse_thread.start() + except Exception as e: + util.log(1, f"MCP SSE服务器启动异常: {e}") + else: + util.log(1, 'MCP SSE服务器默认未开启,设 FAY_MCP_SSE_ENABLE=1 可启用') util.log(1, '服务启动完成!') diff --git a/faymcp/mcp_server.py b/faymcp/mcp_server.py new file mode 100644 index 0000000..e3a4991 --- /dev/null +++ b/faymcp/mcp_server.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Fay broadcast MCP server (SSE transport). + +暴露 `broadcast_message` 工具,将文本/音频透传到 Fay 的 `/transparent-pass`。 + +环境变量: +- FAY_BROADCAST_API 默认 http://127.0.0.1:5000/transparent-pass +- FAY_BROADCAST_USER 默认 User +- FAY_BROADCAST_TIMEOUT 默认 10 +- FAY_MCP_SSE_HOST 默认 0.0.0.0 +- FAY_MCP_SSE_PORT 默认 8765 +- FAY_MCP_SSE_PATH SSE 路径(默认 /sse) +- FAY_MCP_MSG_PATH 消息 POST 路径(默认 /messages) +""" + +import asyncio +import logging +import os +import sys +import json +from typing import Any, Dict, Tuple + +try: + from mcp.server import Server + from mcp.types import Tool, TextContent + from mcp.server.sse import SseServerTransport +except ImportError: + print("缺少 mcp 库,请先安装:pip install mcp", file=sys.stderr) + sys.exit(1) + +try: + from starlette.applications import Starlette + from starlette.responses import Response + from starlette.routing import Mount, Route +except ImportError: + print("缺少 starlette,请先安装:pip install starlette sse-starlette", file=sys.stderr) + sys.exit(1) + +try: + import uvicorn +except ImportError: + print("缺少 uvicorn,请先安装:pip install uvicorn", file=sys.stderr) + sys.exit(1) + +try: + import requests +except ImportError: + print("缺少 requests,请先安装:pip install requests", file=sys.stderr) + sys.exit(1) + + +log = logging.getLogger("fay_mcp_server") +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") + +SERVER_NAME = "fay_broadcast" + +DEFAULT_API_URL = os.environ.get("FAY_BROADCAST_API", "http://127.0.0.1:5000/transparent-pass") +DEFAULT_USER = os.environ.get("FAY_BROADCAST_USER", "User") +REQUEST_TIMEOUT = float(os.environ.get("FAY_BROADCAST_TIMEOUT", "10")) + +HOST = os.environ.get("FAY_MCP_SSE_HOST", "0.0.0.0") +PORT = int(os.environ.get("FAY_MCP_SSE_PORT", "8765")) +SSE_PATH = os.environ.get("FAY_MCP_SSE_PATH", "/sse") +MSG_PATH = os.environ.get("FAY_MCP_MSG_PATH", "/messages") + +server = Server(SERVER_NAME) +sse_transport = SseServerTransport(MSG_PATH) + + +def _text_content(text: str) -> TextContent: + try: + return TextContent(type="text", text=text) + except Exception: + return {"type": "text", "text": text} # type: ignore[return-value] + + +TOOLS: list[Tool] = [ + Tool( + name="broadcast_message", + description="通过 Fay 的 /transparent-pass 广播文本/音频(SSE 服务器)。", + inputSchema={ + "type": "object", + "properties": { + "text": {"type": "string", "description": "要广播的文本(audio_url为空时必填)"}, + "audio_url": {"type": "string", "description": "可选音频 URL"}, + "user": {"type": "string", "description": "目标用户名,默认 FAY_BROADCAST_USER 或 User"}, + }, + "required": [], + }, + ) +] + + +@server.list_tools() +async def list_tools() -> list[Tool]: + return TOOLS + + +def _parse_arguments(arguments: Dict[str, Any]) -> Tuple[str, str, str]: + text = str(arguments.get("text", "") or "").strip() + audio_url = str(arguments.get("audio_url", "") or "").strip() + user = str(arguments.get("user", "") or "").strip() or DEFAULT_USER + return text, audio_url, user + + +async def _send_broadcast(payload: Dict[str, Any]) -> Tuple[bool, str]: + def _post() -> Tuple[bool, str]: + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + resp = requests.post( + DEFAULT_API_URL, + data=body, + headers={"Content-Type": "application/json; charset=utf-8"}, + timeout=REQUEST_TIMEOUT, + ) + try: + data = resp.json() + except Exception: + data = None + + if resp.ok: + if isinstance(data, dict): + msg = data.get("message") or data.get("msg") or "" + code = data.get("code") + if isinstance(code, int) and code >= 400: + return False, msg or f"Broadcast failed with code {code}" + return True, msg or "Broadcast sent via Fay." + return True, "Broadcast sent via Fay." + + err_detail = "" + if isinstance(data, dict): + err_detail = data.get("message") or data.get("error") or data.get("msg") or "" + if not err_detail: + err_detail = resp.text + return False, f"HTTP {resp.status_code}: {err_detail}" + + try: + return await asyncio.to_thread(_post) + except Exception as e: + return False, f"{type(e).__name__}: {e}" + + +@server.call_tool() +async def call_tool(name: str, arguments: Dict[str, Any]) -> list[TextContent]: + if name != "broadcast_message": + return [_text_content(f"Unknown tool: {name}")] + + text, audio_url, user = _parse_arguments(arguments or {}) + if not text and not audio_url: + return [_text_content("Either 'text' or 'audio_url' must be provided.")] + + payload: Dict[str, Any] = {"user": user} + if text: + payload["text"] = text + if audio_url: + payload["audio"] = audio_url + + ok, message = await _send_broadcast(payload) + prefix = "success" if ok else "error" + return [_text_content(f"{prefix}: {message}")] + + +async def sse_endpoint(request): + async with sse_transport.connect_sse(request.scope, request.receive, request._send) as (read_stream, write_stream): + await server.run(read_stream, write_stream, server.create_initialization_options()) + # 客户端断开时返回空响应,避免 NoneType 问题 + return Response() + + +routes = [ + Route(SSE_PATH, sse_endpoint, methods=["GET"]), + Mount(MSG_PATH, app=sse_transport.handle_post_message), +] + +app = Starlette(routes=routes) + + +def main(): + log.info(f"SSE MCP server started at http://{HOST}:{PORT}{SSE_PATH}") + log.info(f"Message endpoint mounted at {MSG_PATH}") + uvicorn.run(app, host=HOST, port=PORT, log_level="info") + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + pass