Starlette+Uvicorn для MCP: тихие исключения на стороне сервера, невозможно даже выполнить аварийную отладку сервераPython

Программы на Python
Ответить
Anonymous
 Starlette+Uvicorn для MCP: тихие исключения на стороне сервера, невозможно даже выполнить аварийную отладку сервера

Сообщение Anonymous »

Несмотря на всю шумиху вокруг агентного ИИ, а также для того, чтобы освоить эту тему и разработать взаимосвязанные сервисы, я попытался создать локальный сервер MCP для подключения к экземпляру llama.cpp, доступ к которому осуществляется через веб-интерфейс, и запускать некоторые инструменты.
После различных трудностей разработки я остановился на решении, приведенном ниже, которое использует Starlette на Uvicorn для предоставления доступа к серверу MCP. Он работает хорошо, llama.cpp удается взаимодействовать с сервером, получать список инструментов, а локальная модель (Gemma 4) вызывает инструменты, когда ожидается.
Так в чем же моя проблема? Исключения на стороне сервера абсолютно бесшумны!
Для демонстрации я добавил явно вызываемое исключение в строке 53. При обновлении соединения с сервером MCP в llama.cpp список инструментов не загружается, что доказывает, что исключение было встречено на пути выполнения, но ничего не отображается на терминале, где был запущен сервер MCP. Операторы журнала также не отображаются, когда программа достигает исключения. WebUI также не отображает дополнительную информацию о соединении, в частности, если скрытая связь включала сообщения об ошибках.
(Примечание: я удалил как можно больше ненужных строк, включая обработку исключений и операторы журнала, но при этом воспроизвожу свою проблему.)
import contextlib
import asyncio
import json
import logging
import sys
from mcp.server import Server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from mcp.types import Tool
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette.middleware.cors import CORSMiddleware
import uvicorn
from typing import Dict

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

def read_json_file(file_name):
try:
with open(file_name, "r") as fic:
return json.load(fic)
except FileNotFoundError as exc:
raise
except json.decoder.JSONDecodeError as exc:
raise

# Sample tool function
def echo(message: str = "") -> Dict[str, str]:
return {"message": f"~~ECHO~~: {message}"}

class ToolsConf:
def __init__(self, tools_file: str = "tools.json"):
self.tools_file = tools_file
self.tools = []
self.tools_data = {}

def get_conf(self):
self.tools_data = read_json_file(self.tools_file)
self.tools = [Tool(**el) for el in self.tools_data]

def get_tool_func(self, func_name):
# Hardcode for example
return echo

class ToolExecutionService:
def __init__(self, tools_conf: ToolsConf):
self.tools_conf = tools_conf

async def list_tools(self):
raise Exception("this won't crash the server") # Toy exception
self.tools_conf.get_conf()
return self.tools_conf.tools

async def call_tool(self, name, arguments):
data_of_tool = None
for el in self.tools_conf.tools_data:
if el["name"] == name:
data_of_tool = el
break
if not data_of_tool:
raise ValueError(f"Unknown tool {name} in tools file {self.tools_conf.tools_file}")
tool_to_call = self.tools_conf.get_tool_func(name)
return tool_to_call(**arguments)

class MCPServerASGIApp:
def __init__(self, mcp_server_obj):
self.session_manager = self.get_session_manager(mcp_server_obj)

def get_session_manager(self, mcp_server_obj):
return StreamableHTTPSessionManager(
app=mcp_server_obj,
json_response=True
)

async def handle_streamable_http(self, scope, receive, send) -> None:
await self.session_manager.handle_request(scope, receive, send)

@contextlib.asynccontextmanager
async def lifespan(self, app: Starlette):
async with self.session_manager.run():
try:
yield
finally:
logger.info("Application shutting down...")

def make_asgi_app(self, mount_point):
app = Starlette(
debug=True,
routes=[Mount(mount_point, app=self.handle_streamable_http)],
lifespan=self.lifespan
)
return app

def add_cors_middleware(self, app, host):
client_port = XXXX # Obfuscated
allowed_origins = [f"http://127.0.0.1:{client_port}", f"http://localhost:{client_port}"]
starlette_app = CORSMiddleware(
app,
allow_origins=allowed_origins,
allow_methods=["GET", "POST"],
expose_headers=["Mcp-Session-Id"],
)
return starlette_app

def build(self, mount_point, host):
app = self.make_asgi_app(mount_point)
return self.add_cors_middleware(app, host)

class MyMCPServer():
def __init__(self, tools_exe_service: ToolExecutionService):
self.server = Server("my-mcp-server")
self.server.list_tools()(tools_exe_service.list_tools)
self.server.call_tool()(tools_exe_service.call_tool)

async def main(self, mount_point, host, port):
asgi_app = MCPServerASGIApp(self.server)
starlette_app = asgi_app.build(mount_point, host)
config = uvicorn.Config(starlette_app, host=host, port=port, log_level="debug", access_log=True)
server_instance = uvicorn.Server(config)
await server_instance.serve()
return 0

def entrypoint():
tools_conf = ToolsConf()
tool_exe_service = ToolExecutionService(tools_conf)
instance = MyMCPServer(tool_exe_service)
handle = instance.main("/mcp", "127.0.0.1", YYYY) # Obfuscated port
asyncio.run(handle)

if __name__ == "__main__":
entrypoint()

Конечно, удаление игрушечного исключения восстанавливает ожидаемую функциональность сервера. Но это препятствует дальнейшему развитию, так как я не могу сказать, было ли, когда и где возникло исключение или произошла какая-то другая ошибка.
Я пытался добавить общую обработку исключений для каждого вызова, который казался важным, но видимых результатов в терминале, где я запускаю сервер, не было.
Единственная видимость, которую я имею, - это PDB, который, похоже, указывает на mcp.server.lowlevel.server._handle_message и mcp.server.lowlevel.server._handle_request, которые находятся чуть выше в трассировке стека, а также включают флаг raise_Exceptions. Но я не уверен, как получить к нему доступ, тем более что я не запускаю сервер MCP напрямую через пакет mcp. Я просмотрел документацию, но не смог найти подходящей опции для переключения флага raise_Exceptions. Также просмотрел документацию Starlette и Uvicorn, но, честно говоря, я даже не уверен, где искать тонкий «чудо» вариант конфигурации.
Что касается клиентской стороны, хотя WebUI бесполезен, я попробовал использовать инспектор MCP (Github) для прямого подключения к серверу и получения дополнительной информации и обнаружил, что он получил сообщение об ошибке: Ошибка MCP 0: это не приведет к сбою сервера. Однако я понятия не имею, что означает «ошибка MCP 0», и поиск в Интернете тоже не кажется полезным. По крайней мере, я могу получить некоторую видимость с помощью этого внешнего инструмента, но все было бы намного проще, если бы ошибки были видны непосредственно из серверного процесса.
Я не слишком опытен в разработке API, поэтому могу упустить что-то очевидное, но в любом случае: Как я могу перехватывать и обрабатывать исключения сервера или, по крайней мере, получать полный сбой для целей отладки?
P.S: Версии пакетов, если это актуально
mcp 1.27.1
starlette 0.41.3
uvicorn 0.46.0
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Python»