From 6b78f7d9494cf84576edd5490ce73036cb6472e3 Mon Sep 17 00:00:00 2001 From: Bjorn Schliebitz <131568482+bjornslib@users.noreply.github.com> Date: Fri, 2 Jan 2026 02:33:58 +1100 Subject: [PATCH] fix(mcp): Chain MCP lifespan with FastAPI app lifespan (#81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP server's lifespan was not being properly chained with the FastAPI app's lifespan, causing the MCP server to not start/stop correctly when mounted as a sub-application. Changes: - Create MCP app before FastAPI app to access its lifespan - Chain MCP lifespan context with FastAPI's lifespan context - Ensures MCP server lifecycle is properly managed This fix is required for the MCP server to function correctly when used with Claude Code and other MCP clients. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 --- hindsight-api/hindsight_api/api/__init__.py | 49 +++++++++++++++------ 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/hindsight-api/hindsight_api/api/__init__.py b/hindsight-api/hindsight_api/api/__init__.py index e894c6a5..1351847a 100644 --- a/hindsight-api/hindsight_api/api/__init__.py +++ b/hindsight-api/hindsight_api/api/__init__.py @@ -5,6 +5,7 @@ Provides both HTTP REST API and MCP (Model Context Protocol) server. """ import logging +from contextlib import asynccontextmanager from typing import Optional from fastapi import FastAPI @@ -45,6 +46,17 @@ def create_app( # Both HTTP and MCP app = create_app(memory, mcp_api_enabled=True) """ + mcp_app = None + + # Create MCP app first if enabled (we need its lifespan for chaining) + if mcp_api_enabled: + try: + from .mcp import create_mcp_app + mcp_app = create_mcp_app(memory=memory) + except ImportError as e: + logger.error(f"MCP server requested but dependencies not available: {e}") + logger.error("Install with: pip install hindsight-api[mcp]") + raise # Import and create HTTP API if enabled if http_api_enabled: @@ -57,20 +69,31 @@ def create_app( app = FastAPI(title="Hindsight API", version="0.0.7") logger.info("HTTP REST API disabled") - # Mount MCP server if enabled - if mcp_api_enabled: - try: - from .mcp import create_mcp_app + # Mount MCP server and chain its lifespan if enabled + if mcp_app is not None: + # Get the MCP app's underlying Starlette app for lifespan access + mcp_starlette_app = mcp_app.mcp_app - # Create MCP app with dynamic bank_id support - # Supports: /mcp/{bank_id}/sse (bank-specific SSE endpoint) - mcp_app = create_mcp_app(memory=memory) - app.mount(mcp_mount_path, mcp_app) - logger.info(f"MCP server enabled at {mcp_mount_path}/{{bank_id}}/sse") - except ImportError as e: - logger.error(f"MCP server requested but dependencies not available: {e}") - logger.error("Install with: pip install hindsight-api[mcp]") - raise + # Store the original lifespan + original_lifespan = app.router.lifespan_context + + @asynccontextmanager + async def chained_lifespan(app_instance: FastAPI): + """Chain the MCP lifespan with the main app lifespan.""" + # Start MCP lifespan first + async with mcp_starlette_app.router.lifespan_context(mcp_starlette_app): + logger.info("MCP lifespan started") + # Then start the original app lifespan + async with original_lifespan(app_instance): + yield + logger.info("MCP lifespan stopped") + + # Replace the app's lifespan with the chained version + app.router.lifespan_context = chained_lifespan + + # Mount the MCP middleware + app.mount(mcp_mount_path, mcp_app) + logger.info(f"MCP server enabled at {mcp_mount_path}/{{bank_id}}/mcp") return app