From a1d874b29ef23baeb134ebac33b6d04cded1c89b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 11 May 2026 19:13:11 +0200 Subject: [PATCH] Fix MCP server lifecycle issue In HTTP/SSE mode, a client disconnection triggered a partial agent shutdown (project deactivation, dashboard manager & GUI viewer shutdown) --- CHANGELOG.md | 3 ++- src/serena/cli.py | 2 +- src/serena/mcp.py | 29 ++++++++++++++++++++++++----- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceffb7d5..9b91ebee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ Status of the `main` branch. Changes prior to the next official version change w Instead, they can define `added_modes` to add modes on top of base and default modes. See updated [documentation on modes](https://oraios.github.io/serena/02-usage/050_configuration.html#modes). - Serena's default configuration now uses `interactive` and `editing` as `base_modes` instead of as `default_modes`. - - Fixed path validation in `search_for_pattern` tool (thanks to [@dodge1218](https://github.com/dodge1218) for the report) + - Fixed path validation in `search_for_pattern` tool (thanks to [@dodge1218](https://github.com/dodge1218) for the report) + - Fix: In HTTP/SSE mode, a client disconnection triggered a partial agent shutdown (project deactivation, dashboard manager & GUI viewer shutdown) * JetBrains: - Add new tools: diff --git a/src/serena/cli.py b/src/serena/cli.py index 8591d712..ee0bf173 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -362,7 +362,7 @@ class TopLevelCommands(AutoRegisteringGroup): if default_modes or added_modes: mode_selection_def = ModeSelectionDefinitionWithAddedModes(default_modes=default_modes or None, added_modes=added_modes or None) - factory = SerenaMCPFactory(context=context, project=project_file, memory_log_handler=memory_log_handler) + factory = SerenaMCPFactory(transport=transport, context=context, project=project_file, memory_log_handler=memory_log_handler) server = factory.create_mcp_server( host=host, port=port, diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 130f432a..7520cacb 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -52,14 +52,22 @@ class SerenaMCPFactory: Factory for the creation of the Serena MCP server with an associated SerenaAgent. """ - def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None, memory_log_handler: MemoryLogHandler | None = None): + def __init__( + self, + transport: Literal["stdio", "sse", "streamable-http"], + context: str = DEFAULT_CONTEXT, + project: str | None = None, + memory_log_handler: MemoryLogHandler | None = None, + ): """ + :param transport: The transport to use for the MCP server. :param context: The context name or path to context file :param project: Either an absolute path to the project directory or a name of an already registered project. If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name afterward. :param memory_log_handler: the in-memory log handler to use for the agent's logging """ + self.transport = transport self.context = SerenaAgentContext.load(context) self.project = project self.agent: SerenaAgent | None = None @@ -342,16 +350,27 @@ class SerenaMCPFactory: @asynccontextmanager async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: - """Manage server startup and shutdown lifecycle.""" + """ + Manages the lifespan of MCP server instances and performs necessary setup and teardown. + For stdio transport, there is a single server instance. + For other transports, this is called once per connection! + + :param mcp_server: the MCP server instance to configure + """ openai_tool_compatible = self.context.name in ["chatgpt", "codex", "oaicompat-agent"] self._set_mcp_tools(mcp_server, openai_tool_compatible=openai_tool_compatible) log.info("MCP server lifetime setup complete") try: yield finally: - log.info("MCP server shutting down") - if self.agent is not None: - self.agent.on_shutdown() + # Shut down the server if we are running in stdio mode. + # For other transports, we do nothing; the singleton agent instance remains active. + if self.transport == "stdio": + log.info("MCP server shutting down") + if self.agent is not None: + self.agent.on_shutdown() + else: + log.info("Client disconnected") def _get_initial_instructions(self) -> str: assert self.agent is not None