fix: Share vector sync state with FastMCP session lifespan via module singleton
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 <noreply@anthropic.com>
This commit is contained in:
@@ -243,6 +243,25 @@ def validate_pkce_support(discovery: dict, discovery_url: str) -> None:
|
|||||||
click.echo(f"✓ PKCE support validated: {code_challenge_methods}")
|
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
|
@dataclass
|
||||||
class AppContext:
|
class AppContext:
|
||||||
"""Application context for BasicAuth mode."""
|
"""Application context for BasicAuth mode."""
|
||||||
@@ -580,8 +599,16 @@ async def app_lifespan_basic(server: FastMCP) -> AsyncIterator[AppContext]:
|
|||||||
initialize_document_processors()
|
initialize_document_processors()
|
||||||
|
|
||||||
# Yield client context - scanner runs at server level (starlette_lifespan)
|
# Yield client context - scanner runs at server level (starlette_lifespan)
|
||||||
|
# Include vector sync state from module singleton (set by starlette_lifespan)
|
||||||
try:
|
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:
|
finally:
|
||||||
logger.info("Shutting down BasicAuth session")
|
logger.info("Shutting down BasicAuth session")
|
||||||
await client.close()
|
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.shutdown_event = shutdown_event
|
||||||
app.state.scanner_wake_event = scanner_wake_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
|
# Also share with browser_app for /app route
|
||||||
for route in app.routes:
|
for route in app.routes:
|
||||||
if isinstance(route, Mount) and route.path == "/app":
|
if isinstance(route, Mount) and route.path == "/app":
|
||||||
|
|||||||
Reference in New Issue
Block a user