Based on my analysis, here's what I found:

**Current state:**
- v2 already has a `process_manager.py` that tracks and kills the agent process + its MCP servers
- It has orphan detection with `find_orphan_processes()` and `kill_orphan_processes()`
- BUT it doesn't track processes spawned **by the project itself** (npm dev, databases, etc.)

**What needs to be added:**

1. **Project Process Tracker** - A new module to track processes created during agent sessions (when the agent runs `npm run dev`, `python manage.py runserver`, etc.)

2. **Hooks at key points:**
   - Before marking a feature as passing
   - Before moving to the next feature
   - Before ending a session
   - On agent crash/stop

3. **Smart cleanup logic:**
   - Kill dev servers between features (unless needed for next feature)
   - Keep databases running (unless explicitly testing database restart)
   - Clean everything on session end

Would you like me to implement this? The implementation would involve:

1. Creating a `ProjectProcessTracker` class that monitors bash commands and tracks PIDs
2. Modifying the prompts to instruct the agent to register/cleanup processes
3. Adding cleanup hooks in the feature MCP server before feature transitions
4. Adding a cleanup step in `agent.py` before ending sessions

Should I go ahead and build this feature?


yes


The user wants me to implement process tracking for projects being built by v2. Let me break down what needs to be done:

1. Create a `ProjectProcessTracker` class that:
   - Tracks PIDs of processes spawned by bash commands
   - Can identify which processes are dev servers vs databases vs one-off commands
   - Can selectively kill processes

2. Integrate it into the bash execution flow:
   - Hook into bash command execution to track spawned processes
   - Parse command output to extract PIDs

3. Add cleanup hooks at:
   - Feature transitions (before marking passing, before getting next feature)
   - Session end
   - Agent stop/crash

4. Modify prompts to instruct the agent about process management

Let me create a comprehensive solution:

1. First, create `server/services/project_process_tracker.py`
2. Integrate it with the bash security hook or MCP server
3. Add cleanup logic to feature MCP server
4. Update prompts to instruct agents on process cleanup
5. Add cleanup to agent.py session end

Let me start implementing this step by step.


I'll implement comprehensive project process tracking for v2. Let me create this feature step by step.


TodoWrite

Create ProjectProcessTracker class to monitor and manage project processes

Integrate tracker with MCP Feature server for cleanup hooks

Integrating tracker with MCP Feature server

Update coding prompt to include process cleanup instructions

Add session-end cleanup to agent.py

Test the process tracking and cleanup system


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Create ProjectProcessTracker class to monitor and manage project processes

Integrate tracker with MCP Feature server for cleanup hooks

Integrating tracker with MCP Feature server

Update coding prompt to include process cleanup instructions

Add session-end cleanup to agent.py

Test the process tracking and cleanup system


Now let me create the ProjectProcessTracker class. This needs to:
1. Track PIDs of processes started during agent sessions
2. Categorize them (dev server, database, one-off command)
3. Provide smart cleanup (kill dev servers, keep databases, etc.)
4. Store state in a file so it persists across agent restarts
5. Integrate with psutil to kill process trees

Let me create this file.


Write

"""
Project Process Tracker
========================

Tracks processes spawned by the project being built (dev servers, databases, etc.)
and provides cleanup functionality to prevent zombie processes.

Separate from AgentProcessManager which tracks the agent itself.
This tracks processes created BY the agent for the project under development.
"""

import json
import logging
import re
import time
from pathlib import Path
from typing import Literal

import psutil

logger = logging.getLogger(__name__)

# Process categories based on command patterns
DEV_SERVER_PATTERNS = [
    r'npm\s+run\s+dev',
    r'npm\s+start',
    r'yarn\s+dev',
    r'yarn\s+start',
    r'pnpm\s+dev',
    r'vite',
    r'next\s+dev',
    r'python.*manage\.py\s+runserver',
    r'flask\s+run',
    r'uvicorn',
    r'fastapi\s+dev',
    r'python.*-m\s+http\.server',
    r'php\s+-S',
]

DATABASE_PATTERNS = [
    r'postgres',
    r'mysqld',
    r'mongod',
    r'redis-server',
    r'sqlite3',
]

BUILD_PATTERNS = [
    r'npm\s+run\s+build',
    r'yarn\s+build',
    r'pnpm\s+build',
    r'webpack',
    r'vite\s+build',
    r'tsc\s+',
]

class ProcessInfo:
    """Information about a tracked process."""

    def __init__(
        self,
        pid: int,
        command: str,
        category: Literal["dev_server", "database", "build", "other"],
        started_at: float,
    ):
        self.pid = pid
        self.command = command
        self.category = category
        self.started_at = started_at

    def to_dict(self) -> dict:
        """Serialize to dict for JSON storage."""
        return {
            "pid": self.pid,
            "command": self.command,
            "category": self.category,
            "started_at": self.started_at,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "ProcessInfo":
        """Deserialize from dict."""
        return cls(
            pid=data["pid"],
            command=data["command"],
            category=data["category"],
            started_at=data["started_at"],
        )

    def is_alive(self) -> bool:
        """Check if the process is still running."""
        return psutil.pid_exists(self.pid)

class ProjectProcessTracker:
    """
    Tracks processes spawned during project development.

    Stores process info in {project_dir}/.project_processes.json
    """

    def __init__(self, project_dir: Path):
        """
        Initialize the tracker.

        Args:
            project_dir: The project directory being developed
        """
        self.project_dir = project_dir
        self.state_file = project_dir / ".project_processes.json"
        self.processes: dict[int, ProcessInfo] = {}
        self._load_state()

    def _load_state(self) -> None:
        """Load tracked processes from state file."""
        if not self.state_file.exists():
            return

        try:
            data = json.loads(self.state_file.read_text())
            for proc_data in data.get("processes", []):
                proc = ProcessInfo.from_dict(proc_data)
                # Only keep processes that are still alive
                if proc.is_alive():
                    self.processes[proc.pid] = proc
                else:
                    logger.debug(f"Skipping dead process from state: PID {proc.pid}")
        except Exception as e:
            logger.warning(f"Failed to load process tracker state: {e}")

    def _save_state(self) -> None:
        """Save tracked processes to state file."""
        try:
            data = {
                "processes": [p.to_dict() for p in self.processes.values()]
            }
            self.state_file.write_text(json.dumps(data, indent=2))
        except Exception as e:
            logger.warning(f"Failed to save process tracker state: {e}")

    def _categorize_command(self, command: str) -> Literal["dev_server", "database", "build", "other"]:
        """Categorize a command based on patterns."""
        cmd_lower = command.lower()

        for pattern in DEV_SERVER_PATTERNS:
            if re.search(pattern, cmd_lower):
                return "dev_server"

        for pattern in DATABASE_PATTERNS:
            if re.search(pattern, cmd_lower):
                return "database"

        for pattern in BUILD_PATTERNS:
            if re.search(pattern, cmd_lower):
                return "build"

        return "other"

    def register_process(self, pid: int, command: str) -> None:
        """
        Register a process for tracking.

        Args:
            pid: Process ID
            command: The command that spawned this process
        """
        category = self._categorize_command(command)

        proc_info = ProcessInfo(
            pid=pid,
            command=command,
            category=category,
            started_at=time.time(),
        )

        self.processes[pid] = proc_info
        self._save_state()

        logger.info(f"Registered {category} process: PID {pid} - {command[:50]}")

    def get_processes_by_category(
        self,
        category: Literal["dev_server", "database", "build", "other"]
    ) -> list[ProcessInfo]:
        """Get all processes of a specific category."""
        return [p for p in self.processes.values() if p.category == category]

    def cleanup_category(
        self,
        category: Literal["dev_server", "database", "build", "other"],
        timeout: float = 5.0
    ) -> dict:
        """
        Kill all processes of a specific category.

        Args:
            category: Category to cleanup
            timeout: Seconds to wait for graceful shutdown

        Returns:
            Dict with killed count and details
        """
        processes = self.get_processes_by_category(category)
        results = {"killed": 0, "failed": 0, "details": []}

        for proc_info in processes:
            try:
                if not proc_info.is_alive():
                    # Already dead, remove from tracking
                    del self.processes[proc_info.pid]
                    continue

                # Kill the process tree
                proc = psutil.Process(proc_info.pid)
                children = proc.children(recursive=True)

                # Terminate parent and children
                proc.terminate()
                for child in children:
                    try:
                        child.terminate()
                    except psutil.NoSuchProcess:
                        pass

                # Wait for graceful shutdown
                gone, alive = psutil.wait_procs([proc] + children, timeout=timeout)

                # Force kill survivors
                for p in alive:
                    try:
                        p.kill()
                    except psutil.NoSuchProcess:
                        pass

                # Remove from tracking
                del self.processes[proc_info.pid]
                results["killed"] += 1
                results["details"].append({
                    "pid": proc_info.pid,
                    "category": proc_info.category,
                    "command": proc_info.command[:100],
                    "status": "killed"
                })

                logger.info(f"Killed {category} process: PID {proc_info.pid}")

            except psutil.NoSuchProcess:
                # Already dead
                del self.processes[proc_info.pid]
                results["killed"] += 1
            except Exception as e:
                results["failed"] += 1
                results["details"].append({
                    "pid": proc_info.pid,
                    "error": str(e),
                    "status": "failed"
                })
                logger.warning(f"Failed to kill process {proc_info.pid}: {e}")

        self._save_state()
        return results

    def cleanup_all(self, timeout: float = 5.0) -> dict:
        """
        Kill all tracked processes.

        Args:
            timeout: Seconds to wait for graceful shutdown

        Returns:
            Dict with killed count and details by category
        """
        results = {
            "total_killed": 0,
            "total_failed": 0,
            "by_category": {}
        }

        for category in ["dev_server", "database", "build", "other"]:
            cat_results = self.cleanup_category(category, timeout)
            results["by_category"][category] = cat_results
            results["total_killed"] += cat_results["killed"]
            results["total_failed"] += cat_results["failed"]

        return results

    def cleanup_between_features(self) -> dict:
        """
        Smart cleanup between features.

        Kills dev servers and build processes, keeps databases.

        Returns:
            Dict with cleanup results
        """
        logger.info("Running smart cleanup between features...")

        results = {
            "dev_servers": self.cleanup_category("dev_server"),
            "builds": self.cleanup_category("build"),
        }

        # Don't kill databases - they should persist
        db_count = len(self.get_processes_by_category("database"))
        if db_count > 0:
            logger.info(f"Keeping {db_count} database process(es) running")

        return results

    def get_status(self) -> dict:
        """Get current status of all tracked processes."""
        # Clean up dead processes first
        dead_pids = [pid for pid, proc in self.processes.items() if not proc.is_alive()]
        for pid in dead_pids:
            del self.processes[pid]
        self._save_state()

        by_category = {}
        for category in ["dev_server", "database", "build", "other"]:
            procs = self.get_processes_by_category(category)
            by_category[category] = {
                "count": len(procs),
                "processes": [
                    {
                        "pid": p.pid,
                        "command": p.command[:100],
                        "running_for": time.time() - p.started_at,
                    }
                    for p in procs
                ]
            }

        return {
            "total_processes": len(self.processes),
            "by_category": by_category,
        }

# Global registry of trackers per project
_trackers: dict[str, ProjectProcessTracker] = {}

def get_tracker(project_dir: Path) -> ProjectProcessTracker:
    """Get or create a process tracker for a project."""
    key = str(project_dir.resolve())
    if key not in _trackers:
        _trackers[key] = ProjectProcessTracker(project_dir)
    return _trackers[key]

def cleanup_all_trackers() -> dict:
    """Clean up all tracked processes across all projects."""
    results = {"projects": {}}

    for project_path, tracker in _trackers.items():
        results["projects"][project_path] = tracker.cleanup_all()

    return results


File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\server\services\project_process_tracker.py

"""
Project Process Tracker
========================

Tracks processes spawned by the project being built (dev servers, databases, etc.)
and provides cleanup functionality to prevent zombie processes.

Separate from AgentProcessManager which tracks the agent itself.
This tracks processes created BY the agent for the project under development.
"""

import json
import logging
import re
import time
from pathlib import Path
from typing import Literal

import psutil

logger = logging.getLogger(__name__)

# Process categories based on command patterns
DEV_SERVER_PATTERNS = [
    r'npm\s+run\s+dev',
    r'npm\s+start',
    r'yarn\s+dev',
    r'yarn\s+start',
    r'pnpm\s+dev',
    r'vite',
    r'next\s+dev',
    r'python.*manage\.py\s+runserver',
    r'flask\s+run',
    r'uvicorn',
    r'fastapi\s+dev',
    r'python.*-m\s+http\.server',
    r'php\s+-S',
]

DATABASE_PATTERNS = [
    r'postgres',
    r'mysqld',
    r'mongod',
    r'redis-server',
    r'sqlite3',
]

BUILD_PATTERNS = [
    r'npm\s+run\s+build',
    r'yarn\s+build',
    r'pnpm\s+build',
    r'webpack',
    r'vite\s+build',
    r'tsc\s+',
]

class ProcessInfo:
    """Information about a tracked process."""

    def __init__(
        self,
        pid: int,
        command: str,
        category: Literal["dev_server", "database", "build", "other"],
        started_at: float,
    ):
        self.pid = pid
        self.command = command
        self.category = category
        self.started_at = started_at

    def to_dict(self) -> dict:
        """Serialize to dict for JSON storage."""
        return {
            "pid": self.pid,
            "command": self.command,
            "category": self.category,
            "started_at": self.started_at,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "ProcessInfo":
        """Deserialize from dict."""
        return cls(
            pid=data["pid"],
            command=data["command"],
            category=data["category"],
            started_at=data["started_at"],
        )

    def is_alive(self) -> bool:
        """Check if the process is still running."""
        return psutil.pid_exists(self.pid)

class ProjectProcessTracker:
    """
    Tracks processes spawned during project development.

    Stores process info in {project_dir}/.project_processes.json
    """

    def __init__(self, project_dir: Path):
        """
        Initialize the tracker.

        Args:
            project_dir: The project directory being developed
        """
        self.project_dir = project_dir
        self.state_file = project_dir / ".project_processes.json"
        self.processes: dict[int, ProcessInfo] = {}
        self._load_state()

    def _load_state(self) -> None:
        """Load tracked processes from state file."""
        if not self.state_file.exists():
            return

        try:
            data = json.loads(self.state_file.read_text())
            for proc_data in data.get("processes", []):
                proc = ProcessInfo.from_dict(proc_data)
                # Only keep processes that are still alive
                if proc.is_alive():
                    self.processes[proc.pid] = proc
                else:
                    logger.debug(f"Skipping dead process from state: PID {proc.pid}")
        except Exception as e:
            logger.warning(f"Failed to load process tracker state: {e}")

    def _save_state(self) -> None:
        """Save tracked processes to state file."""
        try:
            data = {
                "processes": [p.to_dict() for p in self.processes.values()]
            }
            self.state_file.write_text(json.dumps(data, indent=2))
        except Exception as e:
            logger.warning(f"Failed to save process tracker state: {e}")

    def _categorize_command(self, command: str) -> Literal["dev_server", "database", "build", "other"]:
        """Categorize a command based on patterns."""
        cmd_lower = command.lower()

        for pattern in DEV_SERVER_PATTERNS:
            if re.search(pattern, cmd_lower):
                return "dev_server"

        for pattern in DATABASE_PATTERNS:
            if re.search(pattern, cmd_lower):
                return "database"

        for pattern in BUILD_PATTERNS:
            if re.search(pattern, cmd_lower):
                return "build"

        return "other"

    def register_process(self, pid: int, command: str) -> None:
        """
        Register a process for tracking.

        Args:
            pid: Process ID
            command: The command that spawned this process
        """
        category = self._categorize_command(command)

        proc_info = ProcessInfo(
            pid=pid,
            command=command,
            category=category,
            started_at=time.time(),
        )

        self.processes[pid] = proc_info
        self._save_state()

        logger.info(f"Registered {category} process: PID {pid} - {command[:50]}")

    def get_processes_by_category(
        self,
        category: Literal["dev_server", "database", "build", "other"]
    ) -> list[ProcessInfo]:
        """Get all processes of a specific category."""
        return [p for p in self.processes.values() if p.category == category]

    def cleanup_category(
        self,
        category: Literal["dev_server", "database", "build", "other"],
        timeout: float = 5.0
    ) -> dict:
        """
        Kill all processes of a specific category.

        Args:
            category: Category to cleanup
            timeout: Seconds to wait for graceful shutdown

        Returns:
            Dict with killed count and details
        """
        processes = self.get_processes_by_category(category)
        results = {"killed": 0, "failed": 0, "details": []}

        for proc_info in processes:
            try:
                if not proc_info.is_alive():
                    # Already dead, remove from tracking
                    del self.processes[proc_info.pid]
                    continue

                # Kill the process tree
                proc = psutil.Process(proc_info.pid)
                children = proc.children(recursive=True)

                # Terminate parent and children
                proc.terminate()
                for child in children:
                    try:
                        child.terminate()
                    except psutil.NoSuchProcess:
                        pass

                # Wait for graceful shutdown
                gone, alive = psutil.wait_procs([proc] + children, timeout=timeout)

                # Force kill survivors
                for p in alive:
                    try:
                        p.kill()
                    except psutil.NoSuchProcess:
                        pass

                # Remove from tracking
                del self.processes[proc_info.pid]
                results["killed"] += 1
                results["details"].append({
                    "pid": proc_info.pid,
                    "category": proc_info.category,
                    "command": proc_info.command[:100],
                    "status": "killed"
                })

                logger.info(f"Killed {category} process: PID {proc_info.pid}")

            except psutil.NoSuchProcess:
                # Already dead
                del self.processes[proc_info.pid]
                results["killed"] += 1
            except Exception as e:
                results["failed"] += 1
                results["details"].append({
                    "pid": proc_info.pid,
                    "error": str(e),
                    "status": "failed"
                })
                logger.warning(f"Failed to kill process {proc_info.pid}: {e}")

        self._save_state()
        return results

    def cleanup_all(self, timeout: float = 5.0) -> dict:
        """
        Kill all tracked processes.

        Args:
            timeout: Seconds to wait for graceful shutdown

        Returns:
            Dict with killed count and details by category
        """
        results = {
            "total_killed": 0,
            "total_failed": 0,
            "by_category": {}
        }

        for category in ["dev_server", "database", "build", "other"]:
            cat_results = self.cleanup_category(category, timeout)
            results["by_category"][category] = cat_results
            results["total_killed"] += cat_results["killed"]
            results["total_failed"] += cat_results["failed"]

        return results

    def cleanup_between_features(self) -> dict:
        """
        Smart cleanup between features.

        Kills dev servers and build processes, keeps databases.

        Returns:
            Dict with cleanup results
        """
        logger.info("Running smart cleanup between features...")

        results = {
            "dev_servers": self.cleanup_category("dev_server"),
            "builds": self.cleanup_category("build"),
        }

        # Don't kill databases - they should persist
        db_count = len(self.get_processes_by_category("database"))
        if db_count > 0:
            logger.info(f"Keeping {db_count} database process(es) running")

        return results

    def get_status(self) -> dict:
        """Get current status of all tracked processes."""
        # Clean up dead processes first
        dead_pids = [pid for pid, proc in self.processes.items() if not proc.is_alive()]
        for pid in dead_pids:
            del self.processes[pid]
        self._save_state()

        by_category = {}
        for category in ["dev_server", "database", "build", "other"]:
            procs = self.get_processes_by_category(category)
            by_category[category] = {
                "count": len(procs),
                "processes": [
                    {
                        "pid": p.pid,
                        "command": p.command[:100],
                        "running_for": time.time() - p.started_at,
                    }
                    for p in procs
                ]
            }

        return {
            "total_processes": len(self.processes),
            "by_category": by_category,
        }

# Global registry of trackers per project
_trackers: dict[str, ProjectProcessTracker] = {}

def get_tracker(project_dir: Path) -> ProjectProcessTracker:
    """Get or create a process tracker for a project."""
    key = str(project_dir.resolve())
    if key not in _trackers:
        _trackers[key] = ProjectProcessTracker(project_dir)
    return _trackers[key]

def cleanup_all_trackers() -> dict:
    """Clean up all tracked processes across all projects."""
    results = {"projects": {}}

    for project_path, tracker in _trackers.items():
        results["projects"][project_path] = tracker.cleanup_all()

    return results


Good! Now I need to integrate this with the feature MCP server to add cleanup hooks. Let me read the feature MCP server to see how to add the cleanup logic.


     1→#!/usr/bin/env python3
     2→"""
     3→MCP Server for Feature Management
     4→==================================
     5→
     6→Provides tools to manage features in the autonomous coding system,
     7→replacing the previous FastAPI-based REST API.
     8→
     9→Tools:
    10→- feature_get_stats: Get progress statistics
    11→- feature_get_next: Get next feature to implement
    12→- feature_get_for_regression: Get random passing features for testing
    13→- feature_mark_passing: Mark a feature as passing
    14→- feature_skip: Skip a feature (move to end of queue)
    15→- feature_mark_in_progress: Mark a feature as in-progress
    16→- feature_clear_in_progress: Clear in-progress status
    17→- feature_create_bulk: Create multiple features at once
    18→"""
    19→
    20→import json
    21→import os
    22→import sys
    23→from contextlib import asynccontextmanager
    24→from pathlib import Path
    25→from typing import Annotated
    26→
    27→from mcp.server.fastmcp import FastMCP
    28→from pydantic import BaseModel, Field
    29→from sqlalchemy.sql.expression import func
    30→
    31→# Add parent directory to path so we can import from api module
    32→sys.path.insert(0, str(Path(__file__).parent.parent))
    33→
    34→from api.database import Feature, create_database
    35→from api.migration import migrate_json_to_sqlite
    36→
    37→# Configuration from environment
    38→PROJECT_DIR = Path(os.environ.get("PROJECT_DIR", ".")).resolve()
    39→
    40→
    41→# Pydantic models for input validation
    42→class MarkPassingInput(BaseModel):
    43→    """Input for marking a feature as passing."""
    44→    feature_id: int = Field(..., description="The ID of the feature to mark as passing", ge=1)
    45→
    46→
    47→class SkipFeatureInput(BaseModel):
    48→    """Input for skipping a feature."""
    49→    feature_id: int = Field(..., description="The ID of the feature to skip", ge=1)
    50→
    51→
    52→class MarkInProgressInput(BaseModel):
    53→    """Input for marking a feature as in-progress."""
    54→    feature_id: int = Field(..., description="The ID of the feature to mark as in-progress", ge=1)
    55→
    56→
    57→class ClearInProgressInput(BaseModel):
    58→    """Input for clearing in-progress status."""
    59→    feature_id: int = Field(..., description="The ID of the feature to clear in-progress status", ge=1)
    60→
    61→
    62→class RegressionInput(BaseModel):
    63→    """Input for getting regression features."""
    64→    limit: int = Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")
    65→
    66→
    67→class FeatureCreateItem(BaseModel):
    68→    """Schema for creating a single feature."""
    69→    category: str = Field(..., min_length=1, max_length=100, description="Feature category")
    70→    name: str = Field(..., min_length=1, max_length=255, description="Feature name")
    71→    description: str = Field(..., min_length=1, description="Detailed description")
    72→    steps: list[str] = Field(..., min_length=1, description="Implementation/test steps")
    73→
    74→
    75→class BulkCreateInput(BaseModel):
    76→    """Input for bulk creating features."""
    77→    features: list[FeatureCreateItem] = Field(..., min_length=1, description="List of features to create")
    78→
    79→
    80→# Global database session maker (initialized on startup)
    81→_session_maker = None
    82→_engine = None
    83→
    84→
    85→@asynccontextmanager
    86→async def server_lifespan(server: FastMCP):
    87→    """Initialize database on startup, cleanup on shutdown."""
    88→    global _session_maker, _engine
    89→
    90→    # Create project directory if it doesn't exist
    91→    PROJECT_DIR.mkdir(parents=True, exist_ok=True)
    92→
    93→    # Initialize database
    94→    _engine, _session_maker = create_database(PROJECT_DIR)
    95→
    96→    # Run migration if needed (converts legacy JSON to SQLite)
    97→    migrate_json_to_sqlite(PROJECT_DIR, _session_maker)
    98→
    99→    yield
   100→
   101→    # Cleanup
   102→    if _engine:
   103→        _engine.dispose()
   104→
   105→
   106→# Initialize the MCP server
   107→mcp = FastMCP("features", lifespan=server_lifespan)
   108→
   109→
   110→def get_session():
   111→    """Get a new database session."""
   112→    if _session_maker is None:
   113→        raise RuntimeError("Database not initialized")
   114→    return _session_maker()
   115→
   116→
   117→@mcp.tool()
   118→def feature_get_stats() -> str:
   119→    """Get statistics about feature completion progress.
   120→
   121→    Returns the number of passing features, in-progress features, total features,
   122→    and completion percentage. Use this to track overall progress of the implementation.
   123→
   124→    Returns:
   125→        JSON with: passing (int), in_progress (int), total (int), percentage (float)
   126→    """
   127→    session = get_session()
   128→    try:
   129→        total = session.query(Feature).count()
   130→        passing = session.query(Feature).filter(Feature.passes == True).count()
   131→        in_progress = session.query(Feature).filter(Feature.in_progress == True).count()
   132→        percentage = round((passing / total) * 100, 1) if total > 0 else 0.0
   133→
   134→        return json.dumps({
   135→            "passing": passing,
   136→            "in_progress": in_progress,
   137→            "total": total,
   138→            "percentage": percentage
   139→        }, indent=2)
   140→    finally:
   141→        session.close()
   142→
   143→
   144→@mcp.tool()
   145→def feature_get_next() -> str:
   146→    """Get the highest-priority pending feature to work on.
   147→
   148→    Returns the feature with the lowest priority number that has passes=false.
   149→    Use this at the start of each coding session to determine what to implement next.
   150→
   151→    Returns:
   152→        JSON with feature details (id, priority, category, name, description, steps, passes, in_progress)
   153→        or error message if all features are passing.
   154→    """
   155→    session = get_session()
   156→    try:
   157→        feature = (
   158→            session.query(Feature)
   159→            .filter(Feature.passes == False)
   160→            .order_by(Feature.priority.asc(), Feature.id.asc())
   161→            .first()
   162→        )
   163→
   164→        if feature is None:
   165→            return json.dumps({"error": "All features are passing! No more work to do."})
   166→
   167→        return json.dumps(feature.to_dict(), indent=2)
   168→    finally:
   169→        session.close()
   170→
   171→
   172→@mcp.tool()
   173→def feature_get_for_regression(
   174→    limit: Annotated[int, Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")] = 3
   175→) -> str:
   176→    """Get random passing features for regression testing.
   177→
   178→    Returns a random selection of features that are currently passing.
   179→    Use this to verify that previously implemented features still work
   180→    after making changes.
   181→
   182→    Args:
   183→        limit: Maximum number of features to return (1-10, default 3)
   184→
   185→    Returns:
   186→        JSON with: features (list of feature objects), count (int)
   187→    """
   188→    session = get_session()
   189→    try:
   190→        features = (
   191→            session.query(Feature)
   192→            .filter(Feature.passes == True)
   193→            .order_by(func.random())
   194→            .limit(limit)
   195→            .all()
   196→        )
   197→
   198→        return json.dumps({
   199→            "features": [f.to_dict() for f in features],
   200→            "count": len(features)
   201→        }, indent=2)
   202→    finally:
   203→        session.close()
   204→
   205→
   206→@mcp.tool()
   207→def feature_mark_passing(
   208→    feature_id: Annotated[int, Field(description="The ID of the feature to mark as passing", ge=1)]
   209→) -> str:
   210→    """Mark a feature as passing after successful implementation.
   211→
   212→    Updates the feature's passes field to true and clears the in_progress flag.
   213→    Use this after you have implemented the feature and verified it works correctly.
   214→
   215→    Args:
   216→        feature_id: The ID of the feature to mark as passing
   217→
   218→    Returns:
   219→        JSON with the updated feature details, or error if not found.
   220→    """
   221→    session = get_session()
   222→    try:
   223→        feature = session.query(Feature).filter(Feature.id == feature_id).first()
   224→
   225→        if feature is None:
   226→            return json.dumps({"error": f"Feature with ID {feature_id} not found"})
   227→
   228→        feature.passes = True
   229→        feature.in_progress = False
   230→        session.commit()
   231→        session.refresh(feature)
   232→
   233→        return json.dumps(feature.to_dict(), indent=2)
   234→    finally:
   235→        session.close()
   236→
   237→
   238→@mcp.tool()
   239→def feature_skip(
   240→    feature_id: Annotated[int, Field(description="The ID of the feature to skip", ge=1)]
   241→) -> str:
   242→    """Skip a feature by moving it to the end of the priority queue.
   243→
   244→    Use this when a feature cannot be implemented yet due to:
   245→    - Dependencies on other features that aren't implemented yet
   246→    - External blockers (missing assets, unclear requirements)
   247→    - Technical prerequisites that need to be addressed first
   248→
   249→    The feature's priority is set to max_priority + 1, so it will be
   250→    worked on after all other pending features. Also clears the in_progress
   251→    flag so the feature returns to "pending" status.
   252→
   253→    Args:
   254→        feature_id: The ID of the feature to skip
   255→
   256→    Returns:
   257→        JSON with skip details: id, name, old_priority, new_priority, message
   258→    """
   259→    session = get_session()
   260→    try:
   261→        feature = session.query(Feature).filter(Feature.id == feature_id).first()
   262→
   263→        if feature is None:
   264→            return json.dumps({"error": f"Feature with ID {feature_id} not found"})
   265→
   266→        if feature.passes:
   267→            return json.dumps({"error": "Cannot skip a feature that is already passing"})
   268→
   269→        old_priority = feature.priority
   270→
   271→        # Get max priority and set this feature to max + 1
   272→        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
   273→        new_priority = (max_priority_result[0] + 1) if max_priority_result else 1
   274→
   275→        feature.priority = new_priority
   276→        feature.in_progress = False
   277→        session.commit()
   278→        session.refresh(feature)
   279→
   280→        return json.dumps({
   281→            "id": feature.id,
   282→            "name": feature.name,
   283→            "old_priority": old_priority,
   284→            "new_priority": new_priority,
   285→            "message": f"Feature '{feature.name}' moved to end of queue"
   286→        }, indent=2)
   287→    finally:
   288→        session.close()
   289→
   290→
   291→@mcp.tool()
   292→def feature_mark_in_progress(
   293→    feature_id: Annotated[int, Field(description="The ID of the feature to mark as in-progress", ge=1)]
   294→) -> str:
   295→    """Mark a feature as in-progress. Call immediately after feature_get_next().
   296→
   297→    This prevents other agent sessions from working on the same feature.
   298→    Use this as soon as you retrieve a feature to work on.
   299→
   300→    Args:
   301→        feature_id: The ID of the feature to mark as in-progress
   302→
   303→    Returns:
   304→        JSON with the updated feature details, or error if not found or already in-progress.
   305→    """
   306→    session = get_session()
   307→    try:
   308→        feature = session.query(Feature).filter(Feature.id == feature_id).first()
   309→
   310→        if feature is None:
   311→            return json.dumps({"error": f"Feature with ID {feature_id} not found"})
   312→
   313→        if feature.passes:
   314→            return json.dumps({"error": f"Feature with ID {feature_id} is already passing"})
   315→
   316→        if feature.in_progress:
   317→            return json.dumps({"error": f"Feature with ID {feature_id} is already in-progress"})
   318→
   319→        feature.in_progress = True
   320→        session.commit()
   321→        session.refresh(feature)
   322→
   323→        return json.dumps(feature.to_dict(), indent=2)
   324→    finally:
   325→        session.close()
   326→
   327→
   328→@mcp.tool()
   329→def feature_clear_in_progress(
   330→    feature_id: Annotated[int, Field(description="The ID of the feature to clear in-progress status", ge=1)]
   331→) -> str:
   332→    """Clear in-progress status from a feature.
   333→
   334→    Use this when abandoning a feature or manually unsticking a stuck feature.
   335→    The feature will return to the pending queue.
   336→
   337→    Args:
   338→        feature_id: The ID of the feature to clear in-progress status
   339→
   340→    Returns:
   341→        JSON with the updated feature details, or error if not found.
   342→    """
   343→    session = get_session()
   344→    try:
   345→        feature = session.query(Feature).filter(Feature.id == feature_id).first()
   346→
   347→        if feature is None:
   348→            return json.dumps({"error": f"Feature with ID {feature_id} not found"})
   349→
   350→        feature.in_progress = False
   351→        session.commit()
   352→        session.refresh(feature)
   353→
   354→        return json.dumps(feature.to_dict(), indent=2)
   355→    finally:
   356→        session.close()
   357→
   358→
   359→@mcp.tool()
   360→def feature_create_bulk(
   361→    features: Annotated[list[dict], Field(description="List of features to create, each with category, name, description, and steps")]
   362→) -> str:
   363→    """Create multiple features in a single operation.
   364→
   365→    Features are assigned sequential priorities based on their order.
   366→    All features start with passes=false.
   367→
   368→    This is typically used by the initializer agent to set up the initial
   369→    feature list from the app specification.
   370→
   371→    Args:
   372→        features: List of features to create, each with:
   373→            - category (str): Feature category
   374→            - name (str): Feature name
   375→            - description (str): Detailed description
   376→            - steps (list[str]): Implementation/test steps
   377→
   378→    Returns:
   379→        JSON with: created (int) - number of features created
   380→    """
   381→    session = get_session()
   382→    try:
   383→        # Get the starting priority
   384→        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
   385→        start_priority = (max_priority_result[0] + 1) if max_priority_result else 1
   386→
   387→        created_count = 0
   388→        for i, feature_data in enumerate(features):
   389→            # Validate required fields
   390→            if not all(key in feature_data for key in ["category", "name", "description", "steps"]):
   391→                return json.dumps({
   392→                    "error": f"Feature at index {i} missing required fields (category, name, description, steps)"
   393→                })
   394→
   395→            db_feature = Feature(
   396→                priority=start_priority + i,
   397→                category=feature_data["category"],
   398→                name=feature_data["name"],
   399→                description=feature_data["description"],
   400→                steps=feature_data["steps"],
   401→                passes=False,
   402→            )
   403→            session.add(db_feature)
   404→            created_count += 1
   405→
   406→        session.commit()
   407→
   408→        return json.dumps({"created": created_count}, indent=2)
   409→    except Exception as e:
   410→        session.rollback()
   411→        return json.dumps({"error": str(e)})
   412→    finally:
   413→        session.close()
   414→
   415→
   416→if __name__ == "__main__":
   417→    mcp.run()
   418→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

#!/usr/bin/env python3
"""
MCP Server for Feature Management
==================================

Provides tools to manage features in the autonomous coding system,
replacing the previous FastAPI-based REST API.

Tools:
- feature_get_stats: Get progress statistics
- feature_get_next: Get next feature to implement
- feature_get_for_regression: Get random passing features for testing
- feature_mark_passing: Mark a feature as passing
- feature_skip: Skip a feature (move to end of queue)
- feature_mark_in_progress: Mark a feature as in-progress
- feature_clear_in_progress: Clear in-progress status
- feature_create_bulk: Create multiple features at once
"""

import json
import os
import sys
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Annotated

from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
from sqlalchemy.sql.expression import func

# Add parent directory to path so we can import from api module
sys.path.insert(0, str(Path(__file__).parent.parent))

from api.database import Feature, create_database
from api.migration import migrate_json_to_sqlite

# Configuration from environment
PROJECT_DIR = Path(os.environ.get("PROJECT_DIR", ".")).resolve()

# Pydantic models for input validation
class MarkPassingInput(BaseModel):
    """Input for marking a feature as passing."""
    feature_id: int = Field(..., description="The ID of the feature to mark as passing", ge=1)

class SkipFeatureInput(BaseModel):
    """Input for skipping a feature."""
    feature_id: int = Field(..., description="The ID of the feature to skip", ge=1)

class MarkInProgressInput(BaseModel):
    """Input for marking a feature as in-progress."""
    feature_id: int = Field(..., description="The ID of the feature to mark as in-progress", ge=1)

class ClearInProgressInput(BaseModel):
    """Input for clearing in-progress status."""
    feature_id: int = Field(..., description="The ID of the feature to clear in-progress status", ge=1)

class RegressionInput(BaseModel):
    """Input for getting regression features."""
    limit: int = Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")

class FeatureCreateItem(BaseModel):
    """Schema for creating a single feature."""
    category: str = Field(..., min_length=1, max_length=100, description="Feature category")
    name: str = Field(..., min_length=1, max_length=255, description="Feature name")
    description: str = Field(..., min_length=1, description="Detailed description")
    steps: list[str] = Field(..., min_length=1, description="Implementation/test steps")

class BulkCreateInput(BaseModel):
    """Input for bulk creating features."""
    features: list[FeatureCreateItem] = Field(..., min_length=1, description="List of features to create")

# Global database session maker (initialized on startup)
_session_maker = None
_engine = None

@asynccontextmanager
async def server_lifespan(server: FastMCP):
    """Initialize database on startup, cleanup on shutdown."""
    global _session_maker, _engine

    # Create project directory if it doesn't exist
    PROJECT_DIR.mkdir(parents=True, exist_ok=True)

    # Initialize database
    _engine, _session_maker = create_database(PROJECT_DIR)

    # Run migration if needed (converts legacy JSON to SQLite)
    migrate_json_to_sqlite(PROJECT_DIR, _session_maker)

    yield

    # Cleanup
    if _engine:
        _engine.dispose()

# Initialize the MCP server
mcp = FastMCP("features", lifespan=server_lifespan)

def get_session():
    """Get a new database session."""
    if _session_maker is None:
        raise RuntimeError("Database not initialized")
    return _session_maker()

@mcp.tool()
def feature_get_stats() -> str:
    """Get statistics about feature completion progress.

    Returns the number of passing features, in-progress features, total features,
    and completion percentage. Use this to track overall progress of the implementation.

    Returns:
        JSON with: passing (int), in_progress (int), total (int), percentage (float)
    """
    session = get_session()
    try:
        total = session.query(Feature).count()
        passing = session.query(Feature).filter(Feature.passes == True).count()
        in_progress = session.query(Feature).filter(Feature.in_progress == True).count()
        percentage = round((passing / total) * 100, 1) if total > 0 else 0.0

        return json.dumps({
            "passing": passing,
            "in_progress": in_progress,
            "total": total,
            "percentage": percentage
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_get_next() -> str:
    """Get the highest-priority pending feature to work on.

    Returns the feature with the lowest priority number that has passes=false.
    Use this at the start of each coding session to determine what to implement next.

    Returns:
        JSON with feature details (id, priority, category, name, description, steps, passes, in_progress)
        or error message if all features are passing.
    """
    session = get_session()
    try:
        feature = (
            session.query(Feature)
            .filter(Feature.passes == False)
            .order_by(Feature.priority.asc(), Feature.id.asc())
            .first()
        )

        if feature is None:
            return json.dumps({"error": "All features are passing! No more work to do."})

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_get_for_regression(
    limit: Annotated[int, Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")] = 3
) -> str:
    """Get random passing features for regression testing.

    Returns a random selection of features that are currently passing.
    Use this to verify that previously implemented features still work
    after making changes.

    Args:
        limit: Maximum number of features to return (1-10, default 3)

    Returns:
        JSON with: features (list of feature objects), count (int)
    """
    session = get_session()
    try:
        features = (
            session.query(Feature)
            .filter(Feature.passes == True)
            .order_by(func.random())
            .limit(limit)
            .all()
        )

        return json.dumps({
            "features": [f.to_dict() for f in features],
            "count": len(features)
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_mark_passing(
    feature_id: Annotated[int, Field(description="The ID of the feature to mark as passing", ge=1)]
) -> str:
    """Mark a feature as passing after successful implementation.

    Updates the feature's passes field to true and clears the in_progress flag.
    Use this after you have implemented the feature and verified it works correctly.

    Args:
        feature_id: The ID of the feature to mark as passing

    Returns:
        JSON with the updated feature details, or error if not found.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        feature.passes = True
        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_skip(
    feature_id: Annotated[int, Field(description="The ID of the feature to skip", ge=1)]
) -> str:
    """Skip a feature by moving it to the end of the priority queue.

    Use this when a feature cannot be implemented yet due to:
    - Dependencies on other features that aren't implemented yet
    - External blockers (missing assets, unclear requirements)
    - Technical prerequisites that need to be addressed first

    The feature's priority is set to max_priority + 1, so it will be
    worked on after all other pending features. Also clears the in_progress
    flag so the feature returns to "pending" status.

    Args:
        feature_id: The ID of the feature to skip

    Returns:
        JSON with skip details: id, name, old_priority, new_priority, message
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        if feature.passes:
            return json.dumps({"error": "Cannot skip a feature that is already passing"})

        old_priority = feature.priority

        # Get max priority and set this feature to max + 1
        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
        new_priority = (max_priority_result[0] + 1) if max_priority_result else 1

        feature.priority = new_priority
        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps({
            "id": feature.id,
            "name": feature.name,
            "old_priority": old_priority,
            "new_priority": new_priority,
            "message": f"Feature '{feature.name}' moved to end of queue"
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_mark_in_progress(
    feature_id: Annotated[int, Field(description="The ID of the feature to mark as in-progress", ge=1)]
) -> str:
    """Mark a feature as in-progress. Call immediately after feature_get_next().

    This prevents other agent sessions from working on the same feature.
    Use this as soon as you retrieve a feature to work on.

    Args:
        feature_id: The ID of the feature to mark as in-progress

    Returns:
        JSON with the updated feature details, or error if not found or already in-progress.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        if feature.passes:
            return json.dumps({"error": f"Feature with ID {feature_id} is already passing"})

        if feature.in_progress:
            return json.dumps({"error": f"Feature with ID {feature_id} is already in-progress"})

        feature.in_progress = True
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_clear_in_progress(
    feature_id: Annotated[int, Field(description="The ID of the feature to clear in-progress status", ge=1)]
) -> str:
    """Clear in-progress status from a feature.

    Use this when abandoning a feature or manually unsticking a stuck feature.
    The feature will return to the pending queue.

    Args:
        feature_id: The ID of the feature to clear in-progress status

    Returns:
        JSON with the updated feature details, or error if not found.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_create_bulk(
    features: Annotated[list[dict], Field(description="List of features to create, each with category, name, description, and steps")]
) -> str:
    """Create multiple features in a single operation.

    Features are assigned sequential priorities based on their order.
    All features start with passes=false.

    This is typically used by the initializer agent to set up the initial
    feature list from the app specification.

    Args:
        features: List of features to create, each with:
            - category (str): Feature category
            - name (str): Feature name
            - description (str): Detailed description
            - steps (list[str]): Implementation/test steps

    Returns:
        JSON with: created (int) - number of features created
    """
    session = get_session()
    try:
        # Get the starting priority
        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
        start_priority = (max_priority_result[0] + 1) if max_priority_result else 1

        created_count = 0
        for i, feature_data in enumerate(features):
            # Validate required fields
            if not all(key in feature_data for key in ["category", "name", "description", "steps"]):
                return json.dumps({
                    "error": f"Feature at index {i} missing required fields (category, name, description, steps)"
                })

            db_feature = Feature(
                priority=start_priority + i,
                category=feature_data["category"],
                name=feature_data["name"],
                description=feature_data["description"],
                steps=feature_data["steps"],
                passes=False,
            )
            session.add(db_feature)
            created_count += 1

        session.commit()

        return json.dumps({"created": created_count}, indent=2)
    except Exception as e:
        session.rollback()
        return json.dumps({"error": str(e)})
    finally:
        session.close()

if __name__ == "__main__":
    mcp.run()


Good, now I need to add cleanup hooks to the feature_mark_passing and feature_skip functions. I'll modify the feature MCP server to call the process tracker's cleanup_between_features() method before marking features as passing or skipping them.

I also need to add a new MCP tool for manual process cleanup that agents can call if needed.


Edit

from api.database import Feature, create_database
from api.migration import migrate_json_to_sqlite

# Configuration from environment
PROJECT_DIR = Path(os.environ.get("PROJECT_DIR", ".")).resolve()

from api.database import Feature, create_database
from api.migration import migrate_json_to_sqlite

# Import process tracker (only if server module is available)
try:
    from server.services.project_process_tracker import get_tracker
    PROCESS_TRACKER_AVAILABLE = True
except ImportError:
    PROCESS_TRACKER_AVAILABLE = False

# Configuration from environment
PROJECT_DIR = Path(os.environ.get("PROJECT_DIR", ".")).resolve()


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\mcp_server\feature_mcp.py has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
    30→
    31→# Add parent directory to path so we can import from api module
    32→sys.path.insert(0, str(Path(__file__).parent.parent))
    33→
    34→from api.database import Feature, create_database
    35→from api.migration import migrate_json_to_sqlite
    36→
    37→# Import process tracker (only if server module is available)
    38→try:
    39→    from server.services.project_process_tracker import get_tracker
    40→    PROCESS_TRACKER_AVAILABLE = True
    41→except ImportError:
    42→    PROCESS_TRACKER_AVAILABLE = False
    43→
    44→# Configuration from environment
    45→PROJECT_DIR = Path(os.environ.get("PROJECT_DIR", ".")).resolve()
    46→
    47→
    48→# Pydantic models for input validation
    49→class MarkPassingInput(BaseModel):

from api.database import Feature, create_database
from api.migration import migrate_json_to_sqlite

# Import process tracker (only if server module is available)
try:
    from server.services.project_process_tracker import get_tracker
    PROCESS_TRACKER_AVAILABLE = True
except ImportError:
    PROCESS_TRACKER_AVAILABLE = False

# Configuration from environment
PROJECT_DIR = Path(os.environ.get("PROJECT_DIR", ".")).resolve()

#!/usr/bin/env python3
"""
MCP Server for Feature Management
==================================

Provides tools to manage features in the autonomous coding system,
replacing the previous FastAPI-based REST API.

Tools:
- feature_get_stats: Get progress statistics
- feature_get_next: Get next feature to implement
- feature_get_for_regression: Get random passing features for testing
- feature_mark_passing: Mark a feature as passing
- feature_skip: Skip a feature (move to end of queue)
- feature_mark_in_progress: Mark a feature as in-progress
- feature_clear_in_progress: Clear in-progress status
- feature_create_bulk: Create multiple features at once
"""

import json
import os
import sys
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Annotated

from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
from sqlalchemy.sql.expression import func

# Add parent directory to path so we can import from api module
sys.path.insert(0, str(Path(__file__).parent.parent))

from api.database import Feature, create_database
from api.migration import migrate_json_to_sqlite

# Configuration from environment
PROJECT_DIR = Path(os.environ.get("PROJECT_DIR", ".")).resolve()

# Pydantic models for input validation
class MarkPassingInput(BaseModel):
    """Input for marking a feature as passing."""
    feature_id: int = Field(..., description="The ID of the feature to mark as passing", ge=1)

class SkipFeatureInput(BaseModel):
    """Input for skipping a feature."""
    feature_id: int = Field(..., description="The ID of the feature to skip", ge=1)

class MarkInProgressInput(BaseModel):
    """Input for marking a feature as in-progress."""
    feature_id: int = Field(..., description="The ID of the feature to mark as in-progress", ge=1)

class ClearInProgressInput(BaseModel):
    """Input for clearing in-progress status."""
    feature_id: int = Field(..., description="The ID of the feature to clear in-progress status", ge=1)

class RegressionInput(BaseModel):
    """Input for getting regression features."""
    limit: int = Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")

class FeatureCreateItem(BaseModel):
    """Schema for creating a single feature."""
    category: str = Field(..., min_length=1, max_length=100, description="Feature category")
    name: str = Field(..., min_length=1, max_length=255, description="Feature name")
    description: str = Field(..., min_length=1, description="Detailed description")
    steps: list[str] = Field(..., min_length=1, description="Implementation/test steps")

class BulkCreateInput(BaseModel):
    """Input for bulk creating features."""
    features: list[FeatureCreateItem] = Field(..., min_length=1, description="List of features to create")

# Global database session maker (initialized on startup)
_session_maker = None
_engine = None

@asynccontextmanager
async def server_lifespan(server: FastMCP):
    """Initialize database on startup, cleanup on shutdown."""
    global _session_maker, _engine

    # Create project directory if it doesn't exist
    PROJECT_DIR.mkdir(parents=True, exist_ok=True)

    # Initialize database
    _engine, _session_maker = create_database(PROJECT_DIR)

    # Run migration if needed (converts legacy JSON to SQLite)
    migrate_json_to_sqlite(PROJECT_DIR, _session_maker)

    yield

    # Cleanup
    if _engine:
        _engine.dispose()

# Initialize the MCP server
mcp = FastMCP("features", lifespan=server_lifespan)

def get_session():
    """Get a new database session."""
    if _session_maker is None:
        raise RuntimeError("Database not initialized")
    return _session_maker()

@mcp.tool()
def feature_get_stats() -> str:
    """Get statistics about feature completion progress.

    Returns the number of passing features, in-progress features, total features,
    and completion percentage. Use this to track overall progress of the implementation.

    Returns:
        JSON with: passing (int), in_progress (int), total (int), percentage (float)
    """
    session = get_session()
    try:
        total = session.query(Feature).count()
        passing = session.query(Feature).filter(Feature.passes == True).count()
        in_progress = session.query(Feature).filter(Feature.in_progress == True).count()
        percentage = round((passing / total) * 100, 1) if total > 0 else 0.0

        return json.dumps({
            "passing": passing,
            "in_progress": in_progress,
            "total": total,
            "percentage": percentage
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_get_next() -> str:
    """Get the highest-priority pending feature to work on.

    Returns the feature with the lowest priority number that has passes=false.
    Use this at the start of each coding session to determine what to implement next.

    Returns:
        JSON with feature details (id, priority, category, name, description, steps, passes, in_progress)
        or error message if all features are passing.
    """
    session = get_session()
    try:
        feature = (
            session.query(Feature)
            .filter(Feature.passes == False)
            .order_by(Feature.priority.asc(), Feature.id.asc())
            .first()
        )

        if feature is None:
            return json.dumps({"error": "All features are passing! No more work to do."})

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_get_for_regression(
    limit: Annotated[int, Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")] = 3
) -> str:
    """Get random passing features for regression testing.

    Returns a random selection of features that are currently passing.
    Use this to verify that previously implemented features still work
    after making changes.

    Args:
        limit: Maximum number of features to return (1-10, default 3)

    Returns:
        JSON with: features (list of feature objects), count (int)
    """
    session = get_session()
    try:
        features = (
            session.query(Feature)
            .filter(Feature.passes == True)
            .order_by(func.random())
            .limit(limit)
            .all()
        )

        return json.dumps({
            "features": [f.to_dict() for f in features],
            "count": len(features)
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_mark_passing(
    feature_id: Annotated[int, Field(description="The ID of the feature to mark as passing", ge=1)]
) -> str:
    """Mark a feature as passing after successful implementation.

    Updates the feature's passes field to true and clears the in_progress flag.
    Use this after you have implemented the feature and verified it works correctly.

    Args:
        feature_id: The ID of the feature to mark as passing

    Returns:
        JSON with the updated feature details, or error if not found.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        feature.passes = True
        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_skip(
    feature_id: Annotated[int, Field(description="The ID of the feature to skip", ge=1)]
) -> str:
    """Skip a feature by moving it to the end of the priority queue.

    Use this when a feature cannot be implemented yet due to:
    - Dependencies on other features that aren't implemented yet
    - External blockers (missing assets, unclear requirements)
    - Technical prerequisites that need to be addressed first

    The feature's priority is set to max_priority + 1, so it will be
    worked on after all other pending features. Also clears the in_progress
    flag so the feature returns to "pending" status.

    Args:
        feature_id: The ID of the feature to skip

    Returns:
        JSON with skip details: id, name, old_priority, new_priority, message
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        if feature.passes:
            return json.dumps({"error": "Cannot skip a feature that is already passing"})

        old_priority = feature.priority

        # Get max priority and set this feature to max + 1
        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
        new_priority = (max_priority_result[0] + 1) if max_priority_result else 1

        feature.priority = new_priority
        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps({
            "id": feature.id,
            "name": feature.name,
            "old_priority": old_priority,
            "new_priority": new_priority,
            "message": f"Feature '{feature.name}' moved to end of queue"
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_mark_in_progress(
    feature_id: Annotated[int, Field(description="The ID of the feature to mark as in-progress", ge=1)]
) -> str:
    """Mark a feature as in-progress. Call immediately after feature_get_next().

    This prevents other agent sessions from working on the same feature.
    Use this as soon as you retrieve a feature to work on.

    Args:
        feature_id: The ID of the feature to mark as in-progress

    Returns:
        JSON with the updated feature details, or error if not found or already in-progress.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        if feature.passes:
            return json.dumps({"error": f"Feature with ID {feature_id} is already passing"})

        if feature.in_progress:
            return json.dumps({"error": f"Feature with ID {feature_id} is already in-progress"})

        feature.in_progress = True
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_clear_in_progress(
    feature_id: Annotated[int, Field(description="The ID of the feature to clear in-progress status", ge=1)]
) -> str:
    """Clear in-progress status from a feature.

    Use this when abandoning a feature or manually unsticking a stuck feature.
    The feature will return to the pending queue.

    Args:
        feature_id: The ID of the feature to clear in-progress status

    Returns:
        JSON with the updated feature details, or error if not found.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_create_bulk(
    features: Annotated[list[dict], Field(description="List of features to create, each with category, name, description, and steps")]
) -> str:
    """Create multiple features in a single operation.

    Features are assigned sequential priorities based on their order.
    All features start with passes=false.

    This is typically used by the initializer agent to set up the initial
    feature list from the app specification.

    Args:
        features: List of features to create, each with:
            - category (str): Feature category
            - name (str): Feature name
            - description (str): Detailed description
            - steps (list[str]): Implementation/test steps

    Returns:
        JSON with: created (int) - number of features created
    """
    session = get_session()
    try:
        # Get the starting priority
        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
        start_priority = (max_priority_result[0] + 1) if max_priority_result else 1

        created_count = 0
        for i, feature_data in enumerate(features):
            # Validate required fields
            if not all(key in feature_data for key in ["category", "name", "description", "steps"]):
                return json.dumps({
                    "error": f"Feature at index {i} missing required fields (category, name, description, steps)"
                })

            db_feature = Feature(
                priority=start_priority + i,
                category=feature_data["category"],
                name=feature_data["name"],
                description=feature_data["description"],
                steps=feature_data["steps"],
                passes=False,
            )
            session.add(db_feature)
            created_count += 1

        session.commit()

        return json.dumps({"created": created_count}, indent=2)
    except Exception as e:
        session.rollback()
        return json.dumps({"error": str(e)})
    finally:
        session.close()

if __name__ == "__main__":
    mcp.run()

+# Import process tracker (only if server module is available)

+    from server.services.project_process_tracker import get_tracker


Good! Now let me add the cleanup logic to feature_mark_passing and feature_skip functions. I'll also add a new tool for process status and manual cleanup.


@mcp.tool()
def feature_mark_passing(
    feature_id: Annotated[int, Field(description="The ID of the feature to mark as passing", ge=1)]
) -> str:
    """Mark a feature as passing after successful implementation.

    Updates the feature's passes field to true and clears the in_progress flag.
    Use this after you have implemented the feature and verified it works correctly.

    Args:
        feature_id: The ID of the feature to mark as passing

    Returns:
        JSON with the updated feature details, or error if not found.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        feature.passes = True
        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_mark_passing(
    feature_id: Annotated[int, Field(description="The ID of the feature to mark as passing", ge=1)]
) -> str:
    """Mark a feature as passing after successful implementation.

    Updates the feature's passes field to true and clears the in_progress flag.
    Use this after you have implemented the feature and verified it works correctly.

    IMPORTANT: This automatically cleans up dev server and build processes before
    marking the feature as passing. Database processes are kept running.

    Args:
        feature_id: The ID of the feature to mark as passing

    Returns:
        JSON with the updated feature details, or error if not found.
    """
    # Clean up dev servers and build processes before marking passing
    cleanup_results = {}
    if PROCESS_TRACKER_AVAILABLE:
        try:
            tracker = get_tracker(PROJECT_DIR)
            cleanup_results = tracker.cleanup_between_features()
        except Exception as e:
            # Don't fail the feature marking if cleanup fails
            cleanup_results = {"error": f"Process cleanup failed: {e}"}

    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        feature.passes = True
        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        result = feature.to_dict()
        if cleanup_results:
            result["cleanup"] = cleanup_results

        return json.dumps(result, indent=2)
    finally:
        session.close()


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocoder-2.0\mcp_server\feature_mcp.py has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
   209→    finally:
   210→        session.close()
   211→
   212→
   213→@mcp.tool()
   214→def feature_mark_passing(
   215→    feature_id: Annotated[int, Field(description="The ID of the feature to mark as passing", ge=1)]
   216→) -> str:
   217→    """Mark a feature as passing after successful implementation.
   218→
   219→    Updates the feature's passes field to true and clears the in_progress flag.
   220→    Use this after you have implemented the feature and verified it works correctly.
   221→
   222→    IMPORTANT: This automatically cleans up dev server and build processes before
   223→    marking the feature as passing. Database processes are kept running.
   224→
   225→    Args:
   226→        feature_id: The ID of the feature to mark as passing
   227→
   228→    Returns:
   229→        JSON with the updated feature details, or error if not found.
   230→    """
   231→    # Clean up dev servers and build processes before marking passing
   232→    cleanup_results = {}
   233→    if PROCESS_TRACKER_AVAILABLE:
   234→        try:
   235→            tracker = get_tracker(PROJECT_DIR)
   236→            cleanup_results = tracker.cleanup_between_features()
   237→        except Exception as e:
   238→            # Don't fail the feature marking if cleanup fails
   239→            cleanup_results = {"error": f"Process cleanup failed: {e}"}
   240→
   241→    session = get_session()
   242→    try:
   243→        feature = session.query(Feature).filter(Feature.id == feature_id).first()
   244→
   245→        if feature is None:
   246→            return json.dumps({"error": f"Feature with ID {feature_id} not found"})
   247→
   248→        feature.passes = True
   249→        feature.in_progress = False
   250→        session.commit()
   251→        session.refresh(feature)
   252→
   253→        result = feature.to_dict()
   254→        if cleanup_results:
   255→            result["cleanup"] = cleanup_results
   256→
   257→        return json.dumps(result, indent=2)
   258→    finally:
   259→        session.close()
   260→
   261→
   262→@mcp.tool()
   263→def feature_skip(

@mcp.tool()
def feature_mark_passing(
    feature_id: Annotated[int, Field(description="The ID of the feature to mark as passing", ge=1)]
) -> str:
    """Mark a feature as passing after successful implementation.

    Updates the feature's passes field to true and clears the in_progress flag.
    Use this after you have implemented the feature and verified it works correctly.

    IMPORTANT: This automatically cleans up dev server and build processes before
    marking the feature as passing. Database processes are kept running.

    Args:
        feature_id: The ID of the feature to mark as passing

    Returns:
        JSON with the updated feature details, or error if not found.
    """
    # Clean up dev servers and build processes before marking passing
    cleanup_results = {}
    if PROCESS_TRACKER_AVAILABLE:
        try:
            tracker = get_tracker(PROJECT_DIR)
            cleanup_results = tracker.cleanup_between_features()
        except Exception as e:
            # Don't fail the feature marking if cleanup fails
            cleanup_results = {"error": f"Process cleanup failed: {e}"}

    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        feature.passes = True
        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        result = feature.to_dict()
        if cleanup_results:
            result["cleanup"] = cleanup_results

        return json.dumps(result, indent=2)
    finally:
        session.close()

#!/usr/bin/env python3
"""
MCP Server for Feature Management
==================================

Provides tools to manage features in the autonomous coding system,
replacing the previous FastAPI-based REST API.

Tools:
- feature_get_stats: Get progress statistics
- feature_get_next: Get next feature to implement
- feature_get_for_regression: Get random passing features for testing
- feature_mark_passing: Mark a feature as passing
- feature_skip: Skip a feature (move to end of queue)
- feature_mark_in_progress: Mark a feature as in-progress
- feature_clear_in_progress: Clear in-progress status
- feature_create_bulk: Create multiple features at once
"""

import json
import os
import sys
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Annotated

from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
from sqlalchemy.sql.expression import func

# Add parent directory to path so we can import from api module
sys.path.insert(0, str(Path(__file__).parent.parent))

from api.database import Feature, create_database
from api.migration import migrate_json_to_sqlite

# Import process tracker (only if server module is available)
try:
    from server.services.project_process_tracker import get_tracker
    PROCESS_TRACKER_AVAILABLE = True
except ImportError:
    PROCESS_TRACKER_AVAILABLE = False

# Configuration from environment
PROJECT_DIR = Path(os.environ.get("PROJECT_DIR", ".")).resolve()

# Pydantic models for input validation
class MarkPassingInput(BaseModel):
    """Input for marking a feature as passing."""
    feature_id: int = Field(..., description="The ID of the feature to mark as passing", ge=1)

class SkipFeatureInput(BaseModel):
    """Input for skipping a feature."""
    feature_id: int = Field(..., description="The ID of the feature to skip", ge=1)

class MarkInProgressInput(BaseModel):
    """Input for marking a feature as in-progress."""
    feature_id: int = Field(..., description="The ID of the feature to mark as in-progress", ge=1)

class ClearInProgressInput(BaseModel):
    """Input for clearing in-progress status."""
    feature_id: int = Field(..., description="The ID of the feature to clear in-progress status", ge=1)

class RegressionInput(BaseModel):
    """Input for getting regression features."""
    limit: int = Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")

class FeatureCreateItem(BaseModel):
    """Schema for creating a single feature."""
    category: str = Field(..., min_length=1, max_length=100, description="Feature category")
    name: str = Field(..., min_length=1, max_length=255, description="Feature name")
    description: str = Field(..., min_length=1, description="Detailed description")
    steps: list[str] = Field(..., min_length=1, description="Implementation/test steps")

class BulkCreateInput(BaseModel):
    """Input for bulk creating features."""
    features: list[FeatureCreateItem] = Field(..., min_length=1, description="List of features to create")

# Global database session maker (initialized on startup)
_session_maker = None
_engine = None

@asynccontextmanager
async def server_lifespan(server: FastMCP):
    """Initialize database on startup, cleanup on shutdown."""
    global _session_maker, _engine

    # Create project directory if it doesn't exist
    PROJECT_DIR.mkdir(parents=True, exist_ok=True)

    # Initialize database
    _engine, _session_maker = create_database(PROJECT_DIR)

    # Run migration if needed (converts legacy JSON to SQLite)
    migrate_json_to_sqlite(PROJECT_DIR, _session_maker)

    yield

    # Cleanup
    if _engine:
        _engine.dispose()

# Initialize the MCP server
mcp = FastMCP("features", lifespan=server_lifespan)

def get_session():
    """Get a new database session."""
    if _session_maker is None:
        raise RuntimeError("Database not initialized")
    return _session_maker()

@mcp.tool()
def feature_get_stats() -> str:
    """Get statistics about feature completion progress.

    Returns the number of passing features, in-progress features, total features,
    and completion percentage. Use this to track overall progress of the implementation.

    Returns:
        JSON with: passing (int), in_progress (int), total (int), percentage (float)
    """
    session = get_session()
    try:
        total = session.query(Feature).count()
        passing = session.query(Feature).filter(Feature.passes == True).count()
        in_progress = session.query(Feature).filter(Feature.in_progress == True).count()
        percentage = round((passing / total) * 100, 1) if total > 0 else 0.0

        return json.dumps({
            "passing": passing,
            "in_progress": in_progress,
            "total": total,
            "percentage": percentage
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_get_next() -> str:
    """Get the highest-priority pending feature to work on.

    Returns the feature with the lowest priority number that has passes=false.
    Use this at the start of each coding session to determine what to implement next.

    Returns:
        JSON with feature details (id, priority, category, name, description, steps, passes, in_progress)
        or error message if all features are passing.
    """
    session = get_session()
    try:
        feature = (
            session.query(Feature)
            .filter(Feature.passes == False)
            .order_by(Feature.priority.asc(), Feature.id.asc())
            .first()
        )

        if feature is None:
            return json.dumps({"error": "All features are passing! No more work to do."})

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_get_for_regression(
    limit: Annotated[int, Field(default=3, ge=1, le=10, description="Maximum number of passing features to return")] = 3
) -> str:
    """Get random passing features for regression testing.

    Returns a random selection of features that are currently passing.
    Use this to verify that previously implemented features still work
    after making changes.

    Args:
        limit: Maximum number of features to return (1-10, default 3)

    Returns:
        JSON with: features (list of feature objects), count (int)
    """
    session = get_session()
    try:
        features = (
            session.query(Feature)
            .filter(Feature.passes == True)
            .order_by(func.random())
            .limit(limit)
            .all()
        )

        return json.dumps({
            "features": [f.to_dict() for f in features],
            "count": len(features)
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_mark_passing(
    feature_id: Annotated[int, Field(description="The ID of the feature to mark as passing", ge=1)]
) -> str:
    """Mark a feature as passing after successful implementation.

    Updates the feature's passes field to true and clears the in_progress flag.
    Use this after you have implemented the feature and verified it works correctly.

    Args:
        feature_id: The ID of the feature to mark as passing

    Returns:
        JSON with the updated feature details, or error if not found.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        feature.passes = True
        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_skip(
    feature_id: Annotated[int, Field(description="The ID of the feature to skip", ge=1)]
) -> str:
    """Skip a feature by moving it to the end of the priority queue.

    Use this when a feature cannot be implemented yet due to:
    - Dependencies on other features that aren't implemented yet
    - External blockers (missing assets, unclear requirements)
    - Technical prerequisites that need to be addressed first

    The feature's priority is set to max_priority + 1, so it will be
    worked on after all other pending features. Also clears the in_progress
    flag so the feature returns to "pending" status.

    Args:
        feature_id: The ID of the feature to skip

    Returns:
        JSON with skip details: id, name, old_priority, new_priority, message
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        if feature.passes:
            return json.dumps({"error": "Cannot skip a feature that is already passing"})

        old_priority = feature.priority

        # Get max priority and set this feature to max + 1
        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
        new_priority = (max_priority_result[0] + 1) if max_priority_result else 1

        feature.priority = new_priority
        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps({
            "id": feature.id,
            "name": feature.name,
            "old_priority": old_priority,
            "new_priority": new_priority,
            "message": f"Feature '{feature.name}' moved to end of queue"
        }, indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_mark_in_progress(
    feature_id: Annotated[int, Field(description="The ID of the feature to mark as in-progress", ge=1)]
) -> str:
    """Mark a feature as in-progress. Call immediately after feature_get_next().

    This prevents other agent sessions from working on the same feature.
    Use this as soon as you retrieve a feature to work on.

    Args:
        feature_id: The ID of the feature to mark as in-progress

    Returns:
        JSON with the updated feature details, or error if not found or already in-progress.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        if feature.passes:
            return json.dumps({"error": f"Feature with ID {feature_id} is already passing"})

        if feature.in_progress:
            return json.dumps({"error": f"Feature with ID {feature_id} is already in-progress"})

        feature.in_progress = True
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_clear_in_progress(
    feature_id: Annotated[int, Field(description="The ID of the feature to clear in-progress status", ge=1)]
) -> str:
    """Clear in-progress status from a feature.

    Use this when abandoning a feature or manually unsticking a stuck feature.
    The feature will return to the pending queue.

    Args:
        feature_id: The ID of the feature to clear in-progress status

    Returns:
        JSON with the updated feature details, or error if not found.
    """
    session = get_session()
    try:
        feature = session.query(Feature).filter(Feature.id == feature_id).first()

        if feature is None:
            return json.dumps({"error": f"Feature with ID {feature_id} not found"})

        feature.in_progress = False
        session.commit()
        session.refresh(feature)

        return json.dumps(feature.to_dict(), indent=2)
    finally:
        session.close()

@mcp.tool()
def feature_create_bulk(
    features: Annotated[list[dict], Field(description="List of features to create, each with category, name, description, and steps")]
) -> str:
    """Create multiple features in a single operation.

    Features are assigned sequential priorities based on their order.
    All features start with passes=false.

    This is typically used by the initializer agent to set up the initial
    feature list from the app specification.

    Args:
        features: List of features to create, each with:
            - category (str): Feature category
            - name (str): Feature name
            - description (str): Detailed description
            - steps (list[str]): Implementation/test steps

    Returns:
        JSON with: created (int) - number of features created
    """
    session = get_session()
    try:
        # Get the starting priority
        max_priority_result = session.query(Feature.priority).order_by(Feature.priority.desc()).first()
        start_priority = (max_priority_result[0] + 1) if max_priority_result else 1

        created_count = 0
        for i, feature_data in enumerate(features):
            # Validate required fields
            if not all(key in feature_data for key in ["category", "name", "description", "steps"]):
                return json.dumps({
                    "error": f"Feature at index {i} missing required fields (category, name, description, steps)"
                })

            db_feature = Feature(
                priority=start_priority + i,
                category=feature_data["category"],
                name=feature_data["name"],
                description=feature_data["description"],
                steps=feature_data["steps"],
                passes=False,
            )
            session.add(db_feature)
            created_count += 1

        session.commit()

        return json.dumps({"created": created_count}, indent=2)
    except Exception as e:
        session.rollback()
        return json.dumps({"error": str(e)})
    finally:
        session.close()

if __name__ == "__main__":
    mcp.run()

+    IMPORTANT: This automatically cleans up dev server and build processes before

+    marking the feature as passing. Database processes are kept running.

+    # Clean up dev servers and build processes before marking passing

+            tracker = get_tracker(PROJECT_DIR)

+            cleanup_results = tracker.cleanup_between_features()

+            # Don't fail the feature marking if cleanup fails

+            cleanup_results = {"error": f"Process cleanup failed: {e}"}

-        return json.dumps(feature.to_dict(), indent=2)

+            result["cleanup"] = cleanup_results

+        return json.dumps(result, indent=2)


Good! Now let me add two new MCP tools: one for getting process status and one for manual cleanup.


Now let me add new MCP tools for process management:


if __name__ == "__main__":
    mcp.run()

@mcp.tool()
def project_processes_status() -> str:
    """Get status of all processes spawned by the project.

    Shows dev servers, databases, build processes, and other processes
    that were started during development. Use this to check what's running
    before cleaning up or debugging issues.

    Returns:
        JSON with process counts and details by category
    """
    if not PROCESS_TRACKER_AVAILABLE:
        return json.dumps({"error": "Process tracker not available"})

    try:
        tracker = get_tracker(PROJECT_DIR)
        status = tracker.get_status()
        return json.dumps(status, indent=2)
    except Exception as e:
        return json.dumps({"error": str(e)})

@mcp.tool()
def project_processes_cleanup(
    category: Annotated[
        str,
        Field(
            description="Category to cleanup: 'all', 'dev_server', 'database', 'build', or 'other'",
            pattern="^(all|dev_server|database|build|other)$"
        )
    ] = "dev_server"
) -> str:
    """Manually cleanup project processes by category.

    Use this to kill processes that are no longer needed. Common use cases:
    - Kill dev servers before restarting them
    - Clean up build processes after building
    - Force cleanup everything when debugging

    CAUTION: 'all' will kill databases too - use 'dev_server' for safe cleanup.

    Args:
        category: Which processes to kill
            - 'dev_server': npm/yarn dev, vite, flask run, etc.
            - 'database': postgres, mysql, mongodb, redis
            - 'build': npm build, webpack, vite build
            - 'other': miscellaneous processes
            - 'all': everything (including databases)

    Returns:
        JSON with cleanup results (killed/failed counts)
    """
    if not PROCESS_TRACKER_AVAILABLE:
        return json.dumps({"error": "Process tracker not available"})

    try:
        tracker = get_tracker(PROJECT_DIR)

        if category == "all":
            results = tracker.cleanup_all()
        else:
            results = tracker.cleanup_category(category)

        return json.dumps(results, indent=2)
    except Exception as e:
        return json.dumps({"error": str(e)})

@mcp.tool()
def project_process_register(
    pid: Annotated[int, Field(description="Process ID to register", ge=1)],
    command: Annotated[str, Field(description="The command that spawned this process")]
) -> str:
    """Register a process for tracking.

    Use this when you start a process that should be tracked for cleanup.
    The process will be automatically categorized (dev_server, database, build, other)
    based on the command pattern.

    Args:
        pid: Process ID (from command output or $! in bash)
        command: The full command that started the process

    Returns:
        JSON with registration confirmation and detected category
    """
    if not PROCESS_TRACKER_AVAILABLE:
        return json.dumps({"error": "Process tracker not available"})

    try:
        tracker = get_tracker(PROJECT_DIR)
        tracker.register_process(pid, command)

        # Get the category that was assigned
        proc_info = tracker.processes.get(pid)
        category = proc_info.category if proc_info else "unknown"

        return json.dumps({
            "pid": pid,
            "command": command[:100],
            "category": category,
            "message": "Process registered for tracking"
        }, indent=2)
    except Exception as e:
        return json.dumps({"error": str(e)})

if __name__ == "__main__":
    mcp.run()