fix(mcp): wire run_powershell and MCP server for Windows/Scoop environment

- Add .mcp.json at project root (correct location for claude mcp add)
- Add mcp_env.toml: project-scoped PATH/env config for subprocess execution
- shell_runner.py: load mcp_env.toml, add stdin=DEVNULL to fix git hang
- mcp_server.py: call mcp_client.configure() at startup (fix ACCESS DENIED)
- conductor skill files: enforce run_powershell over Bash, tool use hierarchy
- CLAUDE.md: document Bash unreliability on Windows, run_powershell preference
This commit is contained in:
2026-02-28 15:00:05 -05:00
parent 60396f03f8
commit c428e4331a
7 changed files with 125 additions and 18 deletions

View File

@@ -24,11 +24,14 @@ Follow this EXACTLY per `conductor/workflow.md`:
Edit `plan.md`: change `[ ]``[~]` for the current task. Edit `plan.md`: change `[ ]``[~]` for the current task.
### 2. Research Phase (High-Signal) ### 2. Research Phase (High-Signal)
Before touching code, use context-efficient tools: Before touching code, use context-efficient tools IN THIS ORDER:
- `py_get_code_outline` or `py_get_skeleton` (via MCP tools) to map architecture 1. `py_get_code_outline` — FIRST call on any Python file. Maps functions/classes with line ranges.
- `get_git_diff` to understand recent changes 2. `py_get_skeleton` — signatures + docstrings only, no bodies
- `Grep`/`Glob` to locate symbols 3. `get_git_diff` — understand recent changes before modifying touched files
- Only `Read` full files after identifying specific target ranges 4. `Grep`/`Glob` — cross-file symbol search
5. `Read` (targeted, offset+limit only) — ONLY after outline identifies specific ranges
**NEVER** call `Read` on a full Python file >50 lines without a prior `py_get_code_outline` call.
### 3. Write Failing Tests (Red Phase — TDD) ### 3. Write Failing Tests (Red Phase — TDD)
**DELEGATE to Tier 3 Worker** — do NOT write tests yourself: **DELEGATE to Tier 3 Worker** — do NOT write tests yourself:
@@ -48,6 +51,7 @@ Run tests. Confirm they PASS. This is the Green phase.
With passing tests as safety net, refactor if needed. Rerun tests. With passing tests as safety net, refactor if needed. Rerun tests.
### 6. Verify Coverage ### 6. Verify Coverage
Use `run_powershell` MCP tool (not Bash — Bash is a mingw sandbox on Windows):
```powershell ```powershell
uv run pytest --cov=. --cov-report=term-missing {TEST_FILE} uv run pytest --cov=. --cov-report=term-missing {TEST_FILE}
``` ```

View File

