Skip to content

Windows stdio MCP: git subprocesses inherit stdin and hang get_minimal_context_tool / detect_changes_tool #401

@thethereza

Description

@thethereza

Summary

On Windows, some code-review-graph MCP tools hang indefinitely when served over
stdio. The affected path appears to be internal git subprocess calls inheriting the MCP server's stdin.

This causes tools like get_minimal_context_tool and detect_changes_tool to time
out, while simpler graph-only tools continue to work.

Environment

  • OS: Windows
  • Shell: PowerShell
  • code-review-graph: 2.3.2
  • fastmcp: 2.14.7
  • Python: 3.13
  • MCP transport: stdio
  • MCP command:
[mcp_servers.code-review-graph]
command = "C:\\rcycle\\.venv-dev\\Scripts\\code-review-graph.exe"
args = ["serve", "--repo", "C:\\rcycle"]
type = "stdio"

## Symptoms

These tools work quickly:

- list_graph_stats_tool
- semantic_search_nodes_tool
- query_graph_tool
- get_impact_radius_tool

These tools hang / time out:

- get_minimal_context_tool
- detect_changes_tool

Example Codex error:

tool call failed for `code-review-graph/get_minimal_context_tool`

Caused by:
    timed out awaiting tools/call after 120s

## Reproduction

1. Start the MCP server on Windows with stdio:

C:\rcycle\.venv-dev\Scripts\code-review-graph.exe serve --repo C:\rcycle

2. Call:

{
  "name": "get_minimal_context_tool",
  "arguments": {
    "repo_root": "C:\\rcycle",
    "task": "diagnose"
  }
}

3. The request hangs.

I also reproduced this with a minimal raw JSON-RPC stdio client, outside Codex.

## Diagnosis

Direct Python calls are fast:

python -c "from code_review_graph.tools.context import get_minimal_context;
print(get_minimal_context(repo_root=r'C:\rcycle', task='test'))"

That returns in ~0.2s.

Raw MCP calls showed the handler entered get_minimal_context_tool, then stalled
inside analyze_changes(), specifically at parse_git_diff_ranges(), where this runs:

subprocess.run(
    ["git", "diff", "--unified=0", base, "--"],
    capture_output=True,
    text=True,
    encoding="utf-8",
    errors="replace",
    cwd=repo_root,
    timeout=_GIT_TIMEOUT,
)

Adding stdin=subprocess.DEVNULL made the MCP call return immediately.

## Local Patch That Fixed It

The fix was to make git subprocesses non-interactive and prevent them from inheriting
stdio MCP stdin:

def _git_env() -> dict[str, str]:
    env = os.environ.copy()
    env.setdefault("GIT_TERMINAL_PROMPT", "0")
    env.setdefault("GIT_PAGER", "cat")
    return env

Then use this for git subprocess calls:

subprocess.run(
    ["git", "diff", "--unified=0", base, "--"],
    capture_output=True,
    stdin=subprocess.DEVNULL,
    text=True,
    encoding="utf-8",
    errors="replace",
    cwd=repo_root,
    env=_git_env(),
    timeout=_GIT_TIMEOUT,
)

I applied the same pattern to git subprocess calls in:

- changes.py
- incremental.py
- tools/context.py

## Results After Patch

Fresh raw MCP stdio timings:

list_graph_stats_tool       0.005s
get_minimal_context_tool    0.106s
semantic_search_nodes_tool  0.004s
detect_changes_tool         0.048s

## Expected Behavior

MCP tools that call git should not hang the stdio transport on Windows. Git
subprocesses should run non-interactively and never inherit the MCP server stdin.

## Suggested Fix

Centralize git subprocess execution in a helper that always sets:

stdin=subprocess.DEVNULL
env includes:
  GIT_TERMINAL_PROMPT=0
  GIT_PAGER=cat

Then use that helper for all internal git calls.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions