From a134a0fc08da2cfbcb37df406a64ebc227d9da0e Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Sun, 23 Nov 2025 04:20:09 +0100 Subject: [PATCH] fix: Share vector sync state with FastMCP session lifespan via module singleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The refactor in fafeaf3 moved background tasks to Starlette server lifespan but broke nc_get_vector_sync_status because it still looked for streams in FastMCP's AppContext (lifespan_context). Add VectorSyncState module singleton to bridge the lifespans: - starlette_lifespan sets the singleton when starting background tasks - app_lifespan_basic reads from singleton and includes in AppContext - MCP tools can now access document_receive_stream for pending count 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- nextcloud_mcp_server/app.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index b4ab820..f1185d6 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -243,6 +243,25 @@ def validate_pkce_support(discovery: dict, discovery_url: str) -> None: click.echo(f"✓ PKCE support validated: {code_challenge_methods}") +@dataclass +class VectorSyncState: + """ + Module-level state for vector sync background tasks. + + This singleton bridges the Starlette server lifespan (where background tasks run) + and FastMCP session lifespans (where MCP tools need access to the streams). + """ + + document_send_stream: Optional[MemoryObjectSendStream] = None + document_receive_stream: Optional[MemoryObjectReceiveStream] = None + shutdown_event: Optional[anyio.Event] = None + scanner_wake_event: Optional[anyio.Event] = None + + +# Module-level singleton for vector sync state +_vector_sync_state = VectorSyncState() + + @dataclass class AppContext: """Application context for BasicAuth mode.""" @@ -580,8 +599,16 @@ async def app_lifespan_basic(server: FastMCP) -> AsyncIterator[AppContext]: initialize_document_processors() # Yield client context - scanner runs at server level (starlette_lifespan) + # Include vector sync state from module singleton (set by starlette_lifespan) try: - yield AppContext(client=client, storage=storage) + yield AppContext( + client=client, + storage=storage, + document_send_stream=_vector_sync_state.document_send_stream, + document_receive_stream=_vector_sync_state.document_receive_stream, + shutdown_event=_vector_sync_state.shutdown_event, + scanner_wake_event=_vector_sync_state.scanner_wake_event, + ) finally: logger.info("Shutting down BasicAuth session") await client.close() @@ -1228,6 +1255,13 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None = app.state.shutdown_event = shutdown_event app.state.scanner_wake_event = scanner_wake_event + # Also store in module singleton for FastMCP session lifespans + _vector_sync_state.document_send_stream = send_stream + _vector_sync_state.document_receive_stream = receive_stream + _vector_sync_state.shutdown_event = shutdown_event + _vector_sync_state.scanner_wake_event = scanner_wake_event + logger.info("Vector sync state stored in module singleton") + # Also share with browser_app for /app route for route in app.routes: if isinstance(route, Mount) and route.path == "/app":