@@ -10,7 +10,7 @@ STRICT SYSTEM DIRECTIVE: You are a Tier 2 Tech Lead. Focused on architectural de
Read at session start: `conductor/tech-stack.md`, `conductor/workflow.md` Read at session start: `conductor/tech-stack.md`, `conductor/workflow.md`
## Responsibilities ## Responsibilities
- Manage the execution of implementation tracks (`/conductor:implement`) - Manage the execution of implementation tracks (`/conductor-implement`)
- Ensure alignment with `tech-stack.md` and project architecture - Ensure alignment with `tech-stack.md` and project architecture
- Break down tasks into specific technical steps for Tier 3 Workers - Break down tasks into specific technical steps for Tier 3 Workers
- Maintain PERSISTENT context throughout a track's implementation phase (NO Context Amnesia) - Maintain PERSISTENT context throughout a track's implementation phase (NO Context Amnesia)
@@ -26,13 +26,47 @@ uv run python scripts\claude_mma_exec.py --role tier3-worker "[PROMPT]"
uv run python scripts\claude_mma_exec.py --role tier4-qa "[PROMPT]" uv run python scripts\claude_mma_exec.py --role tier4-qa "[PROMPT]"
``` ```
Use `@file/path.py` syntax in prompts to inject file context for the sub-agent. ### @file Syntax for Tier 3 Context Injection
`@filepath` anywhere in the prompt string is detected by `claude_mma_exec.py` and the file is automatically inlined into the Tier 3 context. Use this so Tier 3 has what it needs WITHOUT Tier 2 reading those files first.
```powershell
# Example: Tier 3 gets api_hook_client.py and the styleguide injected automatically
uv run python scripts\claude_mma_exec.py --role tier3-worker "Apply type hints to @api_hook_client.py following @conductor/code_styleguides/python.md. ..."
```
## Tool Use Hierarchy (MANDATORY — enforced order)
Claude has access to all tools and will default to familiar ones. This hierarchy OVERRIDES that default.
**For any Python file investigation, use in this order:**
1. `py_get_code_outline` — structure map (functions, classes, line ranges). Use this FIRST.
2. `py_get_skeleton` — signatures + docstrings, no bodies
3. `get_file_summary` — high-level prose summary
4. `py_get_definition` / `py_get_signature` — targeted symbol lookup
5. `Grep` / `Glob` — cross-file symbol search and pattern matching
6. `Read` (targeted, with offset/limit) — ONLY after outline identifies specific line ranges
**`run_powershell` (MCP tool)** — PRIMARY shell execution on Windows. Use for: git, tests, scan scripts, any shell command. This is native PowerShell, not bash/mingw.
**Bash** — LAST RESORT only when MCP server is not running. Bash runs in a mingw sandbox on Windows and may produce no output. Prefer `run_powershell` for everything.
## Hard Rules (Non-Negotiable)
- **NEVER** call `Read` on a file >50 lines without calling `py_get_code_outline` or `py_get_skeleton` first.
- **NEVER** write implementation code, refactor code, type hint code, or test code inline in this context. If it goes into the codebase, Tier 3 writes it.
- **NEVER** write or run inline Python scripts via Bash. If a script is needed, it already exists or Tier 3 creates it.
- **NEVER** process raw bash output for large outputs inline — write to a file and Read, or delegate to Tier 4 QA.
- **ALWAYS** use `@file` injection in Tier 3 prompts rather than reading and summarizing files yourself.
## Refactor-Heavy Tracks (Type Hints, Style Sweeps)
For tracks with no new logic — only mechanical code changes (type hints, style fixes, renames):
- **No TDD cycle required.** Skip Red/Green phases. The verification is: scan report shows 0 remaining items.
- Tier 2 role: scope the batch, write a precise Tier 3 prompt, delegate, verify with scan script.
- Batch by file group. One Tier 3 call per group (e.g., all scripts/, all simulation/).
- Verification command: `uv run python scripts\scan_all_hints.py` then read `scan_report.txt`
## Limitations ## Limitations
- Do NOT perform heavy implementation work directly — delegate to Tier 3 - Do NOT perform heavy implementation work directly — delegate to Tier 3
- Do NOT write test or implementation code directly - Do NOT write test or implementation code directly
- Minimize full file reads; use Research-First Protocol before reading files >50 lines:
- `py_get_code_outline` / `Grep` to map architecture
- `git diff` to understand recent changes
- `Glob` / `Grep` to locate symbols
- For large error logs, always spawn Tier 4 QA rather than reading raw stderr - For large error logs, always spawn Tier 4 QA rather than reading raw stderr

14
.mcp.json Normal file
View File

@@ -0,0 +1,14 @@
{
"mcpServers": {
"manual-slop": {
"type": "stdio",
"command": "C:\\Users\\Ed\\scoop\\apps\\uv\\current\\uv.exe",
"args": [
"run",
"python",
"C:\\projects\\manual_slop\\scripts\\mcp_server.py"
],
"env": {}
}
}
}

View File

@@ -16,6 +16,7 @@ This file provides guidance to Claude Code when working with this repository.
- Do NOT use bash-specific syntax (use PowerShell equivalents) - Do NOT use bash-specific syntax (use PowerShell equivalents)
- Use `uv run` for all Python execution - Use `uv run` for all Python execution
- Path separators: forward slashes work in PowerShell - Path separators: forward slashes work in PowerShell
- **Shell execution in Claude Code**: The `Bash` tool runs in a mingw sandbox on Windows and produces unreliable/empty output. Use `run_powershell` MCP tool for ALL shell commands (git, tests, scans). Bash is last-resort only when MCP server is not running.
## Session Startup Checklist ## Session Startup Checklist
**IMPORTANT**: At the start of each session: **IMPORTANT**: At the start of each session:

18
mcp_env.toml Normal file
View File

@@ -0,0 +1,18 @@
# mcp_env.toml — Environment configuration for MCP shell runner subprocesses.
# These values are injected into each run_powershell call.
# Does NOT modify Windows system/user environment or PowerShell profile.
[path]
# Directories prepended to PATH for subprocess execution.
# Add any Scoop shim directories or tool paths needed by run_powershell.
prepend = [
"C:\\Users\\Ed\\scoop\\shims",
]
[env]
# Prevent Git credential GUI/prompts from blocking non-interactive subprocesses.
GIT_TERMINAL_PROMPT = "0"
GCM_INTERACTIVE = "never"
GIT_ASKPASS = "echo"
# Ensure HOME is set for Git and MSYS2 tools.
HOME = "${USERPROFILE}"

View File

@@ -79,6 +79,9 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
async def main() -> None: async def main() -> None:
# Configure mcp_client with the project root so py_* tools are not ACCESS DENIED
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
mcp_client.configure([], extra_base_dirs=[project_root])
async with stdio_server() as (read_stream, write_stream): async with stdio_server() as (read_stream, write_stream):
await server.run( await server.run(
read_stream, read_stream,

View File

@@ -1,34 +1,67 @@
# shell_runner.py # shell_runner.py
import subprocess, shutil import os, subprocess, shutil
from pathlib import Path from pathlib import Path
from typing import Callable, Optional from typing import Callable, Optional
try:
import tomllib
except ImportError:
import tomli as tomllib # type: ignore[no-redef]
TIMEOUT_SECONDS: int = 60 TIMEOUT_SECONDS: int = 60
_ENV_CONFIG: dict = {}
def _load_env_config() -> dict:
"""Load mcp_env.toml from project root (sibling of this file or parent dir)."""
candidates = [
Path(__file__).parent / "mcp_env.toml",
Path(__file__).parent.parent / "mcp_env.toml",
]
for p in candidates:
if p.exists():
with open(p, "rb") as f:
return tomllib.load(f)
return {}
def _build_subprocess_env() -> dict[str, str]:
"""Build env dict for subprocess: current env + mcp_env.toml overrides."""
global _ENV_CONFIG
if not _ENV_CONFIG:
_ENV_CONFIG = _load_env_config()
env = os.environ.copy()
# Apply [path].prepend entries
prepend_dirs = _ENV_CONFIG.get("path", {}).get("prepend", [])
if prepend_dirs:
env["PATH"] = os.pathsep.join(prepend_dirs) + os.pathsep + env.get("PATH", "")
# Apply [env] key-value pairs, expanding ${VAR} references
for key, val in _ENV_CONFIG.get("env", {}).items():
env[key] = os.path.expandvars(str(val))
return env
def run_powershell(script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None) -> str: def run_powershell(script: str, base_dir: str, qa_callback: Optional[Callable[[str], str]] = None) -> str:
""" """
Run a PowerShell script with working directory set to base_dir. Run a PowerShell script with working directory set to base_dir.
Returns a string combining stdout, stderr, and exit code. Returns a string combining stdout, stderr, and exit code.
Environment is configured via mcp_env.toml (project root).
If qa_callback is provided and the command fails or has stderr, If qa_callback is provided and the command fails or has stderr,
the callback is called with the stderr content and its result is appended. the callback is called with the stderr content and its result is appended.
""" """
safe_dir: str = str(base_dir).replace("'", "''") safe_dir: str = str(base_dir).replace("'", "''")
full_script: str = f"Set-Location -LiteralPath '{safe_dir}'\n{script}" full_script: str = f"Set-Location -LiteralPath '{safe_dir}'\n{script}"
# Try common executable names
exe: Optional[str] = next((x for x in ["powershell.exe", "pwsh.exe", "powershell", "pwsh"] if shutil.which(x)), None) exe: Optional[str] = next((x for x in ["powershell.exe", "pwsh.exe", "powershell", "pwsh"] if shutil.which(x)), None)
if not exe: return "ERROR: Neither powershell nor pwsh found in PATH" if not exe: return "ERROR: Neither powershell nor pwsh found in PATH"
try: try:
process = subprocess.Popen( process = subprocess.Popen(
[exe, "-NoProfile", "-NonInteractive", "-Command", full_script], [exe, "-NoProfile", "-NonInteractive", "-Command", full_script],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=base_dir stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
cwd=base_dir, env=_build_subprocess_env(),
) )
stdout, stderr = process.communicate(timeout=TIMEOUT_SECONDS) stdout, stderr = process.communicate(timeout=TIMEOUT_SECONDS)
parts: list[str] = [] parts: list[str] = []
if stdout.strip(): parts.append(f"STDOUT:\n{stdout.strip()}") if stdout.strip(): parts.append(f"STDOUT:\n{stdout.strip()}")
if stderr.strip(): parts.append(f"STDERR:\n{stderr.strip()}") if stderr.strip(): parts.append(f"STDERR:\n{stderr.strip()}")
parts.append(f"EXIT CODE: {process.returncode}") parts.append(f"EXIT CODE: {process.returncode}")
if (process.returncode != 0 or stderr.strip()) and qa_callback: if (process.returncode != 0 or stderr.strip()) and qa_callback:
qa_analysis: Optional[str] = qa_callback(stderr.strip()) qa_analysis: Optional[str] = qa_callback(stderr.strip())
if qa_analysis: if qa_analysis: