From 3d0352384b9a3b8fe87dd27d30e015c34f39e2be Mon Sep 17 00:00:00 2001 From: b3nw Date: Sat, 25 Apr 2026 05:20:20 +0000 Subject: [PATCH] fix: monkey-patch mcp cancellation race crash (SDK issue #2416) Patch RequestResponder.respond() and cancel() at startup to handle the race where a notifications/cancelled arrives between handler return and respond(), which crashes the session with "AssertionError: Request already responded to". Also improve build.sh to handle registry push failures gracefully and auto-restart the container after building. --- scripts/build.sh | 12 ++++++++++-- server.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/scripts/build.sh b/scripts/build.sh index df614ad..7b3872d 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -6,6 +6,7 @@ PROJECT_DIR="$(dirname "$SCRIPT_DIR")" SCRAPER_DIR="/home/b3nw/projects/financial/schwab-scraper" IMAGE="gitea.ext.ben.io/b3nw/schwab-mcp-custom:latest" BUILD_HOST="${BUILD_HOST:-docker-test}" +COMPOSE_DIR="/opt/schwab-mcp-custom" cd "$PROJECT_DIR" @@ -28,10 +29,17 @@ echo "==> Building Docker image on $BUILD_HOST..." ssh "$BUILD_HOST" "cd /tmp/schwab-mcp-build && docker build -t $IMAGE ." echo "==> Pushing image to registry..." -ssh "$BUILD_HOST" "docker push $IMAGE" +if ssh "$BUILD_HOST" "docker push $IMAGE" 2>/dev/null; then + echo " Image pushed to registry." +else + echo " Registry push failed (auth?), image available locally only." +fi + +echo "==> Restarting container..." +ssh "$BUILD_HOST" "cd $COMPOSE_DIR && docker compose up -d --force-recreate --pull never --no-deps schwab-mcp" echo "==> Cleaning up..." rm -rf vendor/schwab-scraper ssh "$BUILD_HOST" "rm -rf /tmp/schwab-mcp-build" -echo "==> Done! Image pushed: $IMAGE" +echo "==> Done! Container restarted with new image." diff --git a/server.py b/server.py index b595b3d..f6f0797 100644 --- a/server.py +++ b/server.py @@ -1,4 +1,5 @@ import json +import logging import os from typing import Optional, Any @@ -11,6 +12,40 @@ import uvicorn # Import the unified API from the schwab_scraper dependency import schwab_scraper.unified_api as api +# --------------------------------------------------------------------------- +# Monkey-patch mcp.shared.session.RequestResponder to work around a +# cancellation race in mcp==1.27.0 (github.com/modelcontextprotocol/ +# python-sdk/issues/2416). A concurrent notifications/cancelled can set +# _completed=True between handler return and respond(), crashing the session +# with "AssertionError: Request already responded to". +# Remove once upstream ships a fix (likely mcp>=1.28). +# --------------------------------------------------------------------------- +def _patch_request_responder(): + from mcp.shared.session import RequestResponder + + _orig_respond = RequestResponder.respond + + async def _safe_respond(self, response): + if self._completed: + logging.debug( + "respond() skipped for request %s — already completed (race with cancel)", + self.request_id, + ) + return + return await _orig_respond(self, response) + + _orig_cancel = RequestResponder.cancel + + async def _safe_cancel(self): + if self._completed: + return + return await _orig_cancel(self) + + RequestResponder.respond = _safe_respond + RequestResponder.cancel = _safe_cancel + +_patch_request_responder() + # Initialize FastMCP mcp = FastMCP("SchwabScraper")