# Directory Structure
```
├── .gitignore
├── .python-version
├── package.json
├── pyproject.toml
├── README.md
├── setup.py
├── src
│ ├── mcp_dev_server
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ ├── core
│ │ │ ├── __init__.py
│ │ │ └── server.py
│ │ ├── docker
│ │ │ ├── manager.py
│ │ │ ├── streams.py
│ │ │ ├── templates
│ │ │ │ ├── dev.dockerfile
│ │ │ │ ├── node.dockerfile
│ │ │ │ └── python.dockerfile
│ │ │ ├── templates.py
│ │ │ ├── volumes.py
│ │ │ └── xxx.py
│ │ ├── environments
│ │ │ ├── manager.py
│ │ │ ├── tools.py
│ │ │ └── workflow.py
│ │ ├── handlers
│ │ │ ├── __init__.py
│ │ │ └── input_request_handler.py
│ │ ├── managers
│ │ │ ├── __init__.py
│ │ │ ├── base_manager.py
│ │ │ ├── build_manager.py
│ │ │ ├── dependency_manager.py
│ │ │ ├── project_manager.py
│ │ │ ├── template_manager.py
│ │ │ ├── test_manager.py
│ │ │ └── workflow_manager.py
│ │ ├── models
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── errors.py
│ │ │ └── input_response.py
│ │ ├── package
│ │ │ └── manager.py
│ │ ├── project_manager
│ │ │ ├── base_project.py
│ │ │ ├── context.py
│ │ │ ├── git.py
│ │ │ ├── manager.py
│ │ │ ├── project_types.py
│ │ │ ├── project.py
│ │ │ └── templates.py
│ │ ├── prompts
│ │ │ ├── handler.py
│ │ │ ├── input_protocol.py
│ │ │ ├── project_templates.py
│ │ │ └── templates.py
│ │ ├── server.py
│ │ ├── test
│ │ │ └── manager.py
│ │ ├── utils
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── errors.py
│ │ │ └── logging.py
│ │ └── workflow
│ │ └── manager.py
│ └── resources
│ └── templates
│ └── basic
│ └── files
├── tests
│ └── test_integration.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.12
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *.so
5 | .Python
6 | build/
7 | develop-eggs/
8 | dist/
9 | downloads/
10 | eggs/
11 | .eggs/
12 | lib/
13 | lib64/
14 | parts/
15 | sdist/
16 | var/
17 | wheels/
18 | *.egg-info/
19 | .installed.cfg
20 | *.egg
21 |
22 | # Virtual environments
23 | .env
24 | .venv
25 | env/
26 | venv/
27 | ENV/
28 |
29 | # IDE
30 | .idea/
31 | .vscode/
32 | *.swp
33 | *.swo
34 |
35 | # Project specific
36 | *.log
37 | .docker/
38 | .pytest_cache/
39 | .coverage
40 | htmlcov/
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP Development Server
2 |
3 | A Model Context Protocol (MCP) server that enables Claude to manage software development projects, providing complete project context awareness and handling code execution through Docker environments.
4 |
5 | ## Features
6 |
7 | ### Core Infrastructure
8 | - Project context management
9 | - File system operations
10 | - Template-based project creation
11 | - Git integration
12 |
13 | ### Requirements
14 | - Python 3.12 or higher
15 | - Docker
16 | - Git
17 |
18 | ## Installation
19 |
20 | ```bash
21 | # Using pip
22 | pip install mcp-dev-server
23 |
24 | # Development installation
25 | git clone https://github.com/your-org/mcp-dev-server.git
26 | cd mcp-dev-server
27 | pip install -e .
28 | ```
29 |
30 | ## Configuration
31 |
32 | ### Claude Desktop Configuration
33 |
34 | Add to your Claude Desktop configuration file:
35 |
36 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
37 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
38 |
39 | ```json
40 | {
41 | "mcpServers": {
42 | "dev": {
43 | "command": "mcp-dev-server",
44 | "args": []
45 | }
46 | }
47 | }
48 | ```
49 |
50 | ## Usage
51 |
52 | The server provides several MCP capabilities:
53 |
54 | ### Resources
55 | - Project structure and files
56 | - Build status and artifacts
57 | - Test results
58 | - Docker container status
59 |
60 | ### Tools
61 | - Project initialization
62 | - Build operations
63 | - Test execution
64 | - Docker commands
65 |
66 | ### Prompts
67 | - Project analysis
68 | - Development suggestions
69 | - Error diagnosis
70 |
71 | ## Development
72 |
73 | ### Setting up development environment
74 |
75 | ```bash
76 | # Create virtual environment
77 | python -m venv .venv
78 | source .venv/bin/activate # On Windows: .venv\Scripts\activate
79 |
80 | # Install dependencies
81 | pip install -e ".[dev]"
82 | ```
83 |
84 | ### Running tests
85 |
86 | ```bash
87 | pytest tests/
88 | ```
89 |
90 | ## Contributing
91 |
92 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
93 |
94 | ## License
95 |
96 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/docker/xxx.py:
--------------------------------------------------------------------------------
```python
1 |
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/utils/__init__.py:
--------------------------------------------------------------------------------
```python
1 |
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/core/__init__.py:
--------------------------------------------------------------------------------
```python
1 | from .server import Server
2 |
3 | __all__ = ['Server']
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/prompts/input_protocol.py:
--------------------------------------------------------------------------------
```python
1 | """Input request protocol for MCP server."""
2 | [Previous content...]
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/handlers/__init__.py:
--------------------------------------------------------------------------------
```python
1 | from .input_request_handler import InputRequestHandler
2 |
3 | __all__ = ['InputRequestHandler']
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [build-system]
2 | requires = ["setuptools>=45", "wheel"]
3 | build-backend = "setuptools.build_meta"
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/models/errors.py:
--------------------------------------------------------------------------------
```python
1 | class MCPDevServerError(Exception):
2 | """Base exception class for MCP Development Server errors."""
3 | pass
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/__main__.py:
--------------------------------------------------------------------------------
```python
1 | """Main entry point when run with python -m mcp_dev_server"""
2 | from . import main
3 |
4 | if __name__ == '__main__':
5 | main()
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/managers/test_manager.py:
--------------------------------------------------------------------------------
```python
1 | class TestManager:
2 | """Manager class for test-related operations."""
3 |
4 | def __init__(self):
5 | """Initialize the test manager."""
6 | pass
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/managers/build_manager.py:
--------------------------------------------------------------------------------
```python
1 | class BuildManager:
2 | """Manager class for build-related operations."""
3 |
4 | def __init__(self):
5 | """Initialize the build manager."""
6 | pass
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/models/__init__.py:
--------------------------------------------------------------------------------
```python
1 | from .config import Config
2 | from .input_response import InputResponse
3 | from .errors import MCPDevServerError
4 |
5 | __all__ = ['Config', 'InputResponse', 'MCPDevServerError']
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/managers/template_manager.py:
--------------------------------------------------------------------------------
```python
1 | class TemplateManager:
2 | """Manager class for template-related operations."""
3 |
4 | def __init__(self):
5 | """Initialize the template manager."""
6 | pass
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/managers/workflow_manager.py:
--------------------------------------------------------------------------------
```python
1 | class WorkflowManager:
2 | """Manager class for workflow-related operations."""
3 |
4 | def __init__(self):
5 | """Initialize the workflow manager."""
6 | pass
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/managers/dependency_manager.py:
--------------------------------------------------------------------------------
```python
1 | class DependencyManager:
2 | """Manager class for dependency-related operations."""
3 |
4 | def __init__(self):
5 | """Initialize the dependency manager."""
6 | pass
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/managers/project_manager.py:
--------------------------------------------------------------------------------
```python
1 | from ..models import Config
2 |
3 | class ProjectManager:
4 | """Manager class for project-related operations."""
5 |
6 | def __init__(self, config: Config):
7 | """Initialize the project manager.
8 |
9 | Args:
10 | config: Server configuration
11 | """
12 | self.config = config
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/managers/__init__.py:
--------------------------------------------------------------------------------
```python
1 | from .project_manager import ProjectManager
2 | from .template_manager import TemplateManager
3 | from .build_manager import BuildManager
4 | from .dependency_manager import DependencyManager
5 | from .test_manager import TestManager
6 | from .workflow_manager import WorkflowManager
7 |
8 | __all__ = [
9 | 'ProjectManager',
10 | 'TemplateManager',
11 | 'BuildManager',
12 | 'DependencyManager',
13 | 'TestManager',
14 | 'WorkflowManager'
15 | ]
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/managers/base_manager.py:
--------------------------------------------------------------------------------
```python
1 | """Base manager class with common functionality."""
2 | import uuid
3 | from typing import Dict, Any
4 |
5 | class BaseManager:
6 | """Base class for all managers."""
7 |
8 | def _generate_id(self) -> str:
9 | """Generate a unique identifier.
10 |
11 | Returns:
12 | str: Unique identifier
13 | """
14 | return str(uuid.uuid4())
15 |
16 | async def cleanup(self):
17 | """Clean up resources. Override in subclasses."""
18 | pass
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """MCP Development Server Package."""
2 | from . import server
3 | import asyncio
4 | from typing import Optional
5 | from .utils.logging import setup_logging
6 |
7 | logger = setup_logging(__name__)
8 |
9 | def main():
10 | """Main entry point for the package."""
11 | try:
12 | server_instance = server.MCPDevServer()
13 | asyncio.run(server_instance.run())
14 | except KeyboardInterrupt:
15 | logger.info("Server shutdown requested")
16 | except Exception as e:
17 | logger.error(f"Server error: {str(e)}")
18 | raise
19 |
20 | # Expose key components at package level
21 | __all__ = ['main', 'server']
```
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
```python
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name="mcp-dev-server",
5 | version="0.1.0",
6 | packages=find_packages(where="src"),
7 | package_dir={"": "src"},
8 | install_requires=[
9 | "mcp", # Base MCP package
10 | "aiohttp>=3.8.0",
11 | "websockets>=10.0",
12 | "uvicorn>=0.15.0",
13 | "fastapi>=0.68.0",
14 | "typing_extensions>=4.5.0",
15 | ],
16 | entry_points={
17 | "console_scripts": [
18 | "mcp-dev-server=mcp_dev_server:main",
19 | ],
20 | },
21 | python_requires=">=3.8",
22 | author="Your Name",
23 | description="MCP Development Server"
24 | )
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/models/input_response.py:
--------------------------------------------------------------------------------
```python
1 | from typing import Any, Dict
2 |
3 | class InputResponse:
4 | """Class representing a user's input response."""
5 |
6 | def __init__(self, request_id: str, values: Dict[str, Any]):
7 | """Initialize an input response.
8 |
9 | Args:
10 | request_id: ID of the input request
11 | values: Dictionary of input values
12 | """
13 | self.request_id = request_id
14 | self.values = values
15 |
16 | def validate(self) -> bool:
17 | """Validate the input response.
18 |
19 | Returns:
20 | bool: True if valid, False otherwise
21 | """
22 | return True # TODO: Implement validation
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/utils/errors.py:
--------------------------------------------------------------------------------
```python
1 | """Error definitions for MCP Development Server."""
2 |
3 | class MCPDevServerError(Exception):
4 | """Base error class for MCP Development Server."""
5 | pass
6 |
7 | class ProjectError(MCPDevServerError):
8 | """Project-related errors."""
9 | pass
10 |
11 | class BuildError(MCPDevServerError):
12 | """Build-related errors."""
13 | pass
14 |
15 | class TestError(MCPDevServerError):
16 | """Test-related errors."""
17 | pass
18 |
19 | class EnvironmentError(MCPDevServerError):
20 | """Environment-related errors."""
21 | pass
22 |
23 | class ConfigurationError(MCPDevServerError):
24 | """Configuration-related errors."""
25 | pass
26 |
27 | class WorkflowError(MCPDevServerError):
28 | """Workflow-related errors."""
29 | pass
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/docker/templates/node.dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Node.js development environment
2 | FROM node:{{ node_version }}
3 |
4 | # Install system dependencies
5 | RUN apt-get update && apt-get install -y \
6 | git \
7 | curl \
8 | && rm -rf /var/lib/apt/lists/*
9 |
10 | # Set working directory
11 | WORKDIR /workspace
12 |
13 | {% if package_file %}
14 | # Install Node.js dependencies
15 | COPY {{ package_file }} .
16 | {% if package_lock %}
17 | COPY {{ package_lock }} .
18 | RUN npm ci
19 | {% else %}
20 | RUN npm install
21 | {% endif %}
22 | {% endif %}
23 |
24 | {% if global_packages %}
25 | # Install global packages
26 | RUN npm install -g {% for package in global_packages %}{{ package }} {% endfor %}
27 | {% endif %}
28 |
29 | # Set Node.js environment variables
30 | ENV NODE_ENV=development
31 |
32 | {% if command %}
33 | # Default command
34 | CMD {{ command }}
35 | {% endif %}
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/models/config.py:
--------------------------------------------------------------------------------
```python
1 | class Config:
2 | """Configuration class for MCP Development Server."""
3 |
4 | def __init__(self):
5 | """Initialize configuration with default values."""
6 | self.host = "localhost"
7 | self.port = 8000
8 | self.debug = False
9 |
10 | def load_from_file(self, file_path: str):
11 | """Load configuration from a file.
12 |
13 | Args:
14 | file_path: Path to configuration file
15 | """
16 | pass # TODO: Implement configuration loading
17 |
18 | def save_to_file(self, file_path: str):
19 | """Save current configuration to a file.
20 |
21 | Args:
22 | file_path: Path to save configuration
23 | """
24 | pass # TODO: Implement configuration saving
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-dev-server",
3 | "version": "1.0.0",
4 | "description": "Model Context Protocol Development Server",
5 | "main": "dist/app.js",
6 | "scripts": {
7 | "start": "node dist/app.js",
8 | "dev": "nodemon src/app.ts",
9 | "build": "tsc",
10 | "test": "jest",
11 | "lint": "eslint . --ext .ts"
12 | },
13 | "dependencies": {
14 | "express": "^4.18.2",
15 | "typescript": "^5.0.0",
16 | "mongoose": "^7.0.0",
17 | "dotenv": "^16.0.0",
18 | "winston": "^3.8.0",
19 | "cors": "^2.8.5",
20 | "helmet": "^6.0.0",
21 | "joi": "^17.0.0"
22 | },
23 | "devDependencies": {
24 | "@types/express": "^4.17.17",
25 | "@types/node": "^18.0.0",
26 | "@types/jest": "^29.0.0",
27 | "@typescript-eslint/eslint-plugin": "^5.0.0",
28 | "@typescript-eslint/parser": "^5.0.0",
29 | "eslint": "^8.0.0",
30 | "jest": "^29.0.0",
31 | "nodemon": "^2.0.0",
32 | "ts-jest": "^29.0.0",
33 | "ts-node": "^10.0.0"
34 | }
35 | }
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/docker/templates/python.dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Python development environment
2 | FROM python:{{ python_version }}-slim
3 |
4 | # Install system dependencies
5 | RUN apt-get update && apt-get install -y \
6 | git \
7 | curl \
8 | build-essential \
9 | && rm -rf /var/lib/apt/lists/*
10 |
11 | # Set working directory
12 | WORKDIR /workspace
13 |
14 | {% if install_poetry %}
15 | # Install Poetry
16 | RUN curl -sSL https://install.python-poetry.org | python3 -
17 | ENV PATH="/root/.local/bin:$PATH"
18 | {% endif %}
19 |
20 | {% if requirements_file %}
21 | # Install Python dependencies
22 | COPY {{ requirements_file }} .
23 | RUN pip install -r {{ requirements_file }}
24 | {% endif %}
25 |
26 | {% if additional_packages %}
27 | # Install additional packages
28 | RUN pip install {% for package in additional_packages %}{{ package }} {% endfor %}
29 | {% endif %}
30 |
31 | # Set Python environment variables
32 | ENV PYTHONUNBUFFERED=1 \
33 | PYTHONDONTWRITEBYTECODE=1
34 |
35 | {% if command %}
36 | # Default command
37 | CMD {{ command }}
38 | {% endif %}
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/utils/logging.py:
--------------------------------------------------------------------------------
```python
1 | """Logging configuration for MCP Development Server."""
2 | import logging
3 | import sys
4 | from typing import Optional
5 |
6 | def setup_logging(name: Optional[str] = None, level: int = logging.INFO) -> logging.Logger:
7 | """Setup logging configuration.
8 |
9 | Args:
10 | name: Logger name
11 | level: Logging level
12 |
13 | Returns:
14 | logging.Logger: Configured logger instance
15 | """
16 | # Create logger
17 | logger = logging.getLogger(name or __name__)
18 | logger.setLevel(level)
19 |
20 | # Create stderr handler (MCP protocol requires clean stdout)
21 | handler = logging.StreamHandler(sys.stderr)
22 | handler.setLevel(level)
23 |
24 | # Create formatter
25 | formatter = logging.Formatter(
26 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
27 | )
28 | handler.setFormatter(formatter)
29 |
30 | # Add handler to logger
31 | logger.addHandler(handler)
32 |
33 | return logger
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/handlers/input_request_handler.py:
--------------------------------------------------------------------------------
```python
1 | from typing import Dict, Any, Optional
2 | from ..models import InputResponse
3 |
4 | class InputRequestHandler:
5 | """Handler for input requests."""
6 |
7 | def __init__(self):
8 | """Initialize the input request handler."""
9 | pass
10 |
11 | async def request_input(self, request_type: str, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
12 | """Request input from the user.
13 |
14 | Args:
15 | request_type: Type of input request
16 | context: Additional context for request
17 |
18 | Returns:
19 | Dict[str, Any]: User's input values
20 | """
21 | return {} # TODO: Implement input request handling
22 |
23 | def handle_response(self, response: InputResponse):
24 | """Handle input response from user.
25 |
26 | Args:
27 | response: User's response
28 | """
29 | pass # TODO: Implement response handling
```
--------------------------------------------------------------------------------
/tests/test_integration.py:
--------------------------------------------------------------------------------
```python
1 | """Test MCP server integration with Claude."""
2 | import asyncio
3 | import pytest
4 | from mcp_dev_server.server import MCPDevServer
5 | from mcp_dev_server.utils.config import Config
6 |
7 | @pytest.mark.asyncio
8 | async def test_server_initialization():
9 | """Test server initialization."""
10 | config = Config()
11 | server = MCPDevServer()
12 |
13 | # Test project creation
14 | project = await server.project_manager.create_project(
15 | name="test-project",
16 | project_type="python",
17 | project_config={
18 | "python_version": "3.12",
19 | "project_type": "fastapi",
20 | "dependency_management": "poetry"
21 | }
22 | )
23 |
24 | assert project is not None
25 | assert project.config["name"] == "test-project"
26 |
27 | # Test tool execution
28 | result = await server.handle_call_tool("build", {
29 | "environment": "default",
30 | "command": "build"
31 | })
32 |
33 | assert result[0].type == "text"
34 |
35 | # Cleanup
36 | await server.cleanup()
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/docker/templates.py:
--------------------------------------------------------------------------------
```python
1 | """Dockerfile templates for different environments."""
2 | from typing import Dict, Optional
3 | from jinja2 import Template
4 |
5 | class DockerTemplates:
6 | """Manages Dockerfile templates for different environments."""
7 |
8 | @staticmethod
9 | def get_template(environment: str, config: Optional[Dict[str, Any]] = None) -> str:
10 | """Get Dockerfile template for specific environment."""
11 | config = config or {}
12 |
13 | if environment == "python":
14 | return Template("""
15 | FROM python:{{ python_version|default('3.12-slim') }}
16 |
17 | WORKDIR /app
18 |
19 | {% if requirements_file %}
20 | COPY {{ requirements_file }} .
21 | RUN pip install -r {{ requirements_file }}
22 | {% endif %}
23 |
24 | {% if install_dev_deps %}
25 | RUN pip install pytest mypy black
26 | {% endif %}
27 |
28 | {% for cmd in additional_commands|default([]) %}
29 | RUN {{ cmd }}
30 | {% endfor %}
31 |
32 | COPY . .
33 |
34 | CMD ["python", "{{ entry_point|default('main.py') }}"]
35 | """).render(config)
36 |
37 | elif environment == "node":
38 | return Template("""
39 | FROM node:{{ node_version|default('20-slim') }}
40 |
41 | WORKDIR /app
42 |
43 | COPY package*.json ./
44 |
45 | RUN npm install {% if install_dev_deps %}--include=dev{% endif %}
46 |
47 | {% for cmd in additional_commands|default([]) %}
48 | RUN {{ cmd }}
49 | {% endfor %}
50 |
51 | COPY . .
52 |
53 | CMD ["npm", "{{ npm_command|default('start') }}"]
54 | """).render(config)
55 |
56 | else:
57 | raise ValueError(f"Unknown environment: {environment}")
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/docker/templates/dev.dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Multi-language development environment
2 | FROM ubuntu:{{ ubuntu_version }}
3 |
4 | # Install system dependencies
5 | RUN apt-get update && apt-get install -y \
6 | git \
7 | curl \
8 | build-essential \
9 | software-properties-common \
10 | && rm -rf /var/lib/apt/lists/*
11 |
12 | {% if install_python %}
13 | # Install Python
14 | RUN add-apt-repository ppa:deadsnakes/ppa && \
15 | apt-get update && \
16 | apt-get install -y python{{ python_version }} python{{ python_version }}-venv python{{ python_version }}-dev && \
17 | rm -rf /var/lib/apt/lists/*
18 | {% endif %}
19 |
20 | {% if install_node %}
21 | # Install Node.js
22 | RUN curl -fsSL https://deb.nodesource.com/setup_{{ node_version }}.x | bash - && \
23 | apt-get install -y nodejs && \
24 | rm -rf /var/lib/apt/lists/*
25 | {% endif %}
26 |
27 | {% if install_docker %}
28 | # Install Docker
29 | RUN curl -fsSL https://get.docker.com | sh && \
30 | rm -rf /var/lib/apt/lists/*
31 | {% endif %}
32 |
33 | # Set working directory
34 | WORKDIR /workspace
35 |
36 | {% if requirements_file %}
37 | # Install Python dependencies
38 | COPY {{ requirements_file }} .
39 | RUN pip{{ python_version }} install -r {{ requirements_file }}
40 | {% endif %}
41 |
42 | {% if package_file %}
43 | # Install Node.js dependencies
44 | COPY {{ package_file }} .
45 | {% if package_lock %}
46 | COPY {{ package_lock }} .
47 | RUN npm ci
48 | {% else %}
49 | RUN npm install
50 | {% endif %}
51 | {% endif %}
52 |
53 | {% if additional_tools %}
54 | # Install additional tools
55 | RUN apt-get update && apt-get install -y \
56 | {% for tool in additional_tools %}{{ tool }} {% endfor %} \
57 | && rm -rf /var/lib/apt/lists/*
58 | {% endif %}
59 |
60 | # Set environment variables
61 | ENV PYTHONUNBUFFERED=1 \
62 | PYTHONDONTWRITEBYTECODE=1 \
63 | NODE_ENV=development
64 |
65 | {% if command %}
66 | # Default command
67 | CMD {{ command }}
68 | {% endif %}
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/prompts/handler.py:
--------------------------------------------------------------------------------
```python
1 | [Previous handler.py content...]
2 |
3 | async def process_field_dependencies(self, request: InputRequest, field_updates: Dict[str, Any]):
4 | """Process field dependencies based on user input.
5 |
6 | Some fields might need to be updated based on values of other fields.
7 | For example, if user selects Python as language, we need to show Python version field.
8 |
9 | Args:
10 | request: Current input request
11 | field_updates: Updated field values
12 | """
13 | try:
14 | if request.request_id == "environment_setup":
15 | language = field_updates.get("language")
16 | if language:
17 | # Update required fields based on language selection
18 | for field in request.fields:
19 | if field.name == "python_version":
20 | field.required = language in ["python", "both"]
21 | elif field.name == "node_version":
22 | field.required = language in ["node", "both"]
23 |
24 | elif request.request_id == "test_configuration":
25 | test_framework = field_updates.get("test_framework")
26 | if test_framework:
27 | # Update coverage options based on test framework
28 | for field in request.fields:
29 | if field.name == "include_coverage":
30 | field.options = self._get_coverage_options(test_framework)
31 |
32 | def _get_coverage_options(self, framework: str) -> List[Dict[str, str]]:
33 | """Get coverage tool options based on test framework."""
34 | coverage_tools = {
35 | "pytest": [
36 | {"value": "pytest-cov", "label": "pytest-cov"},
37 | {"value": "coverage", "label": "coverage.py"}
38 | ],
39 | "unittest": [
40 | {"value": "coverage", "label": "coverage.py"}
41 | ],
42 | "jest": [
43 | {"value": "jest-coverage", "label": "Jest Coverage"}
44 | ],
45 | "mocha": [
46 | {"value": "nyc", "label": "Istanbul/nyc"}
47 | ]
48 | }
49 | return coverage_tools.get(framework, [])
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/utils/config.py:
--------------------------------------------------------------------------------
```python
1 | """Configuration management for MCP Development Server."""
2 | import os
3 | import json
4 | from typing import Dict, Any, Optional
5 | from pathlib import Path
6 |
7 | class Config:
8 | """Configuration manager."""
9 |
10 | def __init__(self):
11 | """Initialize configuration."""
12 | self.config_dir = self._get_config_dir()
13 | self.config_file = self.config_dir / "config.json"
14 | self.config: Dict[str, Any] = self._load_config()
15 |
16 | def _get_config_dir(self) -> Path:
17 | """Get configuration directory path."""
18 | if os.name == "nt": # Windows
19 | config_dir = Path(os.getenv("APPDATA")) / "Claude"
20 | else: # macOS/Linux
21 | config_dir = Path.home() / ".config" / "claude"
22 |
23 | config_dir.mkdir(parents=True, exist_ok=True)
24 | return config_dir
25 |
26 | def _load_config(self) -> Dict[str, Any]:
27 | """Load configuration from file."""
28 | if self.config_file.exists():
29 | try:
30 | with open(self.config_file, "r") as f:
31 | return json.load(f)
32 | except Exception as e:
33 | print(f"Error loading config: {e}")
34 | return self._get_default_config()
35 | else:
36 | config = self._get_default_config()
37 | self._save_config(config)
38 | return config
39 |
40 | def _save_config(self, config: Dict[str, Any]):
41 | """Save configuration to file."""
42 | try:
43 | with open(self.config_file, "w") as f:
44 | json.dump(config, f, indent=2)
45 | except Exception as e:
46 | print(f"Error saving config: {e}")
47 |
48 | def _get_default_config(self) -> Dict[str, Any]:
49 | """Get default configuration."""
50 | return {
51 | "projectsDir": str(Path.home() / "Projects"),
52 | "templatesDir": str(self.config_dir / "templates"),
53 | "environments": {
54 | "default": {
55 | "type": "docker",
56 | "image": "python:3.12-slim"
57 | }
58 | }
59 | }
60 |
61 | def get(self, key: str, default: Any = None) -> Any:
62 | """Get configuration value."""
63 | return self.config.get(key, default)
64 |
65 | def set(self, key: str, value: Any):
66 | """Set configuration value."""
67 | self.config[key] = value
68 | self._save_config(self.config)
69 |
70 | def update(self, updates: Dict[str, Any]):
71 | """Update multiple configuration values."""
72 | self.config.update(updates)
73 | self._save_config(self.config)
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/docker/volumes.py:
--------------------------------------------------------------------------------
```python
1 | """Docker volume management for MCP Development Server."""
2 | from typing import Dict, List, Optional
3 | import docker
4 | from docker.errors import DockerException
5 |
6 | from ..utils.logging import setup_logging
7 | from ..utils.errors import DockerError
8 |
9 | logger = setup_logging(__name__)
10 |
11 | class VolumeManager:
12 | """Manages Docker volumes for development environments."""
13 |
14 | def __init__(self):
15 | self.client = docker.from_env()
16 |
17 | async def create_volume(
18 | self,
19 | name: str,
20 | labels: Optional[Dict[str, str]] = None
21 | ) -> str:
22 | """Create a Docker volume."""
23 | try:
24 | volume = self.client.volumes.create(
25 | name=name,
26 | driver='local',
27 | labels=labels or {}
28 | )
29 | logger.info(f"Created volume: {name}")
30 | return volume.name
31 |
32 | except DockerException as e:
33 | raise DockerError(f"Failed to create volume: {str(e)}")
34 |
35 | async def remove_volume(self, name: str) -> None:
36 | """Remove a Docker volume."""
37 | try:
38 | volume = self.client.volumes.get(name)
39 | volume.remove()
40 | logger.info(f"Removed volume: {name}")
41 |
42 | except DockerException as e:
43 | raise DockerError(f"Failed to remove volume: {str(e)}")
44 |
45 | async def list_volumes(
46 | self,
47 | filters: Optional[Dict[str, str]] = None
48 | ) -> List[Dict[str, Any]]:
49 | """List Docker volumes."""
50 | try:
51 | volumes = self.client.volumes.list(filters=filters or {})
52 | return [
53 | {
54 | "name": v.name,
55 | "driver": v.attrs['Driver'],
56 | "mountpoint": v.attrs['Mountpoint'],
57 | "labels": v.attrs['Labels'] or {}
58 | }
59 | for v in volumes
60 | ]
61 |
62 | except DockerException as e:
63 | raise DockerError(f"Failed to list volumes: {str(e)}")
64 |
65 | async def get_volume_info(self, name: str) -> Dict[str, Any]:
66 | """Get detailed information about a volume."""
67 | try:
68 | volume = self.client.volumes.get(name)
69 | return {
70 | "name": volume.name,
71 | "driver": volume.attrs['Driver'],
72 | "mountpoint": volume.attrs['Mountpoint'],
73 | "labels": volume.attrs['Labels'] or {},
74 | "scope": volume.attrs['Scope'],
75 | "status": volume.attrs.get('Status', {})
76 | }
77 |
78 | except DockerException as e:
79 | raise DockerError(f"Failed to get volume info: {str(e)}")
80 |
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/package/manager.py:
--------------------------------------------------------------------------------
```python
1 | """Package management integration for MCP Development Server."""
2 |
3 | from typing import Dict, List, Optional, Any
4 | from enum import Enum
5 | from ..utils.errors import PackageError
6 | from ..utils.logging import setup_logging
7 |
8 | logger = setup_logging(__name__)
9 |
10 | class PackageManager(str, Enum):
11 | """Supported package managers."""
12 | NPM = "npm"
13 | PIP = "pip"
14 | CARGO = "cargo"
15 |
16 | class DependencyManager:
17 | """Manages project dependencies."""
18 |
19 | def __init__(self, env_manager):
20 | self.env_manager = env_manager
21 |
22 | async def install_dependencies(
23 | self,
24 | environment: str,
25 | package_manager: PackageManager,
26 | dependencies: List[str],
27 | dev: bool = False
28 | ) -> Dict[str, Any]:
29 | """Install project dependencies."""
30 | try:
31 | command = self._build_install_command(
32 | package_manager,
33 | dependencies,
34 | dev
35 | )
36 |
37 | result = await self.env_manager.execute_in_environment(
38 | environment,
39 | command
40 | )
41 |
42 | return {
43 | "success": result["exit_code"] == 0,
44 | "output": result["output"],
45 | "error": result.get("error")
46 | }
47 |
48 | except Exception as e:
49 | raise PackageError(f"Failed to install dependencies: {str(e)}")
50 |
51 | async def update_dependencies(
52 | self,
53 | environment: str,
54 | package_manager: PackageManager,
55 | dependencies: Optional[List[str]] = None
56 | ) -> Dict[str, Any]:
57 | """Update project dependencies."""
58 | try:
59 | command = self._build_update_command(package_manager, dependencies)
60 |
61 | result = await self.env_manager.execute_in_environment(
62 | environment,
63 | command
64 | )
65 |
66 | return {
67 | "success": result["exit_code"] == 0,
68 | "output": result["output"],
69 | "error": result.get("error")
70 | }
71 |
72 | except Exception as e:
73 | raise PackageError(f"Failed to update dependencies: {str(e)}")
74 |
75 | def _build_install_command(
76 | self,
77 | package_manager: PackageManager,
78 | dependencies: List[str],
79 | dev: bool
80 | ) -> str:
81 | """Build dependency installation command."""
82 | if package_manager == PackageManager.NPM:
83 | dev_flag = "--save-dev" if dev else ""
84 | deps = " ".join(dependencies)
85 | return f"npm install {dev_flag} {deps}"
86 |
87 | elif package_manager == PackageManager.PIP:
88 | dev_flag = "-D" if dev else ""
89 | deps = " ".join(dependencies)
90 | return f"pip install {dev_flag} {deps}"
91 |
92 | elif package_manager == PackageManager.CARGO:
93 | dev_flag = "--dev" if dev else ""
94 | deps = " ".join(dependencies)
95 | return f"cargo add {dev_flag} {deps}"
96 |
97 | else:
98 | raise PackageError(f"Unsupported package manager: {package_manager}")
99 |
100 | def _build_update_command(
101 | self,
102 | package_manager: PackageManager,
103 | dependencies: Optional[List[str]] = None
104 | ) -> str:
105 | """Build dependency update command."""
106 | if package_manager == PackageManager.NPM:
107 | return "npm update" if not dependencies else f"npm update {' '.join(dependencies)}"
108 |
109 | elif package_manager == PackageManager.PIP:
110 | return "pip install -U -r requirements.txt" if not dependencies else f"pip install -U {' '.join(dependencies)}"
111 |
112 | elif package_manager == PackageManager.CARGO:
113 | return "cargo update" if not dependencies else f"cargo update {' '.join(dependencies)}"
114 |
115 | else:
116 | raise PackageError(f"Unsupported package manager: {package_manager}")
117 |
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/test/manager.py:
--------------------------------------------------------------------------------
```python
1 | """Test system integration for MCP Development Server."""
2 |
3 | import asyncio
4 | from typing import Dict, List, Optional, Any
5 | from enum import Enum
6 | from datetime import datetime
7 | from ..utils.errors import TestError
8 | from ..utils.logging import setup_logging
9 |
10 | logger = setup_logging(__name__)
11 |
12 | class TestStatus(str, Enum):
13 | """Test execution status."""
14 | PENDING = "pending"
15 | RUNNING = "running"
16 | SUCCESS = "success"
17 | FAILED = "failed"
18 | ERROR = "error"
19 |
20 | class TestManager:
21 | """Manages test execution and reporting."""
22 |
23 | def __init__(self, env_manager):
24 | self.env_manager = env_manager
25 | self.test_runs: Dict[str, Dict[str, Any]] = {}
26 |
27 | async def run_tests(
28 | self,
29 | environment: str,
30 | config: Dict[str, Any]
31 | ) -> str:
32 | """Start a test run."""
33 | try:
34 | test_id = f"test_{len(self.test_runs)}"
35 |
36 | # Initialize test run
37 | self.test_runs[test_id] = {
38 | "environment": environment,
39 | "config": config,
40 | "status": TestStatus.PENDING,
41 | "results": [],
42 | "start_time": datetime.now(),
43 | "end_time": None
44 | }
45 |
46 | # Start test execution
47 | asyncio.create_task(self._execute_tests(test_id))
48 |
49 | return test_id
50 |
51 | except Exception as e:
52 | raise TestError(f"Failed to start tests: {str(e)}")
53 |
54 | async def _execute_tests(self, test_id: str) -> None:
55 | """Execute test suite."""
56 | try:
57 | test_run = self.test_runs[test_id]
58 | test_run["status"] = TestStatus.RUNNING
59 |
60 | # Run test command
61 | result = await self.env_manager.execute_in_environment(
62 | test_run["environment"],
63 | test_run["config"].get("command", "npm test"),
64 | workdir=test_run["config"].get("workdir")
65 | )
66 |
67 | # Parse and store results
68 | test_run["results"] = self._parse_test_output(
69 | result["output"],
70 | test_run["config"].get("format", "jest")
71 | )
72 |
73 | # Update test status
74 | test_run["end_time"] = datetime.now()
75 | test_run["status"] = (
76 | TestStatus.SUCCESS
77 | if result["exit_code"] == 0
78 | else TestStatus.FAILED
79 | )
80 |
81 | except Exception as e:
82 | logger.error(f"Test execution error: {str(e)}")
83 | test_run["status"] = TestStatus.ERROR
84 | test_run["error"] = str(e)
85 |
86 | async def get_test_status(self, test_id: str) -> Dict[str, Any]:
87 | """Get status and results of a test run."""
88 | if test_run := self.test_runs.get(test_id):
89 | return {
90 | "id": test_id,
91 | "status": test_run["status"],
92 | "results": test_run["results"],
93 | "start_time": test_run["start_time"],
94 | "end_time": test_run["end_time"],
95 | "error": test_run.get("error")
96 | }
97 | raise TestError(f"Test run not found: {test_id}")
98 |
99 | def _parse_test_output(
100 | self,
101 | output: str,
102 | format: str
103 | ) -> List[Dict[str, Any]]:
104 | """Parse test output into structured results."""
105 | if format == "jest":
106 | return self._parse_jest_output(output)
107 | elif format == "pytest":
108 | return self._parse_pytest_output(output)
109 | else:
110 | logger.warning(f"Unknown test output format: {format}")
111 | return [{"raw_output": output}]
112 |
113 | def _parse_jest_output(self, output: str) -> List[Dict[str, Any]]:
114 | """Parse Jest test output."""
115 | results = []
116 | # Implement Jest output parsing
117 | return results
118 |
119 | def _parse_pytest_output(self, output: str) -> List[Dict[str, Any]]:
120 | """Parse pytest output."""
121 | results = []
122 | # Implement pytest output parsing
123 | return results
124 |
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/server.py:
--------------------------------------------------------------------------------
```python
1 | """MCP Development Server implementation."""
2 | from typing import Dict, Any, Optional, Sequence
3 | import logging
4 | import sys
5 | import json
6 |
7 | # Import MCP components
8 | from mcp.server import Server as MCPServer
9 | from mcp.server.stdio import stdio_server
10 | import mcp.types as types
11 |
12 | from .models import Config, InputResponse, MCPDevServerError
13 | from .managers import (
14 | ProjectManager,
15 | TemplateManager,
16 | BuildManager,
17 | DependencyManager,
18 | TestManager,
19 | WorkflowManager
20 | )
21 | from .handlers import InputRequestHandler
22 |
23 | # Configure logging to stderr to keep stdout clean
24 | logger = logging.getLogger(__name__)
25 | handler = logging.StreamHandler(sys.stderr)
26 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
27 | handler.setFormatter(formatter)
28 | logger.addHandler(handler)
29 | logger.setLevel(logging.DEBUG) # Set to DEBUG for development
30 |
31 | class MCPDevServer:
32 | """MCP Development Server implementation."""
33 |
34 | def __init__(self):
35 | """Initialize the MCP Development Server."""
36 | logger.info("Initializing MCP Development Server")
37 |
38 | try:
39 | # Initialize server
40 | self.server = MCPServer("mcp-dev-server")
41 |
42 | # Initialize configuration
43 | self.config = Config()
44 |
45 | # Initialize all managers
46 | self.project_manager = ProjectManager(self.config)
47 | self.template_manager = TemplateManager()
48 | self.build_manager = BuildManager()
49 | self.dependency_manager = DependencyManager()
50 | self.test_manager = TestManager()
51 | self.workflow_manager = WorkflowManager()
52 | self.input_handler = InputRequestHandler()
53 |
54 | # Setup request handlers
55 | self._setup_resource_handlers()
56 | self._setup_tool_handlers()
57 | self._setup_prompt_handlers()
58 |
59 | logger.info("Server initialization completed successfully")
60 |
61 | except Exception as e:
62 | logger.error(f"Failed to initialize server: {e}")
63 | raise
64 |
65 | def _setup_resource_handlers(self):
66 | """Set up resource request handlers."""
67 | @self.server.list_resources()
68 | async def list_resources() -> list[types.Resource]:
69 | """List available resources."""
70 | logger.debug("Listing resources")
71 | return []
72 |
73 | @self.server.read_resource()
74 | async def read_resource(uri: str) -> str:
75 | """Read resource content."""
76 | logger.debug(f"Reading resource: {uri}")
77 | return ""
78 |
79 | def _setup_tool_handlers(self):
80 | """Set up tool request handlers."""
81 | @self.server.list_tools()
82 | async def list_tools() -> list[types.Tool]:
83 | """List available tools."""
84 | logger.debug("Listing tools")
85 | return []
86 |
87 | @self.server.call_tool()
88 | async def call_tool(name: str, arguments: Dict[str, Any]) -> Sequence[types.TextContent]:
89 | """Execute a tool."""
90 | logger.debug(f"Calling tool {name} with arguments {arguments}")
91 | return [types.TextContent(type="text", text="Tool execution result")]
92 |
93 | def _setup_prompt_handlers(self):
94 | """Set up prompt request handlers."""
95 | @self.server.list_prompts()
96 | async def list_prompts() -> list[types.Prompt]:
97 | """List available prompts."""
98 | logger.debug("Listing prompts")
99 | return []
100 |
101 | async def run(self):
102 | """Run the MCP Development Server."""
103 | try:
104 | logger.info(f"Starting {self.server.name}...")
105 |
106 | # Use stdio transport
107 | async with stdio_server() as streams:
108 | logger.info("Using stdio transport")
109 | await self.server.run(
110 | streams[0], # read stream
111 | streams[1], # write stream
112 | self.server.create_initialization_options(),
113 | raise_exceptions=True # Enable for debugging
114 | )
115 |
116 | except Exception as e:
117 | logger.error(f"Server error: {str(e)}")
118 | raise MCPDevServerError(f"Server error: {str(e)}")
119 |
120 | finally:
121 | logger.info("Server shutdown")
122 |
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/prompts/templates.py:
--------------------------------------------------------------------------------
```python
1 | """Input request templates for common scenarios."""
2 | from typing import Dict
3 | from .input_protocol import InputRequest, InputField
4 |
5 | ENVIRONMENT_SETUP = InputRequest(
6 | request_id="environment_setup",
7 | title="Setup Development Environment",
8 | description="Configure your development environment",
9 | fields=[
10 | InputField(
11 | name="language",
12 | type="select",
13 | description="Primary programming language",
14 | options=[
15 | {"value": "python", "label": "Python"},
16 | {"value": "node", "label": "Node.js"},
17 | {"value": "both", "label": "Python & Node.js"}
18 | ]
19 | ),
20 | InputField(
21 | name="python_version",
22 | type="select",
23 | description="Python version",
24 | options=[
25 | {"value": "3.12", "label": "Python 3.12"},
26 | {"value": "3.11", "label": "Python 3.11"},
27 | {"value": "3.10", "label": "Python 3.10"}
28 | ],
29 | required=False
30 | ),
31 | InputField(
32 | name="node_version",
33 | type="select",
34 | description="Node.js version",
35 | options=[
36 | {"value": "20", "label": "Node.js 20 LTS"},
37 | {"value": "18", "label": "Node.js 18 LTS"}
38 | ],
39 | required=False
40 | ),
41 | InputField(
42 | name="include_docker",
43 | type="confirm",
44 | description="Include Docker support?",
45 | default=False
46 | )
47 | ]
48 | )
49 |
50 | TEST_CONFIGURATION = InputRequest(
51 | request_id="test_configuration",
52 | title="Configure Test Environment",
53 | description="Set up testing parameters",
54 | fields=[
55 | InputField(
56 | name="test_framework",
57 | type="select",
58 | description="Testing framework",
59 | options=[
60 | {"value": "pytest", "label": "pytest"},
61 | {"value": "unittest", "label": "unittest"},
62 | {"value": "jest", "label": "Jest"},
63 | {"value": "mocha", "label": "Mocha"}
64 | ]
65 | ),
66 | InputField(
67 | name="include_coverage",
68 | type="confirm",
69 | description="Include coverage reporting?",
70 | default=True
71 | ),
72 | InputField(
73 | name="parallel",
74 | type="confirm",
75 | description="Run tests in parallel?",
76 | default=False
77 | ),
78 | InputField(
79 | name="test_path",
80 | type="text",
81 | description="Test directory or file pattern",
82 | default="tests/",
83 | required=False
84 | )
85 | ]
86 | )
87 |
88 | DEPLOYMENT_CONFIG = InputRequest(
89 | request_id="deployment_config",
90 | title="Configure Deployment",
91 | description="Set up deployment parameters",
92 | fields=[
93 | InputField(
94 | name="environment",
95 | type="select",
96 | description="Deployment environment",
97 | options=[
98 | {"value": "development", "label": "Development"},
99 | {"value": "staging", "label": "Staging"},
100 | {"value": "production", "label": "Production"}
101 | ]
102 | ),
103 | InputField(
104 | name="deploy_method",
105 | type="select",
106 | description="Deployment method",
107 | options=[
108 | {"value": "docker", "label": "Docker Container"},
109 | {"value": "kubernetes", "label": "Kubernetes"},
110 | {"value": "serverless", "label": "Serverless"}
111 | ]
112 | ),
113 | InputField(
114 | name="auto_deploy",
115 | type="confirm",
116 | description="Enable automatic deployment?",
117 | default=False
118 | ),
119 | InputField(
120 | name="rollback_enabled",
121 | type="confirm",
122 | description="Enable automatic rollback?",
123 | default=True
124 | )
125 | ]
126 | )
127 |
128 | DEBUG_CONFIG = InputRequest(
129 | request_id="debug_config",
130 | title="Configure Debugging Session",
131 | description="Set up debugging parameters",
132 | fields=[
133 | InputField(
134 | name="debug_type",
135 | type="select",
136 | description="Type of debugging",
137 | options=[
138 | {"value": "python", "label": "Python Debugger"},
139 | {"value": "node", "label": "Node.js Debugger"},
140 | {"value": "remote", "label": "Remote Debugging"}
141 | ]
142 | ),
143 | InputField(
144 | name="port",
145 | type="number",
146 | description="Debug port",
147 | default=9229,
148 | validation={"min": 1024, "max": 65535}
149 | ),
150 | InputField(
151 | name="break_on_entry",
152 | type="confirm",
153 | description="Break on entry point?",
154 | default=True
155 | )
156 | ]
157 | )
158 |
159 | TEMPLATE_REQUESTS: Dict[str, InputRequest] = {
160 | "environment_setup": ENVIRONMENT_SETUP,
161 | "test_configuration": TEST_CONFIGURATION,
162 | "deployment_config": DEPLOYMENT_CONFIG,
163 | "debug_config": DEBUG_CONFIG
164 | }
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/project_manager/project_types.py:
--------------------------------------------------------------------------------
```python
1 | """Project type definitions and configurations."""
2 | from typing import Dict, Any, List
3 | from enum import Enum
4 |
5 | class BuildSystem(str, Enum):
6 | """Build system types."""
7 | MAVEN = "maven"
8 | GRADLE = "gradle"
9 | NPM = "npm"
10 | YARN = "yarn"
11 | PIP = "pip"
12 | POETRY = "poetry"
13 | DOTNET = "dotnet"
14 | CARGO = "cargo"
15 | GO = "go"
16 | SBT = "sbt"
17 |
18 | class ProjectType:
19 | """Base project type configuration."""
20 |
21 | def __init__(
22 | self,
23 | name: str,
24 | description: str,
25 | file_structure: Dict[str, Any],
26 | build_systems: List[BuildSystem],
27 | default_build_system: BuildSystem,
28 | config_files: List[str],
29 | environment_variables: Dict[str, str],
30 | docker_templates: List[str],
31 | input_templates: List[str]
32 | ):
33 | self.name = name
34 | self.description = description
35 | self.file_structure = file_structure
36 | self.build_systems = build_systems
37 | self.default_build_system = default_build_system
38 | self.config_files = config_files
39 | self.environment_variables = environment_variables
40 | self.docker_templates = docker_templates
41 | self.input_templates = input_templates
42 |
43 | # Define standard project types
44 | JAVA_PROJECT = ProjectType(
45 | name="java",
46 | description="Java project",
47 | file_structure={
48 | "src/": {
49 | "main/": {
50 | "java/": {},
51 | "resources/": {}
52 | },
53 | "test/": {
54 | "java/": {},
55 | "resources/": {}
56 | }
57 | },
58 | "target/": {},
59 | },
60 | build_systems=[BuildSystem.MAVEN, BuildSystem.GRADLE],
61 | default_build_system=BuildSystem.MAVEN,
62 | config_files=["pom.xml", "build.gradle", ".gitignore", "README.md"],
63 | environment_variables={
64 | "JAVA_HOME": "",
65 | "MAVEN_HOME": "",
66 | "GRADLE_HOME": ""
67 | },
68 | docker_templates=["java-maven", "java-gradle"],
69 | input_templates=["java_config", "maven_config", "gradle_config"]
70 | )
71 |
72 | DOTNET_PROJECT = ProjectType(
73 | name="dotnet",
74 | description=".NET project",
75 | file_structure={
76 | "src/": {},
77 | "tests/": {},
78 | "docs/": {}
79 | },
80 | build_systems=[BuildSystem.DOTNET],
81 | default_build_system=BuildSystem.DOTNET,
82 | config_files=[".csproj", ".sln", "global.json", ".gitignore", "README.md"],
83 | environment_variables={
84 | "DOTNET_ROOT": "",
85 | "ASPNETCORE_ENVIRONMENT": "Development"
86 | },
87 | docker_templates=["dotnet-sdk", "dotnet-runtime"],
88 | input_templates=["dotnet_config", "aspnet_config"]
89 | )
90 |
91 | NODE_PROJECT = ProjectType(
92 | name="node",
93 | description="Node.js project",
94 | file_structure={
95 | "src/": {},
96 | "tests/": {},
97 | "dist/": {},
98 | "public/": {}
99 | },
100 | build_systems=[BuildSystem.NPM, BuildSystem.YARN],
101 | default_build_system=BuildSystem.NPM,
102 | config_files=["package.json", "tsconfig.json", ".gitignore", "README.md"],
103 | environment_variables={
104 | "NODE_ENV": "development",
105 | "NPM_TOKEN": ""
106 | },
107 | docker_templates=["node-dev", "node-prod"],
108 | input_templates=["node_config", "npm_config", "typescript_config"]
109 | )
110 |
111 | PYTHON_PROJECT = ProjectType(
112 | name="python",
113 | description="Python project",
114 | file_structure={
115 | "src/": {},
116 | "tests/": {},
117 | "docs/": {},
118 | "notebooks/": {}
119 | },
120 | build_systems=[BuildSystem.PIP, BuildSystem.POETRY],
121 | default_build_system=BuildSystem.POETRY,
122 | config_files=["pyproject.toml", "setup.py", "requirements.txt", ".gitignore", "README.md"],
123 | environment_variables={
124 | "PYTHONPATH": "src",
125 | "PYTHON_ENV": "development"
126 | },
127 | docker_templates=["python-dev", "python-prod"],
128 | input_templates=["python_config", "poetry_config", "pytest_config"]
129 | )
130 |
131 | GOLANG_PROJECT = ProjectType(
132 | name="golang",
133 | description="Go project",
134 | file_structure={
135 | "cmd/": {},
136 | "internal/": {},
137 | "pkg/": {},
138 | "api/": {}
139 | },
140 | build_systems=[BuildSystem.GO],
141 | default_build_system=BuildSystem.GO,
142 | config_files=["go.mod", "go.sum", ".gitignore", "README.md"],
143 | environment_variables={
144 | "GOPATH": "",
145 | "GO111MODULE": "on"
146 | },
147 | docker_templates=["golang-dev", "golang-prod"],
148 | input_templates=["golang_config", "go_mod_config"]
149 | )
150 |
151 | RUST_PROJECT = ProjectType(
152 | name="rust",
153 | description="Rust project",
154 | file_structure={
155 | "src/": {},
156 | "tests/": {},
157 | "benches/": {},
158 | "examples/": {}
159 | },
160 | build_systems=[BuildSystem.CARGO],
161 | default_build_system=BuildSystem.CARGO,
162 | config_files=["Cargo.toml", "Cargo.lock", ".gitignore", "README.md"],
163 | environment_variables={
164 | "RUST_BACKTRACE": "1",
165 | "CARGO_HOME": ""
166 | },
167 | docker_templates=["rust-dev", "rust-prod"],
168 | input_templates=["rust_config", "cargo_config"]
169 | )
170 |
171 | # Map of all available project types
172 | PROJECT_TYPES: Dict[str, ProjectType] = {
173 | "java": JAVA_PROJECT,
174 | "dotnet": DOTNET_PROJECT,
175 | "node": NODE_PROJECT,
176 | "python": PYTHON_PROJECT,
177 | "golang": GOLANG_PROJECT,
178 | "rust": RUST_PROJECT
179 | }
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/project_manager/git.py:
--------------------------------------------------------------------------------
```python
1 | """Git integration for MCP Development Server."""
2 | import os
3 | from typing import List, Optional
4 | from git import Repo, GitCommandError
5 | from git.objects import Commit
6 |
7 | from ..utils.logging import setup_logging
8 | from ..utils.errors import GitError
9 |
10 | logger = setup_logging(__name__)
11 |
12 | class GitManager:
13 | """Manages Git operations for a project."""
14 |
15 | def __init__(self, project_path: str):
16 | self.project_path = project_path
17 | self.repo: Optional[Repo] = None
18 |
19 | async def initialize(self) -> None:
20 | """Initialize Git repository."""
21 | try:
22 | self.repo = Repo.init(self.project_path)
23 |
24 | # Create default .gitignore if it doesn't exist
25 | gitignore_path = os.path.join(self.project_path, '.gitignore')
26 | if not os.path.exists(gitignore_path):
27 | with open(gitignore_path, 'w') as f:
28 | f.write('\n'.join([
29 | '# Python',
30 | '__pycache__/',
31 | '*.pyc',
32 | '*.pyo',
33 | '*.pyd',
34 | '.Python',
35 | 'env/',
36 | 'venv/',
37 | '.env',
38 | '.venv',
39 | '',
40 | '# IDE',
41 | '.idea/',
42 | '.vscode/',
43 | '*.swp',
44 | '*.swo',
45 | '',
46 | '# Project specific',
47 | '.mcp/',
48 | 'dist/',
49 | 'build/',
50 | '*.egg-info/',
51 | ''
52 | ]))
53 |
54 | # Initial commit
55 | if not self.repo.heads:
56 | self.repo.index.add(['.gitignore'])
57 | self.repo.index.commit("Initial commit")
58 |
59 | logger.info(f"Initialized Git repository at {self.project_path}")
60 |
61 | except Exception as e:
62 | raise GitError(f"Git initialization failed: {str(e)}")
63 |
64 | async def get_status(self) -> dict:
65 | """Get repository status."""
66 | try:
67 | if not self.repo:
68 | raise GitError("Git repository not initialized")
69 |
70 | return {
71 | "branch": self.repo.active_branch.name,
72 | "changed_files": [item.a_path for item in self.repo.index.diff(None)],
73 | "untracked_files": self.repo.untracked_files,
74 | "is_dirty": self.repo.is_dirty(),
75 | "head_commit": {
76 | "hash": self.repo.head.commit.hexsha,
77 | "message": self.repo.head.commit.message,
78 | "author": str(self.repo.head.commit.author),
79 | "date": str(self.repo.head.commit.authored_datetime)
80 | }
81 | }
82 |
83 | except Exception as e:
84 | raise GitError(f"Failed to get Git status: {str(e)}")
85 |
86 | async def commit(self, message: str, files: Optional[List[str]] = None) -> str:
87 | """Create a new commit."""
88 | try:
89 | if not self.repo:
90 | raise GitError("Git repository not initialized")
91 |
92 | # Add specified files or all changes
93 | if files:
94 | self.repo.index.add(files)
95 | else:
96 | self.repo.index.add('.')
97 |
98 | # Create commit
99 | commit = self.repo.index.commit(message)
100 | logger.info(f"Created commit: {commit.hexsha}")
101 |
102 | return commit.hexsha
103 |
104 | except Exception as e:
105 | raise GitError(f"Failed to create commit: {str(e)}")
106 |
107 | async def get_commit_history(
108 | self,
109 | max_count: Optional[int] = None
110 | ) -> List[dict]:
111 | """Get commit history."""
112 | try:
113 | if not self.repo:
114 | raise GitError("Git repository not initialized")
115 |
116 | commits = []
117 | for commit in self.repo.iter_commits(max_count=max_count):
118 | commits.append({
119 | "hash": commit.hexsha,
120 | "message": commit.message,
121 | "author": str(commit.author),
122 | "date": str(commit.authored_datetime),
123 | "files": list(commit.stats.files.keys())
124 | })
125 |
126 | return commits
127 |
128 | except Exception as e:
129 | raise GitError(f"Failed to get commit history: {str(e)}")
130 |
131 | async def create_branch(self, name: str) -> None:
132 | """Create a new branch."""
133 | try:
134 | if not self.repo:
135 | raise GitError("Git repository not initialized")
136 |
137 | self.repo.create_head(name)
138 | logger.info(f"Created branch: {name}")
139 |
140 | except Exception as e:
141 | raise GitError(f"Failed to create branch: {str(e)}")
142 |
143 | async def checkout(self, branch: str) -> None:
144 | """Checkout a branch."""
145 | try:
146 | if not self.repo:
147 | raise GitError("Git repository not initialized")
148 |
149 | self.repo.git.checkout(branch)
150 | logger.info(f"Checked out branch: {branch}")
151 |
152 | except Exception as e:
153 | raise GitError(f"Failed to checkout branch: {str(e)}")
154 |
155 | async def get_diff(
156 | self,
157 | commit_a: Optional[str] = None,
158 | commit_b: Optional[str] = None
159 | ) -> str:
160 | """Get diff between commits or working directory."""
161 | try:
162 | if not self.repo:
163 | raise GitError("Git repository not initialized")
164 |
165 | return self.repo.git.diff(commit_a, commit_b)
166 |
167 | except Exception as e:
168 | raise GitError(f"Failed to get diff: {str(e)}")
169 |
170 | async def cleanup(self) -> None:
171 | """Clean up Git resources."""
172 | self.repo = None
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/environments/manager.py:
--------------------------------------------------------------------------------
```python
1 | """Environment management for MCP Development Server."""
2 | import os
3 | import json
4 | from typing import Dict, List, Optional, Any
5 | from pathlib import Path
6 |
7 | from ..docker.manager import DockerManager
8 | from ..docker.volumes import VolumeManager
9 | from ..docker.templates import DockerTemplates
10 | from ..utils.logging import setup_logging
11 | from ..utils.errors import EnvironmentError
12 |
13 | logger = setup_logging(__name__)
14 |
15 | class EnvironmentManager:
16 | """Manages development environments."""
17 |
18 | def __init__(self):
19 | self.docker_manager = DockerManager()
20 | self.volume_manager = VolumeManager()
21 | self.environments: Dict[str, Dict[str, Any]] = {}
22 |
23 | async def create_environment(
24 | self,
25 | name: str,
26 | project_path: str,
27 | env_type: str,
28 | config: Optional[Dict[str, Any]] = None
29 | ) -> str:
30 | """Create a new development environment."""
31 | try:
32 | config = config or {}
33 |
34 | # Create environment directory
35 | env_path = os.path.join(project_path, '.mcp', 'environments', name)
36 | os.makedirs(env_path, exist_ok=True)
37 |
38 | # Generate Dockerfile
39 | dockerfile_content = DockerTemplates.get_template(env_type, config)
40 | dockerfile_path = os.path.join(env_path, 'Dockerfile')
41 | with open(dockerfile_path, 'w') as f:
42 | f.write(dockerfile_content)
43 |
44 | # Create volumes for persistence
45 | volumes = {}
46 | for volume_name in ['src', 'deps', 'cache']:
47 | volume = await self.volume_manager.create_volume(
48 | f"mcp-{name}-{volume_name}",
49 | labels={
50 | 'mcp.environment': name,
51 | 'mcp.volume.type': volume_name
52 | }
53 | )
54 | volumes[volume] = {'bind': f'/app/{volume_name}', 'mode': 'rw'}
55 |
56 | # Create container
57 | container_id = await self.docker_manager.create_container(
58 | project_path=project_path,
59 | environment=name,
60 | dockerfile=dockerfile_path,
61 | volumes=volumes,
62 | environment_vars=config.get('env_vars'),
63 | ports=config.get('ports')
64 | )
65 |
66 | # Store environment configuration
67 | self.environments[name] = {
68 | 'id': container_id,
69 | 'type': env_type,
70 | 'path': env_path,
71 | 'config': config,
72 | 'volumes': volumes
73 | }
74 |
75 | # Save environment metadata
76 | self._save_environment_metadata(name)
77 |
78 | logger.info(f"Created environment: {name}")
79 | return container_id
80 |
81 | except Exception as e:
82 | raise EnvironmentError(f"Failed to create environment: {str(e)}")
83 |
84 | async def remove_environment(self, name: str) -> None:
85 | """Remove a development environment."""
86 | try:
87 | if env := self.environments.get(name):
88 | # Stop container
89 | await self.docker_manager.stop_container(name)
90 |
91 | # Remove volumes
92 | for volume in env['volumes']:
93 | await self.volume_manager.remove_volume(volume)
94 |
95 | # Remove environment directory
96 | import shutil
97 | shutil.rmtree(env['path'])
98 |
99 | # Remove from environments dict
100 | del self.environments[name]
101 |
102 | logger.info(f"Removed environment: {name}")
103 | else:
104 | raise EnvironmentError(f"Environment not found: {name}")
105 |
106 | except Exception as e:
107 | raise EnvironmentError(f"Failed to remove environment: {str(e)}")
108 |
109 | async def execute_in_environment(
110 | self,
111 | name: str,
112 | command: str,
113 | workdir: Optional[str] = None
114 | ) -> Dict[str, Any]:
115 | """Execute a command in an environment."""
116 | try:
117 | if name not in self.environments:
118 | raise EnvironmentError(f"Environment not found: {name}")
119 |
120 | return await self.docker_manager.execute_command(
121 | environment=name,
122 | command=command,
123 | workdir=workdir
124 | )
125 |
126 | except Exception as e:
127 | raise EnvironmentError(f"Failed to execute command: {str(e)}")
128 |
129 | async def get_environment_status(self, name: str) -> Dict[str, Any]:
130 | """Get environment status including container and volumes."""
131 | try:
132 | if env := self.environments.get(name):
133 | container_status = await self.docker_manager.get_container_status(name)
134 |
135 | volumes_status = {}
136 | for volume in env['volumes']:
137 | volumes_status[volume] = await self.volume_manager.get_volume_info(volume)
138 |
139 | return {
140 | 'container': container_status,
141 | 'volumes': volumes_status,
142 | 'type': env['type'],
143 | 'config': env['config']
144 | }
145 | else:
146 | raise EnvironmentError(f"Environment not found: {name}")
147 |
148 | except Exception as e:
149 | raise EnvironmentError(f"Failed to get environment status: {str(e)}")
150 |
151 | def _save_environment_metadata(self, name: str) -> None:
152 | """Save environment metadata to disk."""
153 | if env := self.environments.get(name):
154 | metadata_path = os.path.join(env['path'], 'metadata.json')
155 | with open(metadata_path, 'w') as f:
156 | json.dump({
157 | 'name': name,
158 | 'type': env['type'],
159 | 'config': env['config'],
160 | 'volumes': list(env['volumes'].keys())
161 | }, f, indent=2)
162 |
163 | async def cleanup(self) -> None:
164 | """Clean up all environments."""
165 | for name in list(self.environments.keys()):
166 | try:
167 | await self.remove_environment(name)
168 | except Exception as e:
169 | logger.error(f"Error cleaning up environment {name}: {str(e)}")
170 |
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/prompts/project_templates.py:
--------------------------------------------------------------------------------
```python
1 | """Project-specific input templates."""
2 | from typing import Dict
3 | from .input_protocol import InputRequest, InputField
4 |
5 | # Java Project Templates
6 | JAVA_CONFIG = InputRequest(
7 | request_id="java_config",
8 | title="Java Project Configuration",
9 | description="Configure Java project settings",
10 | fields=[
11 | InputField(
12 | name="java_version",
13 | type="select",
14 | description="Java version",
15 | options=[
16 | {"value": "21", "label": "Java 21 (LTS)"},
17 | {"value": "17", "label": "Java 17 (LTS)"},
18 | {"value": "11", "label": "Java 11 (LTS)"},
19 | {"value": "8", "label": "Java 8"}
20 | ]
21 | ),
22 | InputField(
23 | name="project_type",
24 | type="select",
25 | description="Project type",
26 | options=[
27 | {"value": "spring-boot", "label": "Spring Boot"},
28 | {"value": "jakarta-ee", "label": "Jakarta EE"},
29 | {"value": "android", "label": "Android"},
30 | {"value": "library", "label": "Java Library"}
31 | ]
32 | ),
33 | InputField(
34 | name="packaging",
35 | type="select",
36 | description="Packaging type",
37 | options=[
38 | {"value": "jar", "label": "JAR"},
39 | {"value": "war", "label": "WAR"},
40 | {"value": "ear", "label": "EAR"}
41 | ]
42 | )
43 | ]
44 | )
45 |
46 | # .NET Project Templates
47 | DOTNET_CONFIG = InputRequest(
48 | request_id="dotnet_config",
49 | title=".NET Project Configuration",
50 | description="Configure .NET project settings",
51 | fields=[
52 | InputField(
53 | name="dotnet_version",
54 | type="select",
55 | description=".NET version",
56 | options=[
57 | {"value": "8.0", "label": ".NET 8.0"},
58 | {"value": "7.0", "label": ".NET 7.0"},
59 | {"value": "6.0", "label": ".NET 6.0 (LTS)"}
60 | ]
61 | ),
62 | InputField(
63 | name="project_type",
64 | type="select",
65 | description="Project type",
66 | options=[
67 | {"value": "webapi", "label": "ASP.NET Core Web API"},
68 | {"value": "mvc", "label": "ASP.NET Core MVC"},
69 | {"value": "blazor", "label": "Blazor"},
70 | {"value": "maui", "label": ".NET MAUI"},
71 | {"value": "library", "label": "Class Library"}
72 | ]
73 | ),
74 | InputField(
75 | name="authentication",
76 | type="select",
77 | description="Authentication type",
78 | options=[
79 | {"value": "none", "label": "None"},
80 | {"value": "individual", "label": "Individual Accounts"},
81 | {"value": "microsoft", "label": "Microsoft Identity Platform"},
82 | {"value": "windows", "label": "Windows Authentication"}
83 | ]
84 | )
85 | ]
86 | )
87 |
88 | # Node.js Project Templates
89 | NODE_CONFIG = InputRequest(
90 | request_id="node_config",
91 | title="Node.js Project Configuration",
92 | description="Configure Node.js project settings",
93 | fields=[
94 | InputField(
95 | name="node_version",
96 | type="select",
97 | description="Node.js version",
98 | options=[
99 | {"value": "20", "label": "Node.js 20 (LTS)"},
100 | {"value": "18", "label": "Node.js 18 (LTS)"}
101 | ]
102 | ),
103 | InputField(
104 | name="project_type",
105 | type="select",
106 | description="Project type",
107 | options=[
108 | {"value": "express", "label": "Express.js"},
109 | {"value": "next", "label": "Next.js"},
110 | {"value": "nest", "label": "NestJS"},
111 | {"value": "library", "label": "NPM Package"}
112 | ]
113 | ),
114 | InputField(
115 | name="typescript",
116 | type="confirm",
117 | description="Use TypeScript?",
118 | default=True
119 | )
120 | ]
121 | )
122 |
123 | # Python Project Templates
124 | PYTHON_CONFIG = InputRequest(
125 | request_id="python_config",
126 | title="Python Project Configuration",
127 | description="Configure Python project settings",
128 | fields=[
129 | InputField(
130 | name="python_version",
131 | type="select",
132 | description="Python version",
133 | options=[
134 | {"value": "3.12", "label": "Python 3.12"},
135 | {"value": "3.11", "label": "Python 3.11"},
136 | {"value": "3.10", "label": "Python 3.10"}
137 | ]
138 | ),
139 | InputField(
140 | name="project_type",
141 | type="select",
142 | description="Project type",
143 | options=[
144 | {"value": "fastapi", "label": "FastAPI"},
145 | {"value": "django", "label": "Django"},
146 | {"value": "flask", "label": "Flask"},
147 | {"value": "library", "label": "Python Package"}
148 | ]
149 | ),
150 | InputField(
151 | name="dependency_management",
152 | type="select",
153 | description="Dependency management",
154 | options=[
155 | {"value": "poetry", "label": "Poetry"},
156 | {"value": "pip", "label": "pip + requirements.txt"},
157 | {"value": "pipenv", "label": "Pipenv"}
158 | ]
159 | )
160 | ]
161 | )
162 |
163 | # Golang Project Templates
164 | GOLANG_CONFIG = InputRequest(
165 | request_id="golang_config",
166 | title="Go Project Configuration",
167 | description="Configure Go project settings",
168 | fields=[
169 | InputField(
170 | name="go_version",
171 | type="select",
172 | description="Go version",
173 | options=[
174 | {"value": "1.22", "label": "Go 1.22"},
175 | {"value": "1.21", "label": "Go 1.21"},
176 | {"value": "1.20", "label": "Go 1.20"}
177 | ]
178 | ),
179 | InputField(
180 | name="project_type",
181 | type="select",
182 | description="Project type",
183 | options=[
184 | {"value": "gin", "label": "Gin Web Framework"},
185 | {"value": "echo", "label": "Echo Framework"},
186 | {"value": "cli", "label": "CLI Application"},
187 | {"value": "library", "label": "Go Module"}
188 | ]
189 | ),
190 | InputField(
191 | name="module_path",
192 | type="text",
193 | description="Module path (e.g., github.com/user/repo)",
194 | validation={"pattern": r"^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+(/[a-zA-Z0-9_.-]+)?$"}
195 | )
196 | ]
197 | )
198 |
199 | # All project templates
200 | PROJECT_TEMPLATES: Dict[str, InputRequest] = {
201 | "java_config": JAVA_CONFIG,
202 | "dotnet_config": DOTNET_CONFIG,
203 | "node_config": NODE_CONFIG,
204 | "python_config": PYTHON_CONFIG,
205 | "golang_config": GOLANG_CONFIG
206 | }
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/project_manager/templates.py:
--------------------------------------------------------------------------------
```python
1 | """Template system for project creation."""
2 | import os
3 | import shutil
4 | from pathlib import Path
5 | from typing import Dict, Any, List
6 | import jinja2
7 | import yaml
8 |
9 | from ..utils.logging import setup_logging
10 | from ..utils.errors import ProjectError
11 |
12 | logger = setup_logging(__name__)
13 |
14 | class TemplateManager:
15 | """Manages project templates."""
16 |
17 | def __init__(self):
18 | """Initialize template manager."""
19 | self.template_dir = self._get_template_dir()
20 | self.env = jinja2.Environment(
21 | loader=jinja2.FileSystemLoader(str(self.template_dir)),
22 | autoescape=jinja2.select_autoescape()
23 | )
24 |
25 | def _get_template_dir(self) -> Path:
26 | """Get templates directory path."""
27 | if os.name == "nt": # Windows
28 | template_dir = Path(os.getenv("APPDATA")) / "Claude" / "templates"
29 | else: # macOS/Linux
30 | template_dir = Path.home() / ".config" / "claude" / "templates"
31 |
32 | template_dir.mkdir(parents=True, exist_ok=True)
33 |
34 | # Initialize with basic template if empty
35 | if not any(template_dir.iterdir()):
36 | self._initialize_basic_template(template_dir)
37 |
38 | return template_dir
39 |
40 | def _initialize_basic_template(self, template_dir: Path):
41 | """Initialize basic project template.
42 |
43 | Args:
44 | template_dir: Templates directory path
45 | """
46 | basic_dir = template_dir / "basic"
47 | basic_dir.mkdir(exist_ok=True)
48 |
49 | # Create template configuration
50 | config = {
51 | "name": "basic",
52 | "description": "Basic project template",
53 | "version": "1.0.0",
54 | "files": [
55 | "README.md",
56 | "requirements.txt",
57 | ".gitignore",
58 | "src/__init__.py",
59 | "tests/__init__.py"
60 | ],
61 | "variables": {
62 | "project_name": "",
63 | "description": ""
64 | },
65 | "features": {
66 | "git": True,
67 | "tests": True,
68 | "docker": False
69 | }
70 | }
71 |
72 | with open(basic_dir / "template.yaml", "w") as f:
73 | yaml.dump(config, f)
74 |
75 | # Create template files
76 | readme_content = """# {{ project_name }}
77 |
78 | {{ description }}
79 |
80 | ## Installation
81 |
82 | ```bash
83 | pip install -r requirements.txt
84 | ```
85 |
86 | ## Usage
87 |
88 | ```python
89 | from {{ project_name.lower() }} import main
90 | ```
91 |
92 | ## Testing
93 |
94 | ```bash
95 | pytest tests/
96 | ```
97 | """
98 |
99 | with open(basic_dir / "README.md", "w") as f:
100 | f.write(readme_content)
101 |
102 | # Create source directory
103 | src_dir = basic_dir / "src"
104 | src_dir.mkdir(exist_ok=True)
105 |
106 | with open(src_dir / "__init__.py", "w") as f:
107 | f.write('"""{{ project_name }} package."""\n')
108 |
109 | # Create tests directory
110 | tests_dir = basic_dir / "tests"
111 | tests_dir.mkdir(exist_ok=True)
112 |
113 | with open(tests_dir / "__init__.py", "w") as f:
114 | f.write('"""Tests for {{ project_name }}."""\n')
115 |
116 | # Create requirements.txt
117 | with open(basic_dir / "requirements.txt", "w") as f:
118 | f.write("pytest>=7.0.0\n")
119 |
120 | # Create .gitignore
121 | gitignore_content = """__pycache__/
122 | *.py[cod]
123 | *$py.class
124 | *.so
125 | .Python
126 | build/
127 | develop-eggs/
128 | dist/
129 | downloads/
130 | eggs/
131 | .eggs/
132 | lib/
133 | lib64/
134 | parts/
135 | sdist/
136 | var/
137 | wheels/
138 | *.egg-info/
139 | .installed.cfg
140 | *.egg
141 | MANIFEST
142 | """
143 |
144 | with open(basic_dir / ".gitignore", "w") as f:
145 | f.write(gitignore_content)
146 |
147 | async def apply_template(self, template_name: str, project: Any) -> None:
148 | """Apply template to project.
149 |
150 | Args:
151 | template_name: Name of template to apply
152 | project: Project instance
153 | """
154 | try:
155 | template_path = self.template_dir / template_name
156 | if not template_path.exists():
157 | raise ProjectError(f"Template not found: {template_name}")
158 |
159 | # Load template configuration
160 | with open(template_path / "template.yaml", "r") as f:
161 | template_config = yaml.safe_load(f)
162 |
163 | # Prepare template variables
164 | variables = {
165 | "project_name": project.config.name,
166 | "description": project.config.description
167 | }
168 |
169 | # Process each template file
170 | for file_path in template_config["files"]:
171 | template_file = template_path / file_path
172 | if template_file.exists():
173 | # Create target directory if needed
174 | target_path = Path(project.path) / file_path
175 | target_path.parent.mkdir(parents=True, exist_ok=True)
176 |
177 | # Render template content
178 | template = self.env.get_template(f"{template_name}/{file_path}")
179 | content = template.render(**variables)
180 |
181 | # Write rendered content
182 | with open(target_path, "w") as f:
183 | f.write(content)
184 |
185 | logger.info(f"Applied template {template_name} to project {project.config.name}")
186 |
187 | except Exception as e:
188 | logger.error(f"Failed to apply template: {str(e)}")
189 | raise ProjectError(f"Template application failed: {str(e)}")
190 |
191 | async def template_has_git(self, template_name: str) -> bool:
192 | """Check if template includes Git initialization.
193 |
194 | Args:
195 | template_name: Template name
196 |
197 | Returns:
198 | bool: True if template includes Git
199 | """
200 | try:
201 | template_path = self.template_dir / template_name
202 | if not template_path.exists():
203 | return False
204 |
205 | # Load template configuration
206 | with open(template_path / "template.yaml", "r") as f:
207 | template_config = yaml.safe_load(f)
208 |
209 | return template_config.get("features", {}).get("git", False)
210 |
211 | except Exception:
212 | return False
213 |
214 | def list_templates(self) -> List[Dict[str, Any]]:
215 | """Get list of available templates.
216 |
217 | Returns:
218 | List[Dict[str, Any]]: Template information
219 | """
220 | templates = []
221 |
222 | for template_dir in self.template_dir.iterdir():
223 | if template_dir.is_dir():
224 | config_path = template_dir / "template.yaml"
225 | if config_path.exists():
226 | with open(config_path, "r") as f:
227 | config = yaml.safe_load(f)
228 | templates.append(config)
229 |
230 | return templates
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/project_manager/context.py:
--------------------------------------------------------------------------------
```python
1 | """Project context management for MCP Development Server."""
2 | import os
3 | import json
4 | import uuid
5 | from datetime import datetime
6 | from pathlib import Path
7 | from typing import Dict, List, Optional, Any
8 |
9 | from pydantic import BaseModel
10 | from ..utils.config import ProjectConfig
11 | from ..utils.logging import setup_logging
12 | from ..utils.errors import ProjectError, FileOperationError
13 |
14 | logger = setup_logging(__name__)
15 |
16 | class ProjectState(BaseModel):
17 | """Project state tracking."""
18 | initialized: bool = False
19 | last_build_time: Optional[datetime] = None
20 | last_build_status: Optional[str] = None
21 | last_test_time: Optional[datetime] = None
22 | last_test_status: Optional[str] = None
23 | git_initialized: bool = False
24 |
25 | class ProjectContext:
26 | """Manages the context and state of a development project."""
27 |
28 | def __init__(self, config: ProjectConfig):
29 | self.id = str(uuid.uuid4())
30 | self.config = config
31 | self.path = config.path
32 | self.state = ProjectState()
33 | self._file_watchers: Dict[str, Any] = {}
34 |
35 | async def initialize(self) -> None:
36 | """Initialize project structure and state."""
37 | try:
38 | # Create project directory
39 | os.makedirs(self.path, exist_ok=True)
40 |
41 | # Create project structure
42 | await self._create_project_structure()
43 |
44 | # Initialize state file
45 | await self._init_state_file()
46 |
47 | # Set up file watchers
48 | await self._setup_file_watchers()
49 |
50 | self.state.initialized = True
51 | logger.info(f"Initialized project {self.config.name} at {self.path}")
52 |
53 | except Exception as e:
54 | raise ProjectError(f"Project initialization failed: {str(e)}")
55 |
56 | async def _create_project_structure(self) -> None:
57 | """Create initial project directory structure."""
58 | try:
59 | # Create standard directories
60 | for dir_name in ['.mcp', 'src', 'tests', 'docs']:
61 | os.makedirs(os.path.join(self.path, dir_name), exist_ok=True)
62 |
63 | # Create basic configuration files
64 | config_path = os.path.join(self.path, '.mcp', 'project.json')
65 | with open(config_path, 'w') as f:
66 | json.dump(self.config.dict(), f, indent=2, default=str)
67 |
68 | except Exception as e:
69 | raise FileOperationError(f"Failed to create project structure: {str(e)}")
70 |
71 | async def _init_state_file(self) -> None:
72 | """Initialize project state file."""
73 | try:
74 | state_path = os.path.join(self.path, '.mcp', 'state.json')
75 | with open(state_path, 'w') as f:
76 | json.dump(self.state.dict(), f, indent=2, default=str)
77 |
78 | except Exception as e:
79 | raise FileOperationError(f"Failed to initialize state file: {str(e)}")
80 |
81 | async def _setup_file_watchers(self) -> None:
82 | """Set up file system watchers for project directories."""
83 | # To be implemented with file watching functionality
84 | pass
85 |
86 | def get_structure(self) -> Dict[str, Any]:
87 | """Get project structure as a dictionary."""
88 | structure = {"name": self.config.name, "type": "directory", "children": []}
89 |
90 | def scan_directory(path: Path, current_dict: Dict[str, Any]) -> None:
91 | try:
92 | for item in path.iterdir():
93 | # Skip hidden files and .mcp directory
94 | if item.name.startswith('.'):
95 | continue
96 |
97 | if item.is_file():
98 | current_dict["children"].append({
99 | "name": item.name,
100 | "type": "file",
101 | "size": item.stat().st_size
102 | })
103 | elif item.is_dir():
104 | dir_dict = {
105 | "name": item.name,
106 | "type": "directory",
107 | "children": []
108 | }
109 | current_dict["children"].append(dir_dict)
110 | scan_directory(item, dir_dict)
111 |
112 | except Exception as e:
113 | logger.error(f"Error scanning directory {path}: {str(e)}")
114 |
115 | scan_directory(Path(self.path), structure)
116 | return structure
117 |
118 | def get_file_content(self, relative_path: str) -> str:
119 | """Get content of a project file."""
120 | try:
121 | file_path = os.path.join(self.path, relative_path)
122 | if not os.path.exists(file_path):
123 | raise FileOperationError(f"File not found: {relative_path}")
124 |
125 | # Basic security check
126 | if not os.path.normpath(file_path).startswith(str(self.path)):
127 | raise FileOperationError("Invalid file path")
128 |
129 | with open(file_path, 'r') as f:
130 | return f.read()
131 |
132 | except Exception as e:
133 | raise FileOperationError(f"Failed to read file {relative_path}: {str(e)}")
134 |
135 | async def update_file(self, relative_path: str, content: str) -> None:
136 | """Update content of a project file."""
137 | try:
138 | file_path = os.path.join(self.path, relative_path)
139 |
140 | # Create directories if needed
141 | os.makedirs(os.path.dirname(file_path), exist_ok=True)
142 |
143 | # Security check
144 | if not os.path.normpath(file_path).startswith(str(self.path)):
145 | raise FileOperationError("Invalid file path")
146 |
147 | with open(file_path, 'w') as f:
148 | f.write(content)
149 |
150 | logger.info(f"Updated file: {relative_path}")
151 |
152 | except Exception as e:
153 | raise FileOperationError(f"Failed to update file {relative_path}: {str(e)}")
154 |
155 | async def delete_file(self, relative_path: str) -> None:
156 | """Delete a project file."""
157 | try:
158 | file_path = os.path.join(self.path, relative_path)
159 |
160 | # Security check
161 | if not os.path.normpath(file_path).startswith(str(self.path)):
162 | raise FileOperationError("Invalid file path")
163 |
164 | if os.path.exists(file_path):
165 | os.remove(file_path)
166 | logger.info(f"Deleted file: {relative_path}")
167 | else:
168 | logger.warning(f"File not found: {relative_path}")
169 |
170 | except Exception as e:
171 | raise FileOperationError(f"Failed to delete file {relative_path}: {str(e)}")
172 |
173 | async def update_state(self, **kwargs) -> None:
174 | """Update project state."""
175 | try:
176 | # Update state object
177 | for key, value in kwargs.items():
178 | if hasattr(self.state, key):
179 | setattr(self.state, key, value)
180 |
181 | # Save to state file
182 | state_path = os.path.join(self.path, '.mcp', 'state.json')
183 | with open(state_path, 'w') as f:
184 | json.dump(self.state.dict(), f, indent=2, default=str)
185 |
186 | logger.info(f"Updated project state: {kwargs}")
187 |
188 | except Exception as e:
189 | raise ProjectError(f"Failed to update project state: {str(e)}")
190 |
191 | async def cleanup(self) -> None:
192 | """Clean up project resources."""
193 | try:
194 | # Stop file watchers
195 | for watcher in self._file_watchers.values():
196 | await watcher.stop()
197 |
198 | logger.info(f"Cleaned up project resources for {self.config.name}")
199 |
200 | except Exception as e:
201 | logger.error(f"Error during project cleanup: {str(e)}")
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/docker/manager.py:
--------------------------------------------------------------------------------
```python
1 | """Docker integration for MCP Development Server."""
2 | import asyncio
3 | import docker
4 | from typing import Dict, Any, Optional, List
5 | from pathlib import Path
6 | import tempfile
7 | import yaml
8 | import jinja2
9 |
10 | from ..utils.logging import setup_logging
11 | from ..utils.errors import MCPDevServerError
12 |
13 | logger = setup_logging(__name__)
14 |
15 | class DockerManager:
16 | """Manages Docker containers and environments."""
17 |
18 | def __init__(self):
19 | """Initialize Docker manager."""
20 | self.client = docker.from_env()
21 | self.active_containers: Dict[str, Any] = {}
22 | self._setup_template_environment()
23 |
24 | def _setup_template_environment(self):
25 | """Set up Jinja2 template environment."""
26 | template_dir = Path(__file__).parent / "templates"
27 | self.template_env = jinja2.Environment(
28 | loader=jinja2.FileSystemLoader(str(template_dir)),
29 | autoescape=jinja2.select_autoescape()
30 | )
31 |
32 | async def create_environment(
33 | self,
34 | name: str,
35 | image: str,
36 | project_path: str,
37 | env_vars: Optional[Dict[str, str]] = None,
38 | ports: Optional[Dict[str, str]] = None,
39 | volumes: Optional[Dict[str, Dict[str, str]]] = None
40 | ) -> str:
41 | """Create a new Docker environment.
42 |
43 | Args:
44 | name: Environment name
45 | image: Docker image name
46 | project_path: Project directory path
47 | env_vars: Environment variables
48 | ports: Port mappings
49 | volumes: Additional volume mappings
50 |
51 | Returns:
52 | str: Environment ID
53 | """
54 | try:
55 | # Ensure image is available
56 | try:
57 | self.client.images.get(image)
58 | except docker.errors.ImageNotFound:
59 | logger.info(f"Pulling image: {image}")
60 | self.client.images.pull(image)
61 |
62 | # Setup default volumes
63 | container_volumes = {
64 | project_path: {
65 | "bind": "/workspace",
66 | "mode": "rw"
67 | }
68 | }
69 | if volumes:
70 | container_volumes.update(volumes)
71 |
72 | # Create container
73 | container = self.client.containers.run(
74 | image=image,
75 | name=f"mcp-env-{name}",
76 | detach=True,
77 | volumes=container_volumes,
78 | environment=env_vars or {},
79 | ports=ports or {},
80 | working_dir="/workspace",
81 | remove=True
82 | )
83 |
84 | env_id = container.id
85 | self.active_containers[env_id] = {
86 | "name": name,
87 | "container": container,
88 | "status": "running"
89 | }
90 |
91 | logger.info(f"Created environment: {name} ({env_id})")
92 | return env_id
93 |
94 | except Exception as e:
95 | logger.error(f"Failed to create environment: {str(e)}")
96 | raise MCPDevServerError(f"Environment creation failed: {str(e)}")
97 |
98 | async def generate_dockerfile(
99 | self,
100 | template: str,
101 | variables: Dict[str, Any],
102 | output_path: Optional[str] = None
103 | ) -> str:
104 | """Generate Dockerfile from template.
105 |
106 | Args:
107 | template: Template name
108 | variables: Template variables
109 | output_path: Optional path to save Dockerfile
110 |
111 | Returns:
112 | str: Generated Dockerfile content
113 | """
114 | try:
115 | template = self.template_env.get_template(f"{template}.dockerfile")
116 | content = template.render(**variables)
117 |
118 | if output_path:
119 | with open(output_path, "w") as f:
120 | f.write(content)
121 |
122 | return content
123 |
124 | except Exception as e:
125 | logger.error(f"Failed to generate Dockerfile: {str(e)}")
126 | raise MCPDevServerError(f"Dockerfile generation failed: {str(e)}")
127 |
128 | async def create_compose_config(
129 | self,
130 | name: str,
131 | services: Dict[str, Any],
132 | output_path: Optional[str] = None
133 | ) -> str:
134 | """Create Docker Compose configuration.
135 |
136 | Args:
137 | name: Project name
138 | services: Service configurations
139 | output_path: Optional path to save docker-compose.yml
140 |
141 | Returns:
142 | str: Generated docker-compose.yml content
143 | """
144 | try:
145 | compose_config = {
146 | "version": "3.8",
147 | "services": services,
148 | "networks": {
149 | "mcp-network": {
150 | "driver": "bridge"
151 | }
152 | }
153 | }
154 |
155 | content = yaml.dump(compose_config, default_flow_style=False)
156 |
157 | if output_path:
158 | with open(output_path, "w") as f:
159 | f.write(content)
160 |
161 | return content
162 |
163 | except Exception as e:
164 | logger.error(f"Failed to create Docker Compose config: {str(e)}")
165 | raise MCPDevServerError(f"Compose config creation failed: {str(e)}")
166 |
167 | async def execute_command(
168 | self,
169 | env_id: str,
170 | command: str,
171 | workdir: Optional[str] = None,
172 | stream: bool = False
173 | ) -> Dict[str, Any]:
174 | """Execute command in Docker environment.
175 |
176 | Args:
177 | env_id: Environment ID
178 | command: Command to execute
179 | workdir: Working directory
180 | stream: Stream output in real-time
181 |
182 | Returns:
183 | Dict[str, Any]: Command execution results
184 | """
185 | try:
186 | if env_id not in self.active_containers:
187 | raise MCPDevServerError(f"Environment not found: {env_id}")
188 |
189 | container = self.active_containers[env_id]["container"]
190 | exec_result = container.exec_run(
191 | command,
192 | workdir=workdir or "/workspace",
193 | stream=True
194 | )
195 |
196 | if stream:
197 | output = []
198 | for line in exec_result.output:
199 | decoded_line = line.decode().strip()
200 | output.append(decoded_line)
201 | yield decoded_line
202 |
203 | return {
204 | "exit_code": exec_result.exit_code,
205 | "output": output
206 | }
207 | else:
208 | output = []
209 | for line in exec_result.output:
210 | output.append(line.decode().strip())
211 |
212 | return {
213 | "exit_code": exec_result.exit_code,
214 | "output": output
215 | }
216 |
217 | except Exception as e:
218 | logger.error(f"Command execution failed: {str(e)}")
219 | raise MCPDevServerError(f"Command execution failed: {str(e)}")
220 |
221 | async def cleanup(self):
222 | """Clean up Docker resources."""
223 | try:
224 | for env_id in list(self.active_containers.keys()):
225 | await self.destroy_environment(env_id)
226 |
227 | except Exception as e:
228 | logger.error(f"Docker cleanup failed: {str(e)}")
229 | raise MCPDevServerError(f"Docker cleanup failed: {str(e)}")
230 |
231 | def get_logs(self, env_id: str, tail: Optional[int] = None) -> str:
232 | """Get container logs.
233 |
234 | Args:
235 | env_id: Environment ID
236 | tail: Number of lines to return from the end
237 |
238 | Returns:
239 | str: Container logs
240 | """
241 | try:
242 | if env_id not in self.active_containers:
243 | raise MCPDevServerError(f"Environment not found: {env_id}")
244 |
245 | container = self.active_containers[env_id]["container"]
246 | return container.logs(tail=tail).decode()
247 |
248 | except Exception as e:
249 | logger.error(f"Failed to get logs: {str(e)}")
250 | raise MCPDevServerError(f"Log retrieval failed: {str(e)}")
251 |
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/project_manager/project.py:
--------------------------------------------------------------------------------
```python
1 | """Project representation and management."""
2 | import uuid
3 | from typing import Dict, Any, Optional, List
4 | from pathlib import Path
5 | import git
6 | from pydantic import BaseModel
7 |
8 | class ProjectConfig(BaseModel):
9 | """Project configuration model."""
10 |
11 | name: str
12 | template: str
13 | description: str = ""
14 | version: str = "0.1.0"
15 |
16 | class ProjectState:
17 | """Project state tracking."""
18 |
19 | def __init__(self):
20 | """Initialize project state."""
21 | self.git_initialized: bool = False
22 | self.last_build: Optional[Dict[str, Any]] = None
23 | self.last_test_run: Optional[Dict[str, Any]] = None
24 | self.active_environments: List[str] = []
25 |
26 | class Project:
27 | """Project instance representation."""
28 |
29 | def __init__(self, path: str, config: ProjectConfig, state: ProjectState):
30 | """Initialize project instance.
31 |
32 | Args:
33 | path: Project directory path
34 | config: Project configuration
35 | state: Project state
36 | """
37 | self.id = str(uuid.uuid4())
38 | self.path = path
39 | self.config = config
40 | self.state = state
41 |
42 | def get_structure(self) -> Dict[str, Any]:
43 | """Get project directory structure.
44 |
45 | Returns:
46 | Dict[str, Any]: Directory structure
47 | """
48 | def scan_dir(path: Path) -> Dict[str, Any]:
49 | structure = {}
50 |
51 | for item in path.iterdir():
52 | if item.name.startswith("."):
53 | continue
54 |
55 | if item.is_file():
56 | structure[item.name] = "file"
57 | elif item.is_dir():
58 | structure[item.name] = scan_dir(item)
59 |
60 | return structure
61 |
62 | return scan_dir(Path(self.path))
63 |
64 | def get_git_status(self) -> Dict[str, Any]:
65 | """Get Git repository status.
66 |
67 | Returns:
68 | Dict[str, Any]: Git status information
69 | """
70 | if not self.state.git_initialized:
71 | return {"initialized": False}
72 |
73 | try:
74 | repo = git.Repo(self.path)
75 | return {
76 | "initialized": True,
77 | "branch": repo.active_branch.name,
78 | "changed_files": [item.a_path for item in repo.index.diff(None)],
79 | "untracked_files": repo.untracked_files,
80 | "ahead": sum(1 for c in repo.iter_commits("origin/main..main")),
81 | "behind": sum(1 for c in repo.iter_commits("main..origin/main"))
82 | }
83 | except Exception as e:
84 | return {
85 | "initialized": False,
86 | "error": str(e)
87 | }
88 |
89 | async def create_git_commit(self, message: str, files: Optional[List[str]] = None) -> Dict[str, Any]:
90 | """Create a Git commit.
91 |
92 | Args:
93 | message: Commit message
94 | files: Optional list of files to commit
95 |
96 | Returns:
97 | Dict[str, Any]: Commit information
98 | """
99 | if not self.state.git_initialized:
100 | raise ValueError("Git is not initialized for this project")
101 |
102 | try:
103 | repo = git.Repo(self.path)
104 |
105 | if files:
106 | repo.index.add(files)
107 | else:
108 | repo.index.add("*")
109 |
110 | commit = repo.index.commit(message)
111 |
112 | return {
113 | "commit_id": commit.hexsha,
114 | "message": message,
115 | "author": str(commit.author),
116 | "files": [item.a_path for item in commit.stats.files]
117 | }
118 | except Exception as e:
119 | raise ValueError(f"Failed to create commit: {str(e)}")
120 |
121 | def get_dependencies(self) -> Dict[str, Any]:
122 | """Get project dependencies.
123 |
124 | Returns:
125 | Dict[str, Any]: Dependency information
126 | """
127 | dependencies = {}
128 |
129 | # Check Python dependencies
130 | req_file = Path(self.path) / "requirements.txt"
131 | if req_file.exists():
132 | with open(req_file, "r") as f:
133 | dependencies["python"] = f.read().splitlines()
134 |
135 | # Check Node.js dependencies
136 | package_file = Path(self.path) / "package.json"
137 | if package_file.exists():
138 | import json
139 | with open(package_file, "r") as f:
140 | package_data = json.load(f)
141 | dependencies["node"] = {
142 | "dependencies": package_data.get("dependencies", {}),
143 | "devDependencies": package_data.get("devDependencies", {})
144 | }
145 |
146 | return dependencies
147 |
148 | def analyze_code(self) -> Dict[str, Any]:
149 | """Analyze project code.
150 |
151 | Returns:
152 | Dict[str, Any]: Code analysis results
153 | """
154 | analysis = {
155 | "files": {},
156 | "summary": {
157 | "total_files": 0,
158 | "total_lines": 0,
159 | "code_lines": 0,
160 | "comment_lines": 0,
161 | "blank_lines": 0
162 | }
163 | }
164 |
165 | def analyze_file(path: Path) -> Dict[str, Any]:
166 | with open(path, "r", encoding="utf-8") as f:
167 | lines = f.readlines()
168 |
169 | total_lines = len(lines)
170 | blank_lines = sum(1 for line in lines if not line.strip())
171 | comment_lines = sum(1 for line in lines if line.strip().startswith("#"))
172 | code_lines = total_lines - blank_lines - comment_lines
173 |
174 | return {
175 | "total_lines": total_lines,
176 | "code_lines": code_lines,
177 | "comment_lines": comment_lines,
178 | "blank_lines": blank_lines
179 | }
180 |
181 | for root, _, files in os.walk(self.path):
182 | for file in files:
183 | if file.endswith(".py"):
184 | file_path = Path(root) / file
185 | try:
186 | file_analysis = analyze_file(file_path)
187 | relative_path = str(file_path.relative_to(self.path))
188 | analysis["files"][relative_path] = file_analysis
189 |
190 | # Update summary
191 | for key in ["total_lines", "code_lines", "comment_lines", "blank_lines"]:
192 | analysis["summary"][key] += file_analysis[key]
193 |
194 | analysis["summary"]["total_files"] += 1
195 | except Exception:
196 | continue
197 |
198 | return analysis
199 |
200 | def get_test_coverage(self) -> Dict[str, Any]:
201 | """Get test coverage information.
202 |
203 | Returns:
204 | Dict[str, Any]: Test coverage data
205 | """
206 | try:
207 | import coverage
208 |
209 | cov = coverage.Coverage()
210 | cov.load()
211 |
212 | return {
213 | "total_coverage": cov.report(),
214 | "missing_lines": dict(cov.analysis2()),
215 | "branch_coverage": cov.get_option("branch"),
216 | "excluded_lines": cov.get_exclude_list()
217 | }
218 | except Exception:
219 | return {
220 | "error": "Coverage data not available"
221 | }
222 |
223 | def get_ci_config(self) -> Dict[str, Any]:
224 | """Get CI configuration.
225 |
226 | Returns:
227 | Dict[str, Any]: CI configuration data
228 | """
229 | ci_configs = {}
230 |
231 | # Check GitHub Actions
232 | github_dir = Path(self.path) / ".github" / "workflows"
233 | if github_dir.exists():
234 | ci_configs["github_actions"] = []
235 | for workflow in github_dir.glob("*.yml"):
236 | with open(workflow, "r") as f:
237 | ci_configs["github_actions"].append({
238 | "name": workflow.stem,
239 | "config": f.read()
240 | })
241 |
242 | # Check GitLab CI
243 | gitlab_file = Path(self.path) / ".gitlab-ci.yml"
244 | if gitlab_file.exists():
245 | with open(gitlab_file, "r") as f:
246 | ci_configs["gitlab"] = f.read()
247 |
248 | return ci_configs
249 |
250 | async def cleanup(self):
251 | """Clean up project resources."""
252 | # Implementation will depend on what resources need cleanup
253 | pass
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/core/server.py:
--------------------------------------------------------------------------------
```python
1 | import asyncio
2 | import json
3 | import websockets
4 | from typing import Callable, Any, Dict, Optional
5 | import logging
6 | import traceback
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 | class Server:
11 | """Core server class implementing JSON-RPC 2.0 protocol."""
12 |
13 | def __init__(self, name: str):
14 | """Initialize the server.
15 |
16 | Args:
17 | name: Server name
18 | """
19 | self.name = name
20 | self.websocket = None
21 | self.input_request_handlers = {}
22 | self.input_response_handlers = {}
23 | self.initialized = False
24 | self.capabilities = {}
25 |
26 | async def start(self, host: str = "localhost", port: int = 8000):
27 | """Start the WebSocket server.
28 |
29 | Args:
30 | host: Host to bind to
31 | port: Port to listen on
32 | """
33 | async def handler(websocket, path):
34 | self.websocket = websocket
35 | try:
36 | logger.info(f"New WebSocket connection from {websocket.remote_address}")
37 | async for message in websocket:
38 | response = None
39 | try:
40 | # Parse JSON-RPC message
41 | data = json.loads(message)
42 | if not isinstance(data, dict):
43 | raise ValueError("Invalid JSON-RPC message")
44 |
45 | # Handle message
46 | response = await self.handle_jsonrpc(data)
47 |
48 | except json.JSONDecodeError as e:
49 | logger.error(f"JSON decode error: {str(e)}")
50 | response = {
51 | "jsonrpc": "2.0",
52 | "error": {
53 | "code": -32700,
54 | "message": "Parse error",
55 | "data": str(e)
56 | },
57 | "id": None
58 | }
59 |
60 | except Exception as e:
61 | logger.error(f"Error handling message: {str(e)}", exc_info=True)
62 | response = {
63 | "jsonrpc": "2.0",
64 | "error": {
65 | "code": -32603,
66 | "message": "Internal error",
67 | "data": {
68 | "error": str(e),
69 | "traceback": traceback.format_exc()
70 | }
71 | },
72 | "id": getattr(data, "id", None) if isinstance(data, dict) else None
73 | }
74 |
75 | # Ensure we always send a properly formatted JSON-RPC response
76 | if response:
77 | try:
78 | if not isinstance(response, dict):
79 | response = {"result": response}
80 |
81 | response["jsonrpc"] = "2.0"
82 | if isinstance(data, dict) and "id" in data:
83 | response["id"] = data["id"]
84 |
85 | # Validate JSON before sending
86 | response_str = json.dumps(response)
87 | await websocket.send(response_str)
88 |
89 | except Exception as e:
90 | logger.error(f"Error sending response: {str(e)}", exc_info=True)
91 | error_response = {
92 | "jsonrpc": "2.0",
93 | "error": {
94 | "code": -32603,
95 | "message": "Error sending response",
96 | "data": str(e)
97 | },
98 | "id": data.get("id") if isinstance(data, dict) else None
99 | }
100 | await websocket.send(json.dumps(error_response))
101 |
102 | except websockets.exceptions.ConnectionClosed:
103 | logger.info("WebSocket connection closed")
104 | finally:
105 | self.websocket = None
106 |
107 | try:
108 | self.server = await websockets.serve(
109 | handler,
110 | host,
111 | port,
112 | ping_interval=20,
113 | ping_timeout=20
114 | )
115 | logger.info(f"Server started on ws://{host}:{port}")
116 | except Exception as e:
117 | logger.error(f"Failed to start server: {str(e)}", exc_info=True)
118 | raise
119 |
120 | async def handle_jsonrpc(self, data: Dict) -> Optional[Dict]:
121 | """Handle JSON-RPC message.
122 |
123 | Args:
124 | data: Parsed JSON-RPC message
125 |
126 | Returns:
127 | Optional response message
128 | """
129 | try:
130 | method = data.get("method")
131 | params = data.get("params", {})
132 |
133 | logger.info(f"Handling method: {method} with params: {params}")
134 |
135 | if method == "initialize":
136 | self.capabilities = params.get("capabilities", {})
137 | self.initialized = True
138 | return {
139 | "result": {
140 | "capabilities": self.capabilities
141 | }
142 | }
143 |
144 | if not self.initialized:
145 | return {
146 | "error": {
147 | "code": -32002,
148 | "message": "Server not initialized"
149 | }
150 | }
151 |
152 | if method == "input/request":
153 | handler = self.input_request_handlers.get("input_request")
154 | if handler:
155 | try:
156 | result = await handler(
157 | params.get("type", ""),
158 | params.get("context", {})
159 | )
160 | return {"result": result}
161 | except Exception as e:
162 | logger.error(f"Error in input request handler: {str(e)}", exc_info=True)
163 | return {
164 | "error": {
165 | "code": -32000,
166 | "message": str(e),
167 | "data": {
168 | "traceback": traceback.format_exc()
169 | }
170 | }
171 | }
172 |
173 | elif method == "input/response":
174 | handler = self.input_response_handlers.get("input_response")
175 | if handler:
176 | try:
177 | await handler(params)
178 | return {"result": None}
179 | except Exception as e:
180 | logger.error(f"Error in input response handler: {str(e)}", exc_info=True)
181 | return {
182 | "error": {
183 | "code": -32000,
184 | "message": str(e),
185 | "data": {
186 | "traceback": traceback.format_exc()
187 | }
188 | }
189 | }
190 |
191 | return {
192 | "error": {
193 | "code": -32601,
194 | "message": f"Method not found: {method}"
195 | }
196 | }
197 |
198 | except Exception as e:
199 | logger.error(f"Error in handle_jsonrpc: {str(e)}", exc_info=True)
200 | return {
201 | "error": {
202 | "code": -32603,
203 | "message": "Internal error",
204 | "data": {
205 | "error": str(e),
206 | "traceback": traceback.format_exc()
207 | }
208 | }
209 | }
210 |
211 | def request_input(self) -> Callable:
212 | """Decorator for input request handlers."""
213 | def decorator(func: Callable) -> Callable:
214 | self.input_request_handlers["input_request"] = func
215 | return func
216 | return decorator
217 |
218 | def handle_input(self) -> Callable:
219 | """Decorator for input response handlers."""
220 | def decorator(func: Callable) -> Callable:
221 | self.input_response_handlers["input_response"] = func
222 | return func
223 | return decorator
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/project_manager/base_project.py:
--------------------------------------------------------------------------------
```python
1 | """Base project class definition."""
2 | import os
3 | import uuid
4 | import xml.etree.ElementTree as ET
5 | import json
6 | import tomli
7 | from pathlib import Path
8 | from typing import Dict, Any, Optional, List
9 | import git
10 |
11 | from .project_types import ProjectType, BuildSystem
12 | from ..utils.errors import ProjectError
13 | from ..utils.logging import setup_logging
14 |
15 | logger = setup_logging(__name__)
16 |
17 | class Project:
18 | """Base project class."""
19 |
20 | def __init__(self, path: str, config: Dict[str, Any], project_type: ProjectType):
21 | """Initialize project instance."""
22 | self.id = str(uuid.uuid4())
23 | self.path = path
24 | self.config = config
25 | self.project_type = project_type
26 | self.build_system = BuildSystem(config["build_system"])
27 |
28 | def get_dependencies(self) -> Dict[str, Any]:
29 | """Get project dependencies."""
30 | if self.build_system == BuildSystem.MAVEN:
31 | return self._get_maven_dependencies()
32 | elif self.build_system == BuildSystem.GRADLE:
33 | return self._get_gradle_dependencies()
34 | elif self.build_system in [BuildSystem.NPM, BuildSystem.YARN]:
35 | return self._get_node_dependencies()
36 | elif self.build_system == BuildSystem.POETRY:
37 | return self._get_poetry_dependencies()
38 | elif self.build_system == BuildSystem.DOTNET:
39 | return self._get_dotnet_dependencies()
40 | elif self.build_system == BuildSystem.GO:
41 | return self._get_go_dependencies()
42 | else:
43 | return {}
44 |
45 | def _get_maven_dependencies(self) -> Dict[str, Any]:
46 | """Get Maven project dependencies."""
47 | pom_path = Path(self.path) / "pom.xml"
48 | if not pom_path.exists():
49 | return {}
50 |
51 | try:
52 | tree = ET.parse(pom_path)
53 | root = tree.getroot()
54 | ns = {'maven': 'http://maven.apache.org/POM/4.0.0'}
55 |
56 | dependencies = []
57 | for dep in root.findall('.//maven:dependency', ns):
58 | dependencies.append({
59 | 'groupId': dep.find('maven:groupId', ns).text,
60 | 'artifactId': dep.find('maven:artifactId', ns).text,
61 | 'version': dep.find('maven:version', ns).text if dep.find('maven:version', ns) is not None else None,
62 | 'scope': dep.find('maven:scope', ns).text if dep.find('maven:scope', ns) is not None else 'compile'
63 | })
64 |
65 | return {'maven': dependencies}
66 | except Exception as e:
67 | logger.error(f"Error parsing Maven dependencies: {e}")
68 | return {}
69 |
70 | def _get_node_dependencies(self) -> Dict[str, Any]:
71 | """Get Node.js project dependencies."""
72 | package_path = Path(self.path) / "package.json"
73 | if not package_path.exists():
74 | return {}
75 |
76 | try:
77 | with open(package_path) as f:
78 | package_data = json.load(f)
79 | return {
80 | 'dependencies': package_data.get('dependencies', {}),
81 | 'devDependencies': package_data.get('devDependencies', {})
82 | }
83 | except Exception as e:
84 | logger.error(f"Error parsing Node.js dependencies: {e}")
85 | return {}
86 |
87 | def _get_poetry_dependencies(self) -> Dict[str, Any]:
88 | """Get Poetry project dependencies."""
89 | pyproject_path = Path(self.path) / "pyproject.toml"
90 | if not pyproject_path.exists():
91 | return {}
92 |
93 | try:
94 | with open(pyproject_path, "rb") as f:
95 | pyproject_data = tomli.load(f)
96 | tool_poetry = pyproject_data.get('tool', {}).get('poetry', {})
97 | return {
98 | 'dependencies': tool_poetry.get('dependencies', {}),
99 | 'dev-dependencies': tool_poetry.get('dev-dependencies', {})
100 | }
101 | except Exception as e:
102 | logger.error(f"Error parsing Poetry dependencies: {e}")
103 | return {}
104 |
105 | def _get_dotnet_dependencies(self) -> Dict[str, Any]:
106 | """Get .NET project dependencies."""
107 | try:
108 | # Find all .csproj files
109 | csproj_files = list(Path(self.path).glob("**/*.csproj"))
110 | dependencies = {}
111 |
112 | for csproj in csproj_files:
113 | tree = ET.parse(csproj)
114 | root = tree.getroot()
115 | project_deps = []
116 |
117 | for item_group in root.findall('.//PackageReference'):
118 | project_deps.append({
119 | 'Include': item_group.get('Include'),
120 | 'Version': item_group.get('Version')
121 | })
122 |
123 | dependencies[csproj.stem] = project_deps
124 |
125 | return dependencies
126 | except Exception as e:
127 | logger.error(f"Error parsing .NET dependencies: {e}")
128 | return {}
129 |
130 | def _get_go_dependencies(self) -> Dict[str, Any]:
131 | """Get Go project dependencies."""
132 | go_mod_path = Path(self.path) / "go.mod"
133 | if not go_mod_path.exists():
134 | return {}
135 |
136 | try:
137 | result = subprocess.run(
138 | ['go', 'list', '-m', 'all'],
139 | capture_output=True,
140 | text=True,
141 | cwd=self.path
142 | )
143 | if result.returncode == 0:
144 | dependencies = []
145 | for line in result.stdout.splitlines()[1:]: # Skip first line (module name)
146 | parts = line.split()
147 | if len(parts) >= 2:
148 | dependencies.append({
149 | 'module': parts[0],
150 | 'version': parts[1]
151 | })
152 | return {'modules': dependencies}
153 | except Exception as e:
154 | logger.error(f"Error parsing Go dependencies: {e}")
155 | return {}
156 |
157 | async def update_dependencies(self, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
158 | """Update project dependencies."""
159 | if self.build_system == BuildSystem.MAVEN:
160 | cmd = "mvn versions:use-latest-versions"
161 | elif self.build_system == BuildSystem.GRADLE:
162 | cmd = "./gradlew dependencyUpdates"
163 | elif self.build_system == BuildSystem.NPM:
164 | cmd = "npm update"
165 | elif self.build_system == BuildSystem.YARN:
166 | cmd = "yarn upgrade"
167 | elif self.build_system == BuildSystem.POETRY:
168 | cmd = "poetry update"
169 | elif self.build_system == BuildSystem.DOTNET:
170 | cmd = "dotnet restore"
171 | else:
172 | raise ProjectError(f"Dependency updates not supported for {self.build_system}")
173 |
174 | return await self.execute_command(cmd)
175 |
176 | async def get_project_analysis(self) -> Dict[str, Any]:
177 | """Get project analysis results."""
178 | analysis = {
179 | "structure": self.get_structure(),
180 | "dependencies": self.get_dependencies(),
181 | "metadata": {
182 | "name": self.config["name"],
183 | "type": self.project_type.name,
184 | "build_system": self.build_system.value,
185 | "config": self.config
186 | }
187 | }
188 |
189 | # Add Git information if available
190 | git_info = self.get_git_status()
191 | if git_info.get("initialized", False):
192 | analysis["git"] = git_info
193 |
194 | # Add build/test status if available
195 | if hasattr(self, 'last_build'):
196 | analysis["last_build"] = self.last_build
197 | if hasattr(self, 'last_test_run'):
198 | analysis["last_test_run"] = self.last_test_run
199 |
200 | return analysis
201 |
202 | def get_structure(self) -> Dict[str, Any]:
203 | """Get project structure."""
204 | def scan_dir(path: Path) -> Dict[str, Any]:
205 | structure = {}
206 | ignore_patterns = ['.git', '__pycache__', 'node_modules', 'target', 'build']
207 |
208 | for item in path.iterdir():
209 | if item.name in ignore_patterns:
210 | continue
211 |
212 | if item.is_file():
213 | structure[item.name] = {
214 | "type": "file",
215 | "size": item.stat().st_size
216 | }
217 | elif item.is_dir():
218 | structure[item.name] = {
219 | "type": "directory",
220 | "contents": scan_dir(item)
221 | }
222 |
223 | return structure
224 |
225 | return scan_dir(Path(self.path))
226 |
227 | async def cleanup(self):
228 | """Clean up project resources."""
229 | try:
230 | # Clean build artifacts
231 | if self.build_system == BuildSystem.MAVEN:
232 | await self.execute_command("mvn clean")
233 | elif self.build_system == BuildSystem.GRADLE:
234 | await self.execute_command("./gradlew clean")
235 | elif self.build_system == BuildSystem.NPM:
236 | await self.execute_command("npm run clean")
237 |
238 | logger.info(f"Cleaned up project: {self.config['name']}")
239 | except Exception as e:
240 | logger.error(f"Project cleanup failed: {e}")
241 | raise ProjectError(f"Cleanup failed: {str(e)}")
242 |
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/environments/workflow.py:
--------------------------------------------------------------------------------
```python
1 | """Development workflow management for environments."""
2 | from typing import Dict, List, Optional, Any, Callable
3 | from enum import Enum
4 | import asyncio
5 |
6 | from ..utils.logging import setup_logging
7 | from ..utils.errors import WorkflowError
8 |
9 | logger = setup_logging(__name__)
10 |
11 | class TaskStatus(str, Enum):
12 | """Workflow task status."""
13 | PENDING = "pending"
14 | RUNNING = "running"
15 | COMPLETED = "completed"
16 | FAILED = "failed"
17 | SKIPPED = "skipped"
18 |
19 | class Task:
20 | """Represents a workflow task."""
21 |
22 | def __init__(
23 | self,
24 | name: str,
25 | command: str,
26 | environment: str,
27 | dependencies: Optional[List[str]] = None,
28 | timeout: Optional[int] = None,
29 | retry_count: int = 0,
30 | on_success: Optional[Callable] = None,
31 | on_failure: Optional[Callable] = None
32 | ):
33 | self.name = name
34 | self.command = command
35 | self.environment = environment
36 | self.dependencies = dependencies or []
37 | self.timeout = timeout
38 | self.retry_count = retry_count
39 | self.status = TaskStatus.PENDING
40 | self.result: Optional[Dict[str, Any]] = None
41 | self.on_success = on_success
42 | self.on_failure = on_failure
43 | self.attempts = 0
44 |
45 | class Workflow:
46 | """Manages development workflows."""
47 |
48 | def __init__(self, env_manager):
49 | self.env_manager = env_manager
50 | self.tasks: Dict[str, Task] = {}
51 | self.running = False
52 |
53 | def add_task(self, task: Task) -> None:
54 | """Add a task to the workflow."""
55 | self.tasks[task.name] = task
56 |
57 | def remove_task(self, task_name: str) -> None:
58 | """Remove a task from the workflow."""
59 | if task_name in self.tasks:
60 | del self.tasks[task_name]
61 |
62 | async def execute(self) -> Dict[str, Any]:
63 | """Execute the workflow."""
64 | try:
65 | self.running = True
66 | results = {}
67 |
68 | # Build dependency graph
69 | graph = self._build_dependency_graph()
70 |
71 | # Execute tasks in order
72 | for task_group in graph:
73 | # Execute tasks in group concurrently
74 | tasks = [self._execute_task(task_name) for task_name in task_group]
75 | group_results = await asyncio.gather(*tasks, return_exceptions=True)
76 |
77 | # Process results
78 | for task_name, result in zip(task_group, group_results):
79 | if isinstance(result, Exception):
80 | self.tasks[task_name].status = TaskStatus.FAILED
81 | results[task_name] = {
82 | "status": TaskStatus.FAILED,
83 | "error": str(result)
84 | }
85 | else:
86 | results[task_name] = result
87 |
88 | return results
89 |
90 | except Exception as e:
91 | raise WorkflowError(f"Workflow execution failed: {str(e)}")
92 | finally:
93 | self.running = False
94 |
95 | async def _execute_task(self, task_name: str) -> Dict[str, Any]:
96 | """Execute a single task."""
97 | task = self.tasks[task_name]
98 |
99 | # Check dependencies
100 | for dep in task.dependencies:
101 | dep_task = self.tasks.get(dep)
102 | if not dep_task or dep_task.status != TaskStatus.COMPLETED:
103 | task.status = TaskStatus.SKIPPED
104 | return {
105 | "status": TaskStatus.SKIPPED,
106 | "reason": f"Dependency {dep} not satisfied"
107 | }
108 |
109 | task.status = TaskStatus.RUNNING
110 | task.attempts += 1
111 |
112 | try:
113 | # Execute the command
114 | result = await asyncio.wait_for(
115 | self.env_manager.execute_in_environment(
116 | task.environment,
117 | task.command
118 | ),
119 | timeout=task.timeout
120 | )
121 |
122 | # Handle execution result
123 | if result['exit_code'] == 0:
124 | task.status = TaskStatus.COMPLETED
125 | if task.on_success:
126 | await task.on_success(result)
127 | return {
128 | "status": TaskStatus.COMPLETED,
129 | "result": result
130 | }
131 | else:
132 | # Handle retry logic
133 | if task.attempts < task.retry_count + 1:
134 | logger.info(f"Retrying task {task_name} (attempt {task.attempts})")
135 | return await self._execute_task(task_name)
136 |
137 | task.status = TaskStatus.FAILED
138 | if task.on_failure:
139 | await task.on_failure(result)
140 | return {
141 | "status": TaskStatus.FAILED,
142 | "result": result
143 | }
144 |
145 | except asyncio.TimeoutError:
146 | task.status = TaskStatus.FAILED
147 | return {
148 | "status": TaskStatus.FAILED,
149 | "error": "Task timeout"
150 | }
151 |
152 | except Exception as e:
153 | task.status = TaskStatus.FAILED
154 | return {
155 | "status": TaskStatus.FAILED,
156 | "error": str(e)
157 | }
158 |
159 | def _build_dependency_graph(self) -> List[List[str]]:
160 | """Build ordered list of task groups based on dependencies."""
161 | # Initialize variables
162 | graph: List[List[str]] = []
163 | completed = set()
164 | remaining = set(self.tasks.keys())
165 |
166 | while remaining:
167 | # Find tasks with satisfied dependencies
168 | group = set()
169 | for task_name in remaining:
170 | task = self.tasks[task_name]
171 | if all(dep in completed for dep in task.dependencies):
172 | group.add(task_name)
173 |
174 | if not group:
175 | # Circular dependency detected
176 | raise WorkflowError("Circular dependency detected in workflow")
177 |
178 | # Add group to graph
179 | graph.append(list(group))
180 | completed.update(group)
181 | remaining.difference_update(group)
182 |
183 | return graph
184 |
185 | def get_status(self) -> Dict[str, Any]:
186 | """Get workflow status."""
187 | return {
188 | "running": self.running,
189 | "tasks": {
190 | name: {
191 | "status": task.status,
192 | "attempts": task.attempts,
193 | "dependencies": task.dependencies
194 | }
195 | for name, task in self.tasks.items()
196 | }
197 | }
198 |
199 | def reset(self) -> None:
200 | """Reset workflow state."""
201 | for task in self.tasks.values():
202 | task.status = TaskStatus.PENDING
203 | task.attempts = 0
204 | task.result = None
205 | self.running = False
206 |
207 | # Example workflow definitions for common development tasks
208 | class CommonWorkflows:
209 | """Predefined development workflows."""
210 |
211 | @staticmethod
212 | def create_build_workflow(env_manager, environment: str) -> Workflow:
213 | """Create a standard build workflow."""
214 | workflow = Workflow(env_manager)
215 |
216 | # Install dependencies
217 | workflow.add_task(Task(
218 | name="install_deps",
219 | command="npm install",
220 | environment=environment,
221 | retry_count=2
222 | ))
223 |
224 | # Run linter
225 | workflow.add_task(Task(
226 | name="lint",
227 | command="npm run lint",
228 | environment=environment,
229 | dependencies=["install_deps"]
230 | ))
231 |
232 | # Run tests
233 | workflow.add_task(Task(
234 | name="test",
235 | command="npm run test",
236 | environment=environment,
237 | dependencies=["install_deps"]
238 | ))
239 |
240 | # Build
241 | workflow.add_task(Task(
242 | name="build",
243 | command="npm run build",
244 | environment=environment,
245 | dependencies=["lint", "test"]
246 | ))
247 |
248 | return workflow
249 |
250 | @staticmethod
251 | def create_test_workflow(env_manager, environment: str) -> Workflow:
252 | """Create a standard test workflow."""
253 | workflow = Workflow(env_manager)
254 |
255 | # Install test dependencies
256 | workflow.add_task(Task(
257 | name="install_test_deps",
258 | command="npm install --only=dev",
259 | environment=environment,
260 | retry_count=2
261 | ))
262 |
263 | # Run unit tests
264 | workflow.add_task(Task(
265 | name="unit_tests",
266 | command="npm run test:unit",
267 | environment=environment,
268 | dependencies=["install_test_deps"]
269 | ))
270 |
271 | # Run integration tests
272 | workflow.add_task(Task(
273 | name="integration_tests",
274 | command="npm run test:integration",
275 | environment=environment,
276 | dependencies=["install_test_deps"]
277 | ))
278 |
279 | # Generate coverage report
280 | workflow.add_task(Task(
281 | name="coverage",
282 | command="npm run coverage",
283 | environment=environment,
284 | dependencies=["unit_tests", "integration_tests"]
285 | ))
286 |
287 | return workflow
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/environments/tools.py:
--------------------------------------------------------------------------------
```python
1 | """Development tools integration for environments."""
2 | import shutil
3 | import subprocess
4 | from typing import Dict, Optional, Any
5 | from pathlib import Path
6 |
7 | from ..utils.logging import setup_logging
8 | from ..utils.errors import ToolError
9 |
10 | logger = setup_logging(__name__)
11 |
12 | class ToolManager:
13 | """Manages development tools in environments."""
14 |
15 | def __init__(self, env_manager):
16 | self.env_manager = env_manager
17 |
18 | async def setup_package_manager(
19 | self,
20 | environment: str,
21 | package_manager: str,
22 | config: Optional[Dict[str, Any]] = None
23 | ) -> Dict[str, Any]:
24 | """Set up package manager in an environment."""
25 | try:
26 | config = config or {}
27 |
28 | if package_manager == "npm":
29 | return await self._setup_npm(environment, config)
30 | elif package_manager == "pip":
31 | return await self._setup_pip(environment, config)
32 | else:
33 | raise ToolError(f"Unsupported package manager: {package_manager}")
34 |
35 | except Exception as e:
36 | raise ToolError(f"Failed to setup package manager: {str(e)}")
37 |
38 | async def setup_build_tool(
39 | self,
40 | environment: str,
41 | build_tool: str,
42 | config: Optional[Dict[str, Any]] = None
43 | ) -> Dict[str, Any]:
44 | """Set up build tool in an environment."""
45 | try:
46 | config = config or {}
47 |
48 | if build_tool == "webpack":
49 | return await self._setup_webpack(environment, config)
50 | elif build_tool == "vite":
51 | return await self._setup_vite(environment, config)
52 | else:
53 | raise ToolError(f"Unsupported build tool: {build_tool}")
54 |
55 | except Exception as e:
56 | raise ToolError(f"Failed to setup build tool: {str(e)}")
57 |
58 | async def setup_test_framework(
59 | self,
60 | environment: str,
61 | test_framework: str,
62 | config: Optional[Dict[str, Any]] = None
63 | ) -> Dict[str, Any]:
64 | """Set up testing framework in an environment."""
65 | try:
66 | config = config or {}
67 |
68 | if test_framework == "jest":
69 | return await self._setup_jest(environment, config)
70 | elif test_framework == "pytest":
71 | return await self._setup_pytest(environment, config)
72 | else:
73 | raise ToolError(f"Unsupported test framework: {test_framework}")
74 |
75 | except Exception as e:
76 | raise ToolError(f"Failed to setup test framework: {str(e)}")
77 |
78 | async def _setup_npm(self, environment: str, config: Dict[str, Any]) -> Dict[str, Any]:
79 | """Set up NPM package manager."""
80 | try:
81 | # Initialize package.json if needed
82 | if not config.get('skip_init'):
83 | result = await self.env_manager.execute_in_environment(
84 | environment,
85 | 'npm init -y'
86 | )
87 | if result['exit_code'] != 0:
88 | raise ToolError(f"npm init failed: {result['error']}")
89 |
90 | # Install dependencies if specified
91 | if deps := config.get('dependencies'):
92 | deps_str = ' '.join(deps)
93 | result = await self.env_manager.execute_in_environment(
94 | environment,
95 | f'npm install {deps_str}'
96 | )
97 | if result['exit_code'] != 0:
98 | raise ToolError(f"npm install failed: {result['error']}")
99 |
100 | return {"status": "success"}
101 |
102 | except Exception as e:
103 | raise ToolError(f"NPM setup failed: {str(e)}")
104 |
105 | async def _setup_pip(self, environment: str, config: Dict[str, Any]) -> Dict[str, Any]:
106 | """Set up Pip package manager."""
107 | try:
108 | # Create virtual environment if needed
109 | if not config.get('skip_venv'):
110 | result = await self.env_manager.execute_in_environment(
111 | environment,
112 | 'python -m venv .venv'
113 | )
114 | if result['exit_code'] != 0:
115 | raise ToolError(f"venv creation failed: {result['error']}")
116 |
117 | # Install dependencies if specified
118 | if deps := config.get('dependencies'):
119 | deps_str = ' '.join(deps)
120 | result = await self.env_manager.execute_in_environment(
121 | environment,
122 | f'pip install {deps_str}'
123 | )
124 | if result['exit_code'] != 0:
125 | raise ToolError(f"pip install failed: {result['error']}")
126 |
127 | return {"status": "success"}
128 |
129 | except Exception as e:
130 | raise ToolError(f"Pip setup failed: {str(e)}")
131 |
132 | async def _setup_webpack(self, environment: str, config: Dict[str, Any]) -> Dict[str, Any]:
133 | """Set up Webpack build tool."""
134 | try:
135 | # Install webpack and dependencies
136 | result = await self.env_manager.execute_in_environment(
137 | environment,
138 | 'npm install webpack webpack-cli --save-dev'
139 | )
140 | if result['exit_code'] != 0:
141 | raise ToolError(f"webpack installation failed: {result['error']}")
142 |
143 | # Create webpack config if not exists
144 | config_content = """
145 | const path = require('path');
146 |
147 | module.exports = {
148 | entry: './src/index.js',
149 | output: {
150 | path: path.resolve(__dirname, 'dist'),
151 | filename: 'bundle.js'
152 | }
153 | };
154 | """
155 |
156 | config_path = Path(self.env_manager.environments[environment]['path']) / 'webpack.config.js'
157 | config_path.write_text(config_content)
158 |
159 | return {"status": "success"}
160 |
161 | except Exception as e:
162 | raise ToolError(f"Webpack setup failed: {str(e)}")
163 |
164 | async def _setup_vite(self, environment: str, config: Dict[str, Any]) -> Dict[str, Any]:
165 | """Set up Vite build tool."""
166 | try:
167 | # Install vite
168 | result = await self.env_manager.execute_in_environment(
169 | environment,
170 | 'npm install vite --save-dev'
171 | )
172 | if result['exit_code'] != 0:
173 | raise ToolError(f"vite installation failed: {result['error']}")
174 |
175 | # Create vite config if not exists
176 | config_content = """
177 | export default {
178 | root: 'src',
179 | build: {
180 | outDir: '../dist'
181 | }
182 | }
183 | """
184 |
185 | config_path = Path(self.env_manager.environments[environment]['path']) / 'vite.config.js'
186 | config_path.write_text(config_content)
187 |
188 | return {"status": "success"}
189 |
190 | except Exception as e:
191 | raise ToolError(f"Vite setup failed: {str(e)}")
192 |
193 | async def _setup_jest(self, environment: str, config: Dict[str, Any]) -> Dict[str, Any]:
194 | """Set up Jest testing framework."""
195 | try:
196 | # Install jest and dependencies
197 | result = await self.env_manager.execute_in_environment(
198 | environment,
199 | 'npm install jest @types/jest --save-dev'
200 | )
201 | if result['exit_code'] != 0:
202 | raise ToolError(f"jest installation failed: {result['error']}")
203 |
204 | # Create jest config if not exists
205 | config_content = """
206 | module.exports = {
207 | testEnvironment: 'node',
208 | testMatch: ['**/*.test.js'],
209 | collectCoverage: true
210 | };
211 | """
212 |
213 | config_path = Path(self.env_manager.environments[environment]['path']) / 'jest.config.js'
214 | config_path.write_text(config_content)
215 |
216 | return {"status": "success"}
217 |
218 | except Exception as e:
219 | raise ToolError(f"Jest setup failed: {str(e)}")
220 |
221 | async def _setup_pytest(self, environment: str, config: Dict[str, Any]) -> Dict[str, Any]:
222 | """Set up Pytest testing framework."""
223 | try:
224 | # Install pytest and dependencies
225 | result = await self.env_manager.execute_in_environment(
226 | environment,
227 | 'pip install pytest pytest-cov'
228 | )
229 | if result['exit_code'] != 0:
230 | raise ToolError(f"pytest installation failed: {result['error']}")
231 |
232 | # Create pytest config if not exists
233 | config_content = """
234 | [pytest]
235 | testpaths = tests
236 | python_files = test_*.py
237 | addopts = --cov=src
238 | """
239 |
240 | config_path = Path(self.env_manager.environments[environment]['path']) / 'pytest.ini'
241 | config_path.write_text(config_content)
242 |
243 | return {"status": "success"}
244 |
245 | except Exception as e:
246 | raise ToolError(f"Pytest setup failed: {str(e)}")
247 |
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/project_manager/manager.py:
--------------------------------------------------------------------------------
```python
1 | """Project management system for MCP Development Server."""
2 | import asyncio
3 | import json
4 | from pathlib import Path
5 | from typing import Dict, Any, Optional, List
6 | import git
7 |
8 | from .project_types import PROJECT_TYPES, ProjectType, BuildSystem
9 | from .templates import TemplateManager
10 | from ..prompts.project_templates import PROJECT_TEMPLATES
11 | from ..utils.logging import setup_logging
12 | from ..utils.errors import ProjectError
13 | from ..docker.manager import DockerManager
14 |
15 | logger = setup_logging(__name__)
16 |
17 | class ProjectManager:
18 | """Manages development projects."""
19 |
20 | def __init__(self, config):
21 | """Initialize project manager.
22 |
23 | Args:
24 | config: Server configuration instance
25 | """
26 | self.config = config
27 | self.template_manager = TemplateManager()
28 | self.docker_manager = DockerManager()
29 | self.current_project = None
30 | self.projects = {}
31 |
32 | def get_available_project_types(self) -> Dict[str, Dict[str, Any]]:
33 | """Get list of available project types.
34 |
35 | Returns:
36 | Dict[str, Dict[str, Any]]: Project type information
37 | """
38 | return {
39 | name: {
40 | "name": pt.name,
41 | "description": pt.description,
42 | "build_systems": [bs.value for bs in pt.build_systems],
43 | "default_build_system": pt.default_build_system.value
44 | }
45 | for name, pt in PROJECT_TYPES.items()
46 | }
47 |
48 | async def create_project(
49 | self,
50 | name: str,
51 | project_type: str,
52 | project_config: Dict[str, Any],
53 | path: Optional[str] = None,
54 | description: str = ""
55 | ) -> Any:
56 | """Create a new project.
57 |
58 | Args:
59 | name: Project name
60 | project_type: Type of project (e.g., java, dotnet, node)
61 | project_config: Project-specific configuration
62 | path: Project directory path (optional)
63 | description: Project description
64 |
65 | Returns:
66 | Project instance
67 | """
68 | try:
69 | if project_type not in PROJECT_TYPES:
70 | raise ProjectError(f"Unsupported project type: {project_type}")
71 |
72 | project_type_info = PROJECT_TYPES[project_type]
73 |
74 | # Determine project path
75 | if not path:
76 | projects_dir = Path(self.config.get("projectsDir"))
77 | path = str(projects_dir / name)
78 |
79 | project_path = Path(path)
80 | if project_path.exists():
81 | raise ProjectError(f"Project path already exists: {path}")
82 |
83 | # Create project directory
84 | project_path.mkdir(parents=True, exist_ok=True)
85 |
86 | # Create project configuration
87 | project_config.update({
88 | "name": name,
89 | "type": project_type,
90 | "description": description,
91 | "build_system": project_config.get("build_system",
92 | project_type_info.default_build_system.value)
93 | })
94 |
95 | # Save project configuration
96 | config_path = project_path / "project.json"
97 | with open(config_path, "w") as f:
98 | json.dump(project_config, f, indent=2)
99 |
100 | # Create project structure
101 | await self._create_project_structure(project_path, project_type_info)
102 |
103 | # Initialize build system
104 | await self._initialize_build_system(
105 | project_path,
106 | project_type_info,
107 | project_config
108 | )
109 |
110 | # Set up Docker environment if requested
111 | if project_config.get("setup_docker", False):
112 | await self._setup_docker_environment(
113 | project_path,
114 | project_type_info,
115 | project_config
116 | )
117 |
118 | # Initialize Git repository if requested
119 | if project_config.get("initialize_git", True):
120 | repo = git.Repo.init(path)
121 | repo.index.add("*")
122 | repo.index.commit("Initial commit")
123 |
124 | # Create project instance
125 | project = await self._create_project_instance(
126 | path,
127 | project_config,
128 | project_type_info
129 | )
130 |
131 | # Store project reference
132 | self.projects[project.id] = project
133 | self.current_project = project
134 |
135 | logger.info(f"Created {project_type} project: {name} at {path}")
136 | return project
137 |
138 | except Exception as e:
139 | logger.error(f"Failed to create project: {str(e)}")
140 | raise ProjectError(f"Project creation failed: {str(e)}")
141 |
142 | async def _create_project_structure(
143 | self,
144 | project_path: Path,
145 | project_type: ProjectType
146 | ):
147 | """Create project directory structure.
148 |
149 | Args:
150 | project_path: Project directory path
151 | project_type: Project type information
152 | """
153 | def create_directory_structure(base_path: Path, structure: Dict[str, Any]):
154 | for name, content in structure.items():
155 | path = base_path / name
156 | if isinstance(content, dict):
157 | path.mkdir(exist_ok=True)
158 | create_directory_structure(path, content)
159 |
160 | create_directory_structure(project_path, project_type.file_structure)
161 |
162 | async def _initialize_build_system(
163 | self,
164 | project_path: Path,
165 | project_type: ProjectType,
166 | project_config: Dict[str, Any]
167 | ):
168 | """Initialize project build system.
169 |
170 | Args:
171 | project_path: Project directory path
172 | project_type: Project type information
173 | project_config: Project configuration
174 | """
175 | build_system = BuildSystem(project_config["build_system"])
176 |
177 | # Generate build system configuration files
178 | if build_system == BuildSystem.MAVEN:
179 | await self.template_manager.generate_maven_pom(
180 | project_path, project_config
181 | )
182 | elif build_system == BuildSystem.GRADLE:
183 | await self.template_manager.generate_gradle_build(
184 | project_path, project_config
185 | )
186 | elif build_system == BuildSystem.DOTNET:
187 | await self.template_manager.generate_dotnet_project(
188 | project_path, project_config
189 | )
190 | elif build_system in [BuildSystem.NPM, BuildSystem.YARN]:
191 | await self.template_manager.generate_package_json(
192 | project_path, project_config
193 | )
194 | elif build_system == BuildSystem.POETRY:
195 | await self.template_manager.generate_pyproject_toml(
196 | project_path, project_config
197 | )
198 |
199 | async def _setup_docker_environment(
200 | self,
201 | project_path: Path,
202 | project_type: ProjectType,
203 | project_config: Dict[str, Any]
204 | ):
205 | """Set up Docker environment for the project.
206 |
207 | Args:
208 | project_path: Project directory path
209 | project_type: Project type information
210 | project_config: Project configuration
211 | """
212 | # Generate Dockerfile from template
213 | dockerfile_template = project_type.docker_templates[0] # Use first template
214 | dockerfile_content = await self.docker_manager.generate_dockerfile(
215 | dockerfile_template,
216 | project_config
217 | )
218 |
219 | dockerfile_path = project_path / "Dockerfile"
220 | with open(dockerfile_path, "w") as f:
221 | f.write(dockerfile_content)
222 |
223 | # Generate docker-compose.yml if needed
224 | if project_config.get("use_docker_compose", False):
225 | services = {
226 | "app": {
227 | "build": ".",
228 | "volumes": [
229 | "./:/workspace"
230 | ],
231 | "environment": project_type.environment_variables
232 | }
233 | }
234 |
235 | compose_content = await self.docker_manager.create_compose_config(
236 | project_config["name"],
237 | services,
238 | project_path / "docker-compose.yml"
239 | )
240 |
241 | async def _create_project_instance(
242 | self,
243 | path: str,
244 | config: Dict[str, Any],
245 | project_type: ProjectType
246 | ) -> Any:
247 | """Create project instance based on type.
248 |
249 | Args:
250 | path: Project directory path
251 | config: Project configuration
252 | project_type: Project type information
253 |
254 | Returns:
255 | Project instance
256 | """
257 | # Import appropriate project class based on type
258 | if project_type.name == "java":
259 | from .java_project import JavaProject
260 | return JavaProject(path, config, project_type)
261 | elif project_type.name == "dotnet":
262 | from .dotnet_project import DotNetProject
263 | return DotNetProject(path, config, project_type)
264 | elif project_type.name == "node":
265 | from .node_project import NodeProject
266 | return NodeProject(path, config, project_type)
267 | elif project_type.name == "python":
268 | from .python_project import PythonProject
269 | return PythonProject(path, config, project_type)
270 | elif project_type.name == "golang":
271 | from .golang_project import GolangProject
272 | return GolangProject(path, config, project_type)
273 | else:
274 | from .base_project import Project
275 | return Project(path, config, project_type)
276 |
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/workflow/manager.py:
--------------------------------------------------------------------------------
```python
1 | """Development workflow management for MCP Development Server."""
2 |
3 | from typing import Dict, List, Optional, Any
4 | from enum import Enum
5 | from datetime import datetime
6 | import asyncio
7 |
8 | from ..utils.errors import WorkflowError
9 | from ..utils.logging import setup_logging
10 |
11 | logger = setup_logging(__name__)
12 |
13 | class WorkflowStatus(str, Enum):
14 | """Workflow execution status."""
15 | PENDING = "pending"
16 | RUNNING = "running"
17 | COMPLETED = "completed"
18 | FAILED = "failed"
19 |
20 | class WorkflowStep:
21 | """Individual step in a workflow."""
22 |
23 | def __init__(
24 | self,
25 | name: str,
26 | command: str,
27 | environment: str,
28 | depends_on: Optional[List[str]] = None,
29 | timeout: Optional[int] = None,
30 | retry_count: int = 0
31 | ):
32 | self.name = name
33 | self.command = command
34 | self.environment = environment
35 | self.depends_on = depends_on or []
36 | self.timeout = timeout
37 | self.retry_count = retry_count
38 | self.status = WorkflowStatus.PENDING
39 | self.result: Optional[Dict[str, Any]] = None
40 | self.attempts = 0
41 |
42 | class WorkflowManager:
43 | """Manages development workflows."""
44 |
45 | def __init__(self, env_manager):
46 | self.env_manager = env_manager
47 | self.workflows: Dict[str, Dict[str, Any]] = {}
48 |
49 | async def create_workflow(
50 | self,
51 | steps: List[WorkflowStep],
52 | config: Optional[Dict[str, Any]] = None
53 | ) -> str:
54 | """Create a new workflow."""
55 | try:
56 | workflow_id = f"workflow_{len(self.workflows)}"
57 |
58 | # Initialize workflow
59 | self.workflows[workflow_id] = {
60 | "steps": steps,
61 | "config": config or {},
62 | "status": WorkflowStatus.PENDING,
63 | "start_time": None,
64 | "end_time": None
65 | }
66 |
67 | return workflow_id
68 |
69 | except Exception as e:
70 | raise WorkflowError(f"Failed to create workflow: {str(e)}")
71 |
72 | async def start_workflow(self, workflow_id: str) -> None:
73 | """Start workflow execution."""
74 | try:
75 | if workflow := self.workflows.get(workflow_id):
76 | workflow["status"] = WorkflowStatus.RUNNING
77 | workflow["start_time"] = datetime.now()
78 |
79 | # Execute workflow steps
80 | asyncio.create_task(self._execute_workflow(workflow_id))
81 |
82 | else:
83 | raise WorkflowError(f"Workflow not found: {workflow_id}")
84 |
85 | except Exception as e:
86 | raise WorkflowError(f"Failed to start workflow: {str(e)}")
87 |
88 | async def _execute_workflow(self, workflow_id: str) -> None:
89 | """Execute workflow steps in order."""
90 | workflow = self.workflows[workflow_id]
91 |
92 | try:
93 | # Build execution graph
94 | graph = self._build_execution_graph(workflow["steps"])
95 |
96 | # Execute steps in dependency order
97 | for step_group in graph:
98 | results = await asyncio.gather(
99 | *[self._execute_step(workflow_id, step) for step in step_group],
100 | return_exceptions=True
101 | )
102 |
103 | # Check for failures
104 | if any(isinstance(r, Exception) for r in results):
105 | workflow["status"] = WorkflowStatus.FAILED
106 | return
107 |
108 | workflow["status"] = WorkflowStatus.COMPLETED
109 |
110 | except Exception as e:
111 | logger.error(f"Workflow execution error: {str(e)}")
112 | workflow["status"] = WorkflowStatus.FAILED
113 | workflow["error"] = str(e)
114 |
115 | finally:
116 | workflow["end_time"] = datetime.now()
117 |
118 | async def _execute_step(
119 | self,
120 | workflow_id: str,
121 | step: WorkflowStep
122 | ) -> None:
123 | """Execute a single workflow step."""
124 | try:
125 | step.status = WorkflowStatus.RUNNING
126 | step.attempts += 1
127 |
128 | # Execute step command
129 | result = await asyncio.wait_for(
130 | self.env_manager.execute_in_environment(
131 | step.environment,
132 | step.command
133 | ),
134 | timeout=step.timeout
135 | )
136 |
137 | # Handle step result
138 | success = result["exit_code"] == 0
139 | step.result = {
140 | "output": result["output"],
141 | "error": result.get("error"),
142 | "exit_code": result["exit_code"]
143 | }
144 |
145 | if success:
146 | step.status = WorkflowStatus.COMPLETED
147 | else:
148 | # Handle retry logic
149 | if step.attempts < step.retry_count + 1:
150 | logger.info(f"Retrying step {step.name} (attempt {step.attempts})")
151 | return await self._execute_step(workflow_id, step)
152 | step.status = WorkflowStatus.FAILED
153 |
154 | except asyncio.TimeoutError:
155 | step.status = WorkflowStatus.FAILED
156 | step.result = {
157 | "error": "Step execution timed out"
158 | }
159 | except Exception as e:
160 | step.status = WorkflowStatus.FAILED
161 | step.result = {
162 | "error": str(e)
163 | }
164 |
165 | def _build_execution_graph(
166 | self,
167 | steps: List[WorkflowStep]
168 | ) -> List[List[WorkflowStep]]:
169 | """Build ordered list of step groups based on dependencies."""
170 | # Initialize variables
171 | graph: List[List[WorkflowStep]] = []
172 | completed = set()
173 | remaining = set(step.name for step in steps)
174 | steps_by_name = {step.name: step for step in steps}
175 |
176 | while remaining:
177 | # Find steps with satisfied dependencies
178 | group = set()
179 | for step_name in remaining:
180 | step = steps_by_name[step_name]
181 | if all(dep in completed for dep in step.depends_on):
182 | group.add(step_name)
183 |
184 | if not group:
185 | # Circular dependency detected
186 | raise WorkflowError("Circular dependency detected in workflow steps")
187 |
188 | # Add group to graph
189 | graph.append([steps_by_name[name] for name in group])
190 | completed.update(group)
191 | remaining.difference_update(group)
192 |
193 | return graph
194 |
195 | async def get_workflow_status(self, workflow_id: str) -> Dict[str, Any]:
196 | """Get status and results of a workflow."""
197 | if workflow := self.workflows.get(workflow_id):
198 | return {
199 | "id": workflow_id,
200 | "status": workflow["status"],
201 | "steps": [
202 | {
203 | "name": step.name,
204 | "status": step.status,
205 | "result": step.result,
206 | "attempts": step.attempts
207 | }
208 | for step in workflow["steps"]
209 | ],
210 | "start_time": workflow["start_time"],
211 | "end_time": workflow["end_time"],
212 | "error": workflow.get("error")
213 | }
214 | raise WorkflowError(f"Workflow not found: {workflow_id}")
215 |
216 | def get_common_workflows(self) -> Dict[str, List[WorkflowStep]]:
217 | """Get predefined common workflow templates."""
218 | return {
219 | "build": [
220 | WorkflowStep(
221 | name="install",
222 | command="npm install",
223 | environment="default"
224 | ),
225 | WorkflowStep(
226 | name="lint",
227 | command="npm run lint",
228 | environment="default",
229 | depends_on=["install"]
230 | ),
231 | WorkflowStep(
232 | name="test",
233 | command="npm test",
234 | environment="default",
235 | depends_on=["install"]
236 | ),
237 | WorkflowStep(
238 | name="build",
239 | command="npm run build",
240 | environment="default",
241 | depends_on=["lint", "test"]
242 | )
243 | ],
244 | "test": [
245 | WorkflowStep(
246 | name="install_deps",
247 | command="npm install",
248 | environment="default"
249 | ),
250 | WorkflowStep(
251 | name="unit_tests",
252 | command="npm run test:unit",
253 | environment="default",
254 | depends_on=["install_deps"]
255 | ),
256 | WorkflowStep(
257 | name="integration_tests",
258 | command="npm run test:integration",
259 | environment="default",
260 | depends_on=["install_deps"]
261 | ),
262 | WorkflowStep(
263 | name="coverage",
264 | command="npm run coverage",
265 | environment="default",
266 | depends_on=["unit_tests", "integration_tests"]
267 | )
268 | ],
269 | "release": [
270 | WorkflowStep(
271 | name="bump_version",
272 | command="npm version patch",
273 | environment="default"
274 | ),
275 | WorkflowStep(
276 | name="build",
277 | command="npm run build",
278 | environment="default",
279 | depends_on=["bump_version"]
280 | ),
281 | WorkflowStep(
282 | name="publish",
283 | command="npm publish",
284 | environment="default",
285 | depends_on=["build"]
286 | )
287 | ]
288 | }
```
--------------------------------------------------------------------------------
/src/mcp_dev_server/docker/streams.py:
--------------------------------------------------------------------------------
```python
1 | """Container output streaming and file synchronization."""
2 | import os
3 | import time
4 | import asyncio
5 | import hashlib
6 | import collections
7 | from enum import Enum
8 | from datetime import datetime
9 | from typing import Dict, List, Optional, AsyncGenerator, Any
10 | from pathlib import Path
11 | from watchdog.observers import Observer
12 | from watchdog.events import FileSystemEventHandler
13 |
14 | from ..utils.logging import setup_logging
15 | from ..utils.errors import StreamError, SyncError
16 |
17 | logger = setup_logging(__name__)
18 |
19 | class OutputFormat(str, Enum):
20 | """Output stream formats."""
21 | STDOUT = "stdout"
22 | STDERR = "stderr"
23 | COMBINED = "combined"
24 | FORMATTED = "formatted"
25 |
26 | class StreamConfig:
27 | """Stream configuration."""
28 | def __init__(
29 | self,
30 | format: OutputFormat = OutputFormat.COMBINED,
31 | buffer_size: int = 1024,
32 | filters: Optional[List[str]] = None,
33 | timestamp: bool = False
34 | ):
35 | self.format = format
36 | self.buffer_size = buffer_size
37 | self.filters = filters or []
38 | self.timestamp = timestamp
39 |
40 | class SyncConfig:
41 | """Synchronization configuration."""
42 | def __init__(
43 | self,
44 | ignore_patterns: Optional[List[str]] = None,
45 | sync_interval: float = 1.0,
46 | atomic: bool = True
47 | ):
48 | self.ignore_patterns = ignore_patterns or []
49 | self.sync_interval = sync_interval
50 | self.atomic = atomic
51 |
52 | class StreamInfo:
53 | """Information about an active stream."""
54 | def __init__(self, task: asyncio.Task, config: StreamConfig):
55 | self.task = task
56 | self.config = config
57 | self.start_time = datetime.now()
58 |
59 | class EnhancedOutputStreamManager:
60 | """Enhanced streaming output manager."""
61 |
62 | def __init__(self, docker_manager):
63 | self.docker_manager = docker_manager
64 | self.active_streams: Dict[str, StreamInfo] = {}
65 | self._buffer = collections.deque(maxlen=1000) # Keep last 1000 messages
66 |
67 | async def start_stream(
68 | self,
69 | container_name: str,
70 | command: str,
71 | config: StreamConfig,
72 | callback: Optional[callable] = None
73 | ) -> AsyncGenerator[str, None]:
74 | """Start enhanced output stream."""
75 | try:
76 | container = self.docker_manager.containers.get(container_name)
77 | if not container:
78 | raise StreamError(f"Container not found: {container_name}")
79 |
80 | # Create execution with specified format
81 | exec_result = container.exec_run(
82 | command,
83 | stream=True,
84 | demux=True,
85 | socket=True # Use socket for better streaming
86 | )
87 |
88 | async def stream_handler():
89 | buffer = []
90 | try:
91 | async for data in exec_result.output:
92 | # Apply format and filtering
93 | processed_data = self._process_stream_data(data, config)
94 |
95 | if processed_data:
96 | buffer.extend(processed_data)
97 | if len(buffer) >= config.buffer_size:
98 | output = ''.join(buffer)
99 | buffer.clear()
100 |
101 | self._buffer.append(output)
102 |
103 | if callback:
104 | await callback(output)
105 | yield output
106 | except Exception as e:
107 | logger.error(f"Stream processing error: {str(e)}")
108 | raise StreamError(f"Stream processing error: {str(e)}")
109 | finally:
110 | if buffer:
111 | output = ''.join(buffer)
112 | self._buffer.append(output)
113 | if callback:
114 | await callback(output)
115 | yield output
116 |
117 | if container_name in self.active_streams:
118 | del self.active_streams[container_name]
119 |
120 | # Create and store stream task
121 | stream_task = asyncio.create_task(stream_handler())
122 | self.active_streams[container_name] = StreamInfo(stream_task, config)
123 |
124 | async for output in stream_task:
125 | yield output
126 |
127 | except Exception as e:
128 | logger.error(f"Failed to start stream: {str(e)}")
129 | raise StreamError(f"Failed to start stream: {str(e)}")
130 |
131 | def _process_stream_data(
132 | self,
133 | data: bytes,
134 | config: StreamConfig
135 | ) -> Optional[str]:
136 | """Process stream data according to config."""
137 | if not data:
138 | return None
139 |
140 | # Split streams if demuxed
141 | stdout, stderr = data if isinstance(data, tuple) else (data, None)
142 |
143 | # Apply format
144 | if config.format == OutputFormat.STDOUT and stdout:
145 | output = stdout.decode()
146 | elif config.format == OutputFormat.STDERR and stderr:
147 | output = stderr.decode()
148 | elif config.format == OutputFormat.COMBINED:
149 | output = ''
150 | if stdout:
151 | output += stdout.decode()
152 | if stderr:
153 | output += stderr.decode()
154 | elif config.format == OutputFormat.FORMATTED:
155 | output = self._format_output(stdout, stderr)
156 | else:
157 | return None
158 |
159 | # Apply filters
160 | for filter_pattern in config.filters:
161 | if filter_pattern in output:
162 | return None
163 |
164 | # Add timestamp if requested
165 | if config.timestamp:
166 | output = f"[{datetime.now().isoformat()}] {output}"
167 |
168 | return output
169 |
170 | @staticmethod
171 | def _format_output(stdout: Optional[bytes], stderr: Optional[bytes]) -> str:
172 | """Format output with colors and prefixes."""
173 | output = []
174 |
175 | if stdout:
176 | output.append(f"\033[32m[OUT]\033[0m {stdout.decode()}")
177 | if stderr:
178 | output.append(f"\033[31m[ERR]\033[0m {stderr.decode()}")
179 |
180 | return '\n'.join(output)
181 |
182 | async def stop_stream(self, container_name: str) -> None:
183 | """Stop streaming from a container."""
184 | if stream_info := self.active_streams.get(container_name):
185 | stream_info.task.cancel()
186 | try:
187 | await stream_info.task
188 | except asyncio.CancelledError:
189 | pass
190 | del self.active_streams[container_name]
191 |
192 | class BiDirectionalSync:
193 | """Enhanced bi-directional file synchronization."""
194 |
195 | def __init__(self, docker_manager):
196 | self.docker_manager = docker_manager
197 | self.sync_handlers: Dict[str, EnhancedSyncHandler] = {}
198 | self.observer = Observer()
199 | self.observer.start()
200 |
201 | async def start_sync(
202 | self,
203 | container_name: str,
204 | host_path: str,
205 | container_path: str,
206 | config: SyncConfig
207 | ) -> None:
208 | """Start bi-directional file sync."""
209 | try:
210 | # Validate paths
211 | if not os.path.exists(host_path):
212 | raise SyncError(f"Host path does not exist: {host_path}")
213 |
214 | container = self.docker_manager.containers.get(container_name)
215 | if not container:
216 | raise SyncError(f"Container not found: {container_name}")
217 |
218 | # Create sync handler
219 | handler = EnhancedSyncHandler(
220 | container=container,
221 | container_path=container_path,
222 | host_path=host_path,
223 | config=config
224 | )
225 |
226 | # Start watching both directions
227 | self.observer.schedule(
228 | handler,
229 | host_path,
230 | recursive=True
231 | )
232 |
233 | # Start container file watcher
234 | await handler.start_container_watcher()
235 |
236 | self.sync_handlers[container_name] = handler
237 | logger.info(f"Started bi-directional sync for container: {container_name}")
238 |
239 | except Exception as e:
240 | raise SyncError(f"Failed to start sync: {str(e)}")
241 |
242 | async def stop_sync(self, container_name: str) -> None:
243 | """Stop synchronization for a container."""
244 | if handler := self.sync_handlers.get(container_name):
245 | self.observer.unschedule_all()
246 | await handler.stop_container_watcher()
247 | del self.sync_handlers[container_name]
248 | logger.info(f"Stopped sync for container: {container_name}")
249 |
250 | async def cleanup(self) -> None:
251 | """Clean up all synchronization handlers."""
252 | for container_name in list(self.sync_handlers.keys()):
253 | await self.stop_sync(container_name)
254 | self.observer.stop()
255 | self.observer.join()
256 |
257 | class EnhancedSyncHandler(FileSystemEventHandler):
258 | """Enhanced sync handler with bi-directional support."""
259 |
260 | def __init__(
261 | self,
262 | container,
263 | container_path: str,
264 | host_path: str,
265 | config: SyncConfig
266 | ):
267 | super().__init__()
268 | self.container = container
269 | self.container_path = container_path
270 | self.host_path = host_path
271 | self.config = config
272 | self.sync_lock = asyncio.Lock()
273 | self.pending_syncs: Dict[str, float] = {}
274 | self._container_watcher: Optional[asyncio.Task] = None
275 |
276 | async def start_container_watcher(self) -> None:
277 | """Start watching container files."""
278 | cmd = f"""
279 | inotifywait -m -r -e modify,create,delete,move {self.container_path}
280 | """
281 |
282 | exec_result = self.container.exec_run(
283 | cmd,
284 | stream=True,
285 | detach=True
286 | )
287 |
288 | self._container_watcher = asyncio.create_task(
289 | self._handle_container_events(exec_result.output)
290 | )
291 |
292 | async def stop_container_watcher(self) -> None:
293 | """Stop container file watcher."""
294 | if self._container_watcher:
295 | self._container_watcher.cancel()
296 | try:
297 | await self._container_watcher
298 | except asyncio.CancelledError:
299 | pass
300 | self._container_watcher = None
301 |
302 | async def _handle_container_events(self, output_stream: AsyncGenerator) -> None:
303 | """Handle container file events."""
304 | try:
305 | async for event in output_stream:
306 | await self._handle_container_change(event.decode())
307 | except Exception as e:
308 | logger.error(f"Container watcher error: {str(e)}")
309 |
310 | async def _handle_container_change(self, event: str) -> None:
311 | """Handle container file change."""
312 | try:
313 | # Parse inotify event
314 | parts = event.strip().split()
315 | if len(parts) >= 3:
316 | path = parts[0]
317 | change_type = parts[1]
318 | filename = parts[2]
319 |
320 | container_path = os.path.join(path, filename)
321 | host_path = self._container_to_host_path(container_path)
322 |
323 | # Apply filters
324 | if self._should_ignore(host_path):
325 | return
326 |
327 | async with self.sync_lock:
328 | # Check if change is from host sync
329 | if host_path in self.pending_syncs:
330 | if time.time() - self.pending_syncs[host_path] < self.config.sync_interval:
331 | return
332 |
333 | # Sync from container to host
334 | await self._sync_to_host(container_path, host_path)
335 |
336 | except Exception as e:
337 | logger.error(f"Error handling container change: {str(e)}")
338 |
339 | def _container_to_host_path(self, container_path: str) -> str:
340 | """Convert container path to host path."""
341 | rel_path = os.path.relpath(container_path, self.container_path)
342 | return os.path.join(self.host_path, rel_path)
343 |
344 | def _should_ignore(self, path: str) -> bool:
345 | """Check if path should be ignored."""
346 | return any(pattern in path for pattern in self.config.ignore_patterns)
347 |
348 | async def _sync_to_host(
349 | self,
350 | container_path: str,
351 | host_path: str
352 | ) -> None:
353 | """Sync file from container to host."""
354 | try:
355 | # Get file from container
356 | stream, stat = self.container.get_archive(container_path)
357 |
358 | # Create parent directories
359 | os.makedirs(os.path.dirname(host_path), exist_ok=True)
360 |
361 | if self.config.atomic:
362 | # Save file atomically using temporary file
363 | tmp_path = f"{host_path}.tmp"
364 | with open(tmp_path, 'wb') as f:
365 | for chunk in stream:
366 | f.write(chunk)
367 | os.rename(tmp_path, host_path)
368 | else:
369 | # Direct write
370 | with open(host_path, 'wb') as f:
371 | for chunk in stream:
372 | f.write(chunk)
373 |
374 | # Update sync tracking
375 | self.pending_syncs[host_path] = time.time()
376 |
377 | except Exception as e:
378 | logger.error(f"Error syncing to host: {str(e)}")
379 | raise SyncError(f"Failed to sync file {container_path}: {str(e)}")
```