Here's a scenario that'll feel painfully familiar if you've been building MCP servers that interact with the shell. One of your AI agents kicks off a test suite. The suite takes four minutes to complete. Your MCP client's idle timeout is sixty seconds. At second sixty, the client cancels the call. Meanwhile, the process keeps running—nobody told it to stop—while the model sits there holding a cancellation where its tests should have been.

The Fundamental Mismatch

Shell commands are inherently synchronous and blocking. You run a command, you wait for output, you're done. MCP, on the other hand, is built around an event-driven paradigm where tool calls can take unbounded time to complete. When your shell tool blocks waiting for stdout, it freezes the entire event loop. This isn't just a performance issue—it's an architectural mismatch that breaks the mental model developers rely on. The real problem emerges when you have operations that outlive their context. Your test suite is still chugging along at minute two while the MCP client has already moved on and torn down resources. The subprocess becomes an orphan, consuming CPU cycles with no handler to collect its output or react to completion signals.

Making Shell Execution Event-Driven

The solution involves decoupling your shell command from the tool call that initiated it. Instead of blocking until the process completes, you spawn it asynchronously and register handlers for stdout, stderr, and exit events. The tool returns immediately with a job identifier while the actual execution happens in the background. When output streams arrive, you send progress notifications back through MCP's notification mechanism. When the process exits, you deliver the final result package—exit code, combined output, execution duration—back to the caller. This transforms what was a blocking call into a streaming operation that respects timeouts and cancellations at every layer.

Why Progress Notifications Matter

Long-running shell operations need visibility. Without progress updates, your agent has no idea whether the command is stuck on user input, actively running, or hung waiting for network resources. Sending periodic stdout/stderr chunks as notifications gives the model something to work with and keeps the conversation alive. The key insight is that tool calls don't have to be atomic operations. MCP supports streaming responses, which means your shell tool can emit partial results in real-time while the underlying process continues executing. This mirrors how developers already think about background jobs in web applications.

Key Takeaways

  • Synchronous blocking shell commands break MCP's event-driven model and cause orphaned processes
  • Spawn subprocesses asynchronously with handlers for stdout, stderr, and exit events
  • Send progress notifications through MCP to keep the agent informed during long operations
  • Return immediately from tool calls while execution happens in the background

The Bottom Line

Stop writing shell tools that block. If your MCP server's shell interaction looks like synchronous function calls, you're building it wrong—and you'll hit this timeout wall every time you give an AI agent something real to do.