Skip to content

MCP server child processes not killed on session.destroy (stdio servers accumulate per session) #1132

@PolyphonyRequiem

Description

@PolyphonyRequiem

Summary

When using the Python SDK (v0.2.2) to create multiple sessions on a single CopilotClient, each create_session(mcp_servers={...}) call spawns new stdio MCP server child processes under copilot.exe. When session.disconnect() is called (which sends session.destroy RPC), these MCP server processes are not terminated. Over many sessions, this causes hundreds of orphaned processes.

Environment

  • github-copilot-sdk: 0.2.2
  • copilot.exe: bundled with SDK
  • OS: Windows 11
  • Python: 3.12+
  • Usage: Conductor multi-agent orchestration framework

Reproduction

The SDK is used by Conductor to orchestrate multi-agent workflows. Each agent gets its own session with the same MCP server configuration:

client = CopilotClient()
await client.start()

# Agent 1
session1 = await client.create_session(
    on_permission_request=PermissionHandler.approve_all,
    mcp_servers={
        "my-server": {
            "command": "my-mcp-server",
            "tools": ["*"],
        }
    }
)
response1 = await session1.send_and_wait("Do task 1")
await session1.disconnect()  # sends session.destroy RPC

# Agent 2 - creates NEW mcp server processes, old ones still alive
session2 = await client.create_session(
    on_permission_request=PermissionHandler.approve_all,
    mcp_servers={
        "my-server": {
            "command": "my-mcp-server",
            "tools": ["*"],
        }
    }
)
response2 = await session2.send_and_wait("Do task 2")
await session2.disconnect()  # old processes STILL alive

# After N sessions: N instances of my-mcp-server running, none killed
await client.stop()  # terminate() on copilot.exe, but children survive on Windows

Observed behavior

Using Windows process monitoring (Get-CimInstance Win32_Process):

  1. After create_session #1: 1x my-mcp-server.exe child of copilot.exe
  2. After session.disconnect() #1: still 1x (not killed)
  3. After create_session Fix snapshot filename collisions on case-insensitive filesystems #2: 2x my-mcp-server.exe (new one spawned)
  4. After 24 agent turns: 24x my-mcp-server.exe under one copilot.exe

Real-world impact from a conductor workflow with 3 stdio MCP servers declared:

  • 464 node.exe processes
  • 155 twig-mcp.exe processes
  • 463 cmd.exe processes

Timeline of child process creation under copilot.exe PID 33872:

twig-mcp.exe  CreationDate: 2026-04-23 15:38:42
twig-mcp.exe  CreationDate: 2026-04-23 15:40:19
twig-mcp.exe  CreationDate: 2026-04-23 15:41:33
...
twig-mcp.exe  CreationDate: 2026-04-23 16:29:11
twig-mcp.exe  CreationDate: 2026-04-23 16:30:28

One new MCP server spawned per session creation (~2 min apart matching agent turn cadence), none killed on session.destroy.

Expected behavior

When session.disconnect() / session.destroy RPC is processed by copilot.exe, all stdio MCP server child processes that were spawned for that session should be terminated.

Additionally, client.stop()self._process.terminate() on copilot.exe should also kill all descendant processes. On Windows, Popen.terminate() calls TerminateProcess() which only kills the target process, not its children.

SDK source references

  • copilot/client.py:1183create_session() accepts mcp_servers parameter
  • copilot/client.py:1355 — MCP servers sent as mcpServers in JSON-RPC payload
  • copilot/session.py:1835disconnect() sends session.destroy RPC
  • copilot/client.py:1117-1123stop() calls terminate() then wait(5) then kill()

Suggested fix

  1. Primary: When processing session.destroy RPC, copilot.exe should terminate all stdio MCP server processes spawned for that session
  2. Secondary: Consider reusing MCP server processes across sessions on the same client when the config is identical (optimization)
  3. Tertiary: On Windows, client.stop() should use a Job Object or enumerate+kill descendant processes rather than just terminate() on copilot.exe

Workaround

Currently using a periodic cleanup watchdog script that kills accumulated MCP server children.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions