Agent SDK query(): MCP server processes remain as zombies after session ends

Resolved 💬 2 comments Opened Mar 7, 2026 by pjongyun-creater Closed Mar 7, 2026

Bug Report

Description

When using the Agent SDK's query() to orchestrate multiple sequential sessions (relay pattern), MCP server processes spawned internally by each session often survive after the session ends. This creates zombie processes that accumulate over time.

The core problem

There is no way for an external orchestrator to determine which MCP process belongs to which session. query() spawns MCP servers internally, but does not:

  1. Expose the spawned MCP process PIDs to the caller
  2. Guarantee cleanup of MCP processes when the session ends (including timeout/error cases)
  3. Provide any session-scoped tagging/grouping mechanism for spawned child processes

Reproduction steps

  1. Create a TypeScript orchestrator that calls query() in a loop (relay pattern)
  2. Each session uses MCP servers configured in .claude/mcp.json
  3. After each session ends (success, timeout, or error), check for remaining MCP processes
  4. Observe that MCP server processes (node.exe for MCP Memory, python.exe for MCP RAG, etc.) accumulate with each session

Workaround attempted

We use wmic (Windows) to snapshot all MCP-related PIDs before/after each session and kill the diff. This works for single-mission orchestration only. It fundamentally breaks for concurrent missions because:

  • Before/after PID diffs cannot distinguish which mission owns which MCP process
  • PID reuse (rare but real on Windows) can cause incorrect kills of unrelated processes

Since the SDK spawns MCP servers internally with no external tagging, there is no reliable way to associate MCP processes with their parent session from outside.

Impact

After 5-10 relay sessions, dozens of zombie node.exe (MCP Memory server) and python.exe (MCP RAG server) processes accumulate, consuming memory and potentially causing port conflicts.

Proposed solutions

Any of these would resolve the issue:

  1. query() guarantees MCP cleanup on session end (including timeout/error/max_turns cases)
  2. query() returns spawned child PIDs in the result message, so the orchestrator can clean them up
  3. Session-scoped process group (e.g., Windows Job Object or Unix process group) so all child processes can be killed as a unit
  4. Option to pass existing MCP server connections to query() instead of spawning new ones per session (also saves startup time)

Environment

  • OS: Windows 11
  • SDK: @anthropic-ai/claude-agent-sdk
  • Language: TypeScript
  • Pattern: Relay orchestrator using query() in a loop with maxTurns per session
  • MCP servers: Node.js (MCP Memory) + Python (MCP RAG)

🤖 Generated with Claude Code

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