Fix: Add shutdown handler for proper connection cleanup

Added GraphitiService.shutdown() and QueueService.shutdown() methods,
and registered shutdown handler in main MCP server lifecycle to ensure
proper cleanup of Neo4j connections on server exit.

Changes:
- Added GraphitiService.shutdown() method to close Neo4j driver
- Added QueueService.shutdown() method to log worker cancellation
- Wrapped run_mcp_server() in try/finally block to call shutdown
- Updated dependency to graphiti-core-varming>=0.23.2 (connection pool config)

This prevents "Response write failure" errors during server shutdown by
ensuring all database connections are properly closed before exit.

Version: graphiti-mcp-varming 1.0.6

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Lars Varming 2025-11-14 14:34:40 +01:00
parent a74bdf8321
commit 0e52b339b2
3 changed files with 91 additions and 41 deletions

View file

@ -10,7 +10,7 @@ allow-direct-references = true
[project] [project]
name = "graphiti-mcp-varming" name = "graphiti-mcp-varming"
version = "1.0.5" version = "1.0.6"
description = "Graphiti MCP Server - Enhanced fork with additional tools by Varming" description = "Graphiti MCP Server - Enhanced fork with additional tools by Varming"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10,<4" requires-python = ">=3.10,<4"
@ -31,7 +31,7 @@ classifiers = [
dependencies = [ dependencies = [
"mcp>=1.21.0", "mcp>=1.21.0",
"openai>=1.91.0", "openai>=1.91.0",
"graphiti-core-varming>=0.23.1", # Varming fork with database parameter fix (0.23.1+varming.1) "graphiti-core-varming>=0.23.2", # Varming fork with connection pool config (0.23.2)
"pydantic-settings>=2.0.0", "pydantic-settings>=2.0.0",
"pyyaml>=6.0", "pyyaml>=6.0",
] ]
@ -46,7 +46,7 @@ Issues = "https://github.com/Varming73/graphiti/issues"
[project.optional-dependencies] [project.optional-dependencies]
# FalkorDB support (Neo4j is included in graphiti-core-varming by default) # FalkorDB support (Neo4j is included in graphiti-core-varming by default)
falkordb = ["graphiti-core-varming[falkordb]>=0.23.1"] falkordb = ["graphiti-core-varming[falkordb]>=0.23.2"]
# Azure support # Azure support
azure = [ azure = [
@ -82,7 +82,7 @@ all = [
] ]
dev = [ dev = [
"graphiti-core-varming>=0.23.1", "graphiti-core-varming>=0.23.2",
"httpx>=0.28.1", "httpx>=0.28.1",
"mcp>=1.21.0", "mcp>=1.21.0",
"pyright>=1.1.404", "pyright>=1.1.404",

View file

@ -333,6 +333,29 @@ class GraphitiService:
logger.error(f'Failed to initialize Graphiti client: {e}') logger.error(f'Failed to initialize Graphiti client: {e}')
raise raise
async def shutdown(self) -> None:
"""Clean shutdown of Graphiti service.
Closes database connections and cancels queue workers.
"""
logger.info('Shutting down Graphiti service...')
try:
# Cancel all queue workers
if self.queue_service:
await self.queue_service.shutdown()
# Close Graphiti client (which closes the driver)
if self.client and self.client.driver:
logger.info('Closing Neo4j driver...')
await self.client.driver.close()
logger.info('Neo4j driver closed successfully')
except Exception as e:
logger.error(f'Error during shutdown: {e}')
finally:
logger.info('Graphiti service shutdown complete')
async def get_client(self) -> Graphiti: async def get_client(self) -> Graphiti:
"""Get the Graphiti client, initializing if necessary.""" """Get the Graphiti client, initializing if necessary."""
if self.client is None: if self.client is None:
@ -1614,7 +1637,10 @@ async def initialize_server() -> ServerConfig:
async def run_mcp_server(): async def run_mcp_server():
"""Run the MCP server in the current event loop.""" """Run the MCP server with proper lifecycle management."""
global graphiti_service
try:
# Initialize the server # Initialize the server
mcp_config = await initialize_server() mcp_config = await initialize_server()
@ -1655,6 +1681,11 @@ async def run_mcp_server():
else: else:
raise ValueError(f'Unsupported transport: {mcp_config.transport}. Use "sse" or "stdio"') raise ValueError(f'Unsupported transport: {mcp_config.transport}. Use "sse" or "stdio"')
finally:
# Always clean up on exit
if graphiti_service:
await graphiti_service.shutdown()
def main(): def main():
"""Main function to run the Graphiti MCP server.""" """Main function to run the Graphiti MCP server."""

View file

@ -98,6 +98,25 @@ class QueueService:
self._graphiti_client = graphiti_client self._graphiti_client = graphiti_client
logger.info('Queue service initialized with graphiti client') logger.info('Queue service initialized with graphiti client')
async def shutdown(self) -> None:
"""Cancel all queue workers and wait for completion."""
if not self._queue_workers:
logger.info('No queue workers to shut down')
return
logger.info(f'Shutting down {len(self._queue_workers)} queue workers...')
# Cancel all worker tasks if we have task references
# Note: Currently we don't store task references, so workers will be
# cancelled when the event loop shuts down. This is a placeholder for
# Phase 4 enhancement where we'll add task tracking.
for group_id, is_running in list(self._queue_workers.items()):
if is_running:
logger.info(f'Queue worker for {group_id} will be cancelled on shutdown')
logger.info('Queue service shutdown complete')
async def add_episode( async def add_episode(
self, self,
group_id: str, group_id: str,