Process Lifecycle for start_process, interact_with_process, and force_terminate in Desktop Commander MCP

The process lifecycle in Desktop Commander MCP follows a three-stage state machine: start_process creates a persistent shell session and returns a PID, interact_with_process sends input and polls for output until a prompt or completion is detected, and force_terminate ends the session with SIGINT followed by SIGKILL.

Desktop Commander MCP manages long-lived shell and REPL sessions that persist across tool invocations, enabling interactive workflows like Python data analysis or multi-step DevOps automation. Understanding the process lifecycle for the start_process, interact_with_process, and force_terminate tools is essential for building reliable MCP server integrations that maintain state across multiple AI agent turns.

The Three-Stage Process Lifecycle

The lifecycle operates as a managed state machine within the TerminalManager class, which stores active sessions in a Map<number, TerminalSession> and tracks completed sessions separately for pagination.

Stage 1: Creating Sessions with start_process

The start_process tool in src/tools/improved-process-tools.ts spawns a new shell or REPL and prepares it for interaction.

The implementation follows these steps:

  1. Argument validationStartProcessArgsSchema parses { command, timeout_ms?, shell?, verbose_timing? }.
  2. Shell selection – If shell is omitted, the default is retrieved from configManager.getConfig() or falls back to cmd.exe on Windows and /bin/sh on Unix. See the shell-selection logic in src/terminal-manager.ts (lines 77-89).
  3. Process spawnTerminalManager.executeCommand builds a platform-specific spawn configuration using getShellSpawnArgs and calls spawn.
  4. Early-output buffering – While the child runs, the wait buffer (MAX_WAIT_OUTPUT_CHARS) accumulates the first chunk of output.
  5. Prompt detection – The system uses quickPromptPatterns regex and analyzeProcessState heuristics to determine if the process is blocked waiting for input. If a prompt appears before timeout, the tool resolves with { pid, output, isBlocked: true }.
  6. Return – The tool returns a message containing the PID, initial output, and status emoji (🔄 if waiting, if finished, otherwise).

The response construction in src/tools/improved-process-tools.ts (lines 102-108) formats the result:

return {
  content: [{
    type: "text",
    text: `Process started with PID ${result.pid} (shell: ${shellUsed})\nInitial output:\n${result.output}${statusMessage}${timingMessage}`
  }],
};

Stage 2: Interacting with Running Sessions

Once a session exists, interact_with_process sends commands and captures output without restarting the shell, preserving environment variables and working directory state.

The interaction flow in src/terminal-manager.ts and src/tools/improved-process-tools.ts works as follows:

  1. SnapshotterminalManager.captureOutputSnapshot(pid) records the current character and line count before sending input.
  2. Send inputterminalManager.sendInputToProcess(pid, input) appends a newline (if missing) and writes to the child’s stdin.
  3. Waiting loop – Every 50ms, the system fetches new output since the snapshot via getOutputSinceSnapshot. It checks quickPromptPatterns or runs analyzeProcessState to detect if the process is waiting for input or has finished. The loop exits after maxAttempts = timeout_ms / pollIntervalMs iterations.
  4. Output cleaningcleanProcessOutput strips echoed input and trims whitespace. If output exceeds config.fileReadLineLimit, it truncates with a warning.
  5. Result – Returns a status emoji (✅ or 🔄) with state message, cleaned output, and optional timing telemetry.

The final response construction (lines 640-645 in improved-process-tools.ts) returns:

return {
  content: [{
    type: "text",
    text: responseText
  }],
};

Stage 3: Terminating Sessions with force_terminate

The force_terminate tool ends sessions reliably using a two-stage kill sequence.

The termination logic in src/terminal-manager.ts (lines 172-182) handles two cases:

  • Virtual Node sessions – For node:local fallbacks (negative PIDs), the entry is simply deleted from the sessions map.
  • Real processes – The system sends SIGINT, waits 1 second, then sends SIGKILL if the process remains in this.sessions.
session.process.kill('SIGINT');
setTimeout(() => {
  if (this.sessions.has(pid)) {
    session.process.kill('SIGKILL');
  }
}, 1000);

How the Tools Work Together

The three tools form a cohesive workflow for persistent shell management:

  1. Createstart_process returns PID 1234 and stores the session in TerminalManager’s active sessions map.
  2. Interactinteract_with_process sends commands like ls -l\n, waits for prompt detection, and updates lastReadIndex automatically.
  3. Read Moreread_process_output (optional) fetches additional lines using offset-based pagination without sending new input.
  4. Finishforce_terminate clears the session from memory and kills the underlying process.

Because TerminalSession objects persist in memory, environment state (variables, current directory, loaded modules) survives across multiple interact_with_process calls, enabling true REPL-style workflows.

Practical Usage Examples

The following examples demonstrate the complete lifecycle using MCP tool invocations:

// 1. Start a Python REPL session
const start = await callTool('start_process', {
  command: 'python3 -i',
  timeout_ms: 12000,
  origin: 'ui'
});
// Returns: "Process started with PID 5678 ... 🔄 ..."

// 2. Execute Python code and wait for the REPL prompt
const interact = await callTool('interact_with_process', {
  pid: 5678,
  input: 'import pandas as pd; df = pd.read_csv("/abs/data.csv")\n',
  timeout_ms: 8000,
  wait_for_prompt: true,
  verbose_timing: false
});
// Returns: "✅ Input executed in process 5678:\n\n📤 Output:\n[...]"

// 3. Retrieve additional output after long-running operations
const more = await callTool('read_process_output', {
  pid: 5678,
  offset: 0,
  length: 200,
  timeout_ms: 3000
});
// Returns: "[Reading 42 new lines ...]"

// 4. Clean up when finished
await callTool('force_terminate', { pid: 5678 });
// Returns: "Successfully initiated termination of session 5678"

Key Source Files and Implementation

File Purpose Key Functions
src/tools/improved-process-tools.ts Public tool implementations startProcess, interactWithProcess, forceTerminate
src/terminal-manager.ts Core session management executeCommand, sendInputToProcess, captureOutputSnapshot, getOutputSinceSnapshot, forceTerminate
src/tools/schemas.ts Zod validation schemas StartProcessArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema
src/utils/process-detection.ts State heuristics analyzeProcessState, quickPromptPatterns
src/config-manager.ts Configuration defaults getConfig(), default shell resolution

Summary

  • start_process creates persistent shell sessions using TerminalManager.executeCommand, detects initial prompts via analyzeProcessState, and returns a stable PID for future interactions.
  • interact_with_process manages the running state by capturing output snapshots, sending stdin input, and polling every 50ms until detecting a prompt or completion, preserving all shell state between calls.
  • force_terminate implements reliable cleanup with a graceful SIGINT followed by SIGKILL after 1 second, or simple deletion for virtual node:local sessions.
  • The TerminalManager maintains all state in memory using Map<number, TerminalSession>, enabling long-running REPL workflows that survive across multiple MCP tool invocations.

Frequently Asked Questions

How does prompt detection work in Desktop Commander MCP?

Prompt detection uses a combination of quick regex patterns (quickPromptPatterns) and deeper process state analysis (analyzeProcessState in src/utils/process-detection.ts). The system checks output every 50ms during the waiting loop to determine if the process is blocked awaiting input, has finished execution, or is still processing.

What happens if a process hangs during interaction?

If interact_with_process exceeds the timeout_ms limit (calculated as maxAttempts = timeout_ms / pollIntervalMs), the tool returns with a timeout status. The session remains active in the TerminalManager, allowing subsequent interaction attempts or force_terminate to recover or kill the process.

Can I run multiple shell processes simultaneously?

Yes. Each call to start_process creates a distinct TerminalSession stored in the sessions Map with a unique PID. You can interact with multiple PIDs concurrently, though each interaction is synchronous and blocks until the prompt is detected or timeout occurs.

How is output buffering handled across interactions?

The captureOutputSnapshot method records the current character count before sending input, and getOutputSinceSnapshot retrieves only new output generated after that point. This incremental approach prevents duplicate data while respecting the fileReadLineLimit configuration for large outputs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →