runtime/mcp: spawn servers in their own process group

A terminal Ctrl+C is delivered to every process in the foreground process
group, so a server spawned in our group died on any Ctrl+C during a turn -
killing in-flight tool calls and leaving the connection dead until
/mcp reconnect. start_new_session (POSIX) / CREATE_NEW_PROCESS_GROUP
(Windows) puts each server in its own session/group instead.

Tests assert the spawned server's process group and session differ from the
CLI's (they used to be the same group).
This commit is contained in:
2026-09-19 11:05:33 +08:00
parent a9f77a3b96
commit 0790b5dd9f
2 changed files with 39 additions and 0 deletions
+18
View File
@@ -11,11 +11,15 @@ import asyncio
import importlib.metadata
import json
import os
import subprocess
import sys
from collections import deque
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
if TYPE_CHECKING:
from typing import Any
from plyngent.config.models import McpConfig, McpServerConfig
MCP_PROTOCOL_VERSION = "2025-06-18"
@@ -45,6 +49,19 @@ def _client_version() -> str:
return "0.0.0"
def _isolated_spawn_kwargs() -> dict[str, Any]:
"""Spawn kwargs that keep a server out of our process/console group.
Ctrl+C in the terminal is delivered to every process in the foreground
process group (POSIX) / console process group (Windows), so without this a
single Ctrl+C during a turn also killed every MCP server — killing in-flight
tool calls and leaving the connection dead until ``/mcp reconnect``.
"""
if sys.platform == "win32":
return {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP}
return {"start_new_session": True}
class McpConnectionError(RuntimeError):
"""Server process failed to spawn, handshake, or answer in time."""
@@ -143,6 +160,7 @@ class McpServerConnection:
stderr=asyncio.subprocess.PIPE,
env=os.environ | self._config.env,
cwd=self._config.cwd or None,
**_isolated_spawn_kwargs(),
)
except (OSError, ValueError) as exc:
self.error = f"spawn {self._config.command!r}: {exc}"