# Sprint 01: Implementation Guide - Core Architecture Correction **Related Proposal:** [sprint-01-core-architecture-correction.md](./sprint-01-core-architecture-correction.md) This guide provides step-by-step technical implementation details for each file change. --- ## Phase 1: Package Restructuring (Issues #1-2) ### Issue #1: Rename Package Directory **Estimated Time:** 15 minutes **Dependencies:** None **Type:** Refactor **Steps:** ```bash cd /home/lmiranda/gitea-mcp-remote git mv src/gitea_http_wrapper src/gitea_mcp_remote ``` **Validation:** ```bash # Should exist ls -la src/gitea_mcp_remote/ # Should NOT exist ls -la src/gitea_http_wrapper/ 2>&1 | grep "No such file" ``` --- ### Issue #2: Update Configuration Module **Estimated Time:** 1-2 hours **Dependencies:** Issue #1 **Files:** `src/gitea_mcp_remote/config/settings.py`, `src/gitea_mcp_remote/config/__init__.py` **Changes to `settings.py`:** ```python # Line 1: Update docstring """Configuration settings for Gitea MCP HTTP transport.""" # Lines 33-36: Make gitea_repo optional gitea_repo: str | None = Field( default=None, description="Default repository name (optional)", ) # Lines 39-45: Update HTTP defaults http_host: str = Field( default="0.0.0.0", description="HTTP server bind address", ) http_port: int = Field( default=8080, ge=1, le=65535, description="HTTP server port", ) # After line 54 (after auth_token): Add new field mcp_auth_mode: str = Field( default="optional", description="MCP authentication mode: 'required', 'optional', or 'none'", ) # Delete lines 88-95: Remove get_gitea_mcp_env() method # (No longer needed - we use direct Python imports, not subprocess) ``` **No import changes needed in this file** (it doesn't import from gitea_http_wrapper). **Update `__init__.py`:** ```python """Configuration module for Gitea MCP HTTP transport.""" from gitea_mcp_remote.config.settings import GiteaSettings, load_settings __all__ = ["GiteaSettings", "load_settings"] ``` **Validation:** ```python # Test in Python REPL from gitea_mcp_remote.config import GiteaSettings # Should have new field assert hasattr(GiteaSettings, 'mcp_auth_mode') # Should have optional gitea_repo settings = GiteaSettings( gitea_url="https://test.com", gitea_token="test", gitea_owner="test" # gitea_repo is optional now ) ``` --- ## Phase 2: Update Supporting Modules (Issues #3-4) ### Issue #3: Update Middleware Module **Estimated Time:** 30 minutes **Dependencies:** Issue #1 **Files:** `src/gitea_mcp_remote/middleware/auth.py`, `src/gitea_mcp_remote/middleware/__init__.py` **Changes to `auth.py`:** - Keep ALL logic unchanged - Only update imports **Changes to `__init__.py`:** ```python """Middleware components for MCP HTTP transport.""" from gitea_mcp_remote.middleware.auth import ( BearerAuthMiddleware, HealthCheckBypassMiddleware, ) __all__ = [ "BearerAuthMiddleware", "HealthCheckBypassMiddleware", ] ``` **Validation:** ```python from gitea_mcp_remote.middleware import BearerAuthMiddleware assert BearerAuthMiddleware is not None ``` --- ### Issue #4: Update Filtering Module **Estimated Time:** 45 minutes **Dependencies:** Issue #1 **Files:** `src/gitea_mcp_remote/filtering/filter.py`, `src/gitea_mcp_remote/filtering/__init__.py` **Changes to `filter.py`:** ```python # Line 1: Update docstring """Tool filtering for MCP client compatibility.""" # Add import at top import logging logger = logging.getLogger(__name__) # Lines 29-32: Change ValueError to warning if enabled_tools is not None and disabled_tools is not None: logger.warning( "Both enabled_tools and disabled_tools specified. " "Using disabled_tools (blacklist mode). " "Recommendation: Choose one filtering mode." ) # Continue with disabled_tools taking precedence ``` **Changes to `__init__.py`:** ```python """Tool filtering module for MCP HTTP transport.""" from gitea_mcp_remote.filtering.filter import ToolFilter __all__ = ["ToolFilter"] ``` **Validation:** ```python from gitea_mcp_remote.filtering import ToolFilter # Should log warning, not raise filter = ToolFilter( enabled_tools=["tool1"], disabled_tools=["tool2"] ) # Should use disabled_tools (blacklist mode) assert filter.disabled_tools == {"tool2"} ``` --- ## Phase 3: Relocate and Update Tests (Issues #5-6) ### Issue #5: Move Tests to Root **Estimated Time:** 30 minutes **Dependencies:** Issue #1 **Type:** Refactor **Steps:** ```bash cd /home/lmiranda/gitea-mcp-remote git mv src/gitea_mcp_remote/tests tests ``` **Validation:** ```bash # Should exist ls -la tests/test_config.py ls -la tests/test_middleware.py ls -la tests/test_filtering.py ls -la tests/conftest.py # Should NOT exist ls -la src/gitea_mcp_remote/tests/ 2>&1 | grep "No such file" ``` --- ### Issue #6: Update Test Imports **Estimated Time:** 1 hour **Dependencies:** Issue #5 **Files:** All files in `tests/` directory **Global search-replace in all test files:** ```python # OLD from gitea_http_wrapper.config import ... from gitea_http_wrapper.middleware import ... from gitea_http_wrapper.filtering import ... # NEW from gitea_mcp_remote.config import ... from gitea_mcp_remote.middleware import ... from gitea_mcp_remote.filtering import ... ``` **Specific files to update:** - `tests/conftest.py` - `tests/test_config.py` - `tests/test_middleware.py` - `tests/test_filtering.py` **Validation:** ```bash pytest tests/ -v # All existing tests should pass ``` --- ## Phase 4: Core Server Replacement (Issues #7-8) ### Issue #7: Remove Old Server **Estimated Time:** 5 minutes **Dependencies:** Issues #2-6 (ensure all imports work first) **Type:** Deletion **Steps:** ```bash git rm src/gitea_mcp_remote/server.py ``` **Validation:** ```bash ls -la src/gitea_mcp_remote/server.py 2>&1 | grep "No such file" ``` --- ### Issue #8: Create New MCP HTTP Server **Estimated Time:** 4-6 hours **Dependencies:** Issue #7 **Files:** `src/gitea_mcp_remote/server_http.py` **Complete new file:** ```python """MCP HTTP transport server for Gitea operations. This server implements the MCP Streamable HTTP protocol, providing JSON-RPC 2.0 communication with Claude Desktop clients. """ import asyncio import json import logging from typing import Any import uvicorn from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import JSONResponse, Response from starlette.routing import Route # Import from marketplace gitea-mcp-server from mcp_server import ( GiteaClient, GiteaConfig, create_tool_dispatcher, get_tool_definitions, ) from gitea_mcp_remote.config import GiteaSettings, load_settings from gitea_mcp_remote.filtering import ToolFilter from gitea_mcp_remote.middleware import ( BearerAuthMiddleware, HealthCheckBypassMiddleware, ) # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logger = logging.getLogger(__name__) # MCP Protocol version MCP_VERSION = "2024-11-05" class GiteaMCPServer: """ MCP HTTP transport server for Gitea. Implements MCP Streamable HTTP protocol with JSON-RPC 2.0. """ def __init__(self, settings: GiteaSettings): """Initialize MCP server with settings.""" self.settings = settings # Initialize Gitea client self.gitea_config = GiteaConfig( base_url=settings.gitea_url, api_token=settings.gitea_token, default_owner=settings.gitea_owner, default_repo=settings.gitea_repo, ) self.gitea_client = GiteaClient(self.gitea_config) # Initialize tool filtering self.tool_filter = ToolFilter( enabled_tools=settings.enabled_tools_list, disabled_tools=settings.disabled_tools_list, ) # Get tool definitions and create dispatcher self.tool_definitions = get_tool_definitions() self.tool_dispatcher = create_tool_dispatcher(self.gitea_client) logger.info(f"Initialized MCP server for {settings.gitea_url}") logger.info(f"Tool filtering: {self.tool_filter.get_filter_stats()}") async def handle_list_tools(self, params: dict) -> dict: """Handle tools/list MCP method.""" # Get all tool definitions tools = self.tool_definitions # Apply filtering filtered_tools = self.tool_filter.filter_tools_list(tools) logger.info(f"Listed {len(filtered_tools)} tools (filtered from {len(tools)})") return { "tools": filtered_tools } async def handle_call_tool(self, params: dict) -> dict: """Handle tools/call MCP method.""" tool_name = params.get("name") arguments = params.get("arguments", {}) # Check if tool is filtered if not self.tool_filter.should_include_tool(tool_name): logger.warning(f"Tool '{tool_name}' is filtered out") raise ValueError(f"Tool '{tool_name}' is not available") logger.info(f"Calling tool: {tool_name}") # Dispatch to tool handler result = await self.tool_dispatcher(tool_name, arguments) return { "content": result } async def handle_initialize(self, params: dict) -> dict: """Handle initialize MCP method.""" return { "protocolVersion": MCP_VERSION, "serverInfo": { "name": "gitea-mcp-remote", "version": "1.1.0", }, "capabilities": { "tools": {} } } async def handle_jsonrpc_request(self, request_data: dict) -> dict: """Handle JSON-RPC 2.0 request.""" method = request_data.get("method") params = request_data.get("params", {}) request_id = request_data.get("id") try: # Route to appropriate handler if method == "initialize": result = await self.handle_initialize(params) elif method == "tools/list": result = await self.handle_list_tools(params) elif method == "tools/call": result = await self.handle_call_tool(params) else: raise ValueError(f"Unknown method: {method}") # Success response return { "jsonrpc": "2.0", "id": request_id, "result": result, } except Exception as e: logger.exception(f"Error handling {method}") # Error response return { "jsonrpc": "2.0", "id": request_id, "error": { "code": -32000, "message": str(e), } } # Global server instance mcp_server: GiteaMCPServer | None = None async def mcp_endpoint_head(request: Request) -> Response: """ Handle HEAD /mcp - Protocol version check. Returns MCP protocol version in X-MCP-Version header. """ return Response( status_code=200, headers={ "X-MCP-Version": MCP_VERSION, } ) async def mcp_endpoint_post(request: Request) -> JSONResponse: """ Handle POST /mcp - JSON-RPC 2.0 messages. Main MCP communication endpoint. """ try: # Parse JSON-RPC request request_data = await request.json() # Handle request response_data = await mcp_server.handle_jsonrpc_request(request_data) return JSONResponse(response_data) except json.JSONDecodeError: return JSONResponse( { "jsonrpc": "2.0", "id": None, "error": { "code": -32700, "message": "Parse error: Invalid JSON", } }, status_code=400, ) except Exception as e: logger.exception("Error in MCP endpoint") return JSONResponse( { "jsonrpc": "2.0", "id": None, "error": { "code": -32603, "message": f"Internal error: {str(e)}", } }, status_code=500, ) async def health_check(request: Request) -> JSONResponse: """Health check endpoint.""" return JSONResponse({"status": "healthy"}) async def startup() -> None: """Application startup handler.""" global mcp_server settings = load_settings() mcp_server = GiteaMCPServer(settings) logger.info(f"MCP HTTP server starting on {settings.http_host}:{settings.http_port}") # Define routes routes = [ # MCP protocol endpoints Route("/mcp", mcp_endpoint_post, methods=["POST"]), Route("/mcp", mcp_endpoint_head, methods=["HEAD"]), # Health check endpoints Route("/health", health_check, methods=["GET"]), Route("/healthz", health_check, methods=["GET"]), Route("/ping", health_check, methods=["GET"]), ] # Create Starlette app app = Starlette( routes=routes, on_startup=[startup], ) def create_app(settings: GiteaSettings | None = None) -> Starlette: """ Create and configure the Starlette application. Args: settings: Optional settings override for testing. Returns: Configured Starlette application. """ if settings is None: settings = load_settings() # Add middleware app.add_middleware(HealthCheckBypassMiddleware) if settings.mcp_auth_mode == "required": app.add_middleware(BearerAuthMiddleware, auth_token=settings.auth_token) return app def main() -> None: """Main entry point for the MCP HTTP server.""" settings = load_settings() # Log configuration logger.info(f"MCP Protocol Version: {MCP_VERSION}") logger.info(f"Gitea URL: {settings.gitea_url}") logger.info(f"Auth mode: {settings.mcp_auth_mode}") # Run server uvicorn.run( "gitea_mcp_remote.server_http:app", host=settings.http_host, port=settings.http_port, log_level="info", ) if __name__ == "__main__": main() ``` **Validation:** ```bash # Should import successfully python3 -c "from gitea_mcp_remote.server_http import GiteaMCPServer" # Check MCP endpoints exist python3 -c "from gitea_mcp_remote.server_http import app; print([r.path for r in app.routes])" # Should show: ['/mcp', '/mcp', '/health', '/healthz', '/ping'] ``` --- ## Phase 5: Update Project Configuration (Issues #9-10) ### Issue #9: Replace pyproject.toml **Estimated Time:** 30 minutes **Dependencies:** Issue #8 **File:** `pyproject.toml` **Complete replacement:** ```toml [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "gitea-mcp-remote" version = "1.1.0" description = "MCP HTTP transport for Gitea operations" readme = "README.md" requires-python = ">=3.10" license = { text = "MIT" } authors = [ { name = "Leo Miranda", email = "lmiranda@example.com" } ] keywords = ["mcp", "gitea", "model-context-protocol", "http-transport"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] dependencies = [ "gitea-mcp-server @ git+https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git#subdirectory=mcp-servers/gitea", "mcp>=0.9.0", "uvicorn>=0.27.0", "pydantic>=2.0.0", "pydantic-settings>=2.0.0", "python-dotenv>=1.0.0", "starlette>=0.36.0", ] [project.optional-dependencies] dev = [ "pytest>=7.0.0", "pytest-asyncio>=0.21.0", "pytest-cov>=4.0.0", "httpx>=0.24.0", ] [project.scripts] gitea-mcp-remote = "gitea_mcp_remote.server_http:main" [project.urls] Homepage = "https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote" Repository = "https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote" [tool.setuptools.packages.find] where = ["src"] [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] ``` **Validation:** ```bash pip install -e . # Should install successfully including marketplace dependency gitea-mcp-remote --help # Should show help (if we add --help support) or start server ``` --- ### Issue #10: Update pytest.ini **Estimated Time:** 5 minutes **Dependencies:** Issue #9 **File:** `pytest.ini` **Changes:** ```ini [pytest] asyncio_mode = auto testpaths = tests python_files = test_*.py python_classes = Test* python_functions = test_* addopts = -v --strict-markers ``` **Validation:** ```bash pytest --collect-only # Should collect tests from tests/ directory ``` --- ## Phase 6: Docker Infrastructure (Issues #11-14) ### Issue #11: Create Docker Directory Structure **Estimated Time:** 15 minutes **Dependencies:** None **Type:** Setup **Steps:** ```bash mkdir -p docker ``` **Validation:** ```bash ls -la docker/ ``` --- ### Issue #12: Create Docker Compose Configuration **Estimated Time:** 1-2 hours **Dependencies:** Issue #11 **File:** `docker/docker-compose.yml` **Complete file:** ```yaml version: '3.8' services: app: build: context: .. dockerfile: docker/Dockerfile container_name: gitea-mcp-remote-app restart: unless-stopped environment: # Gitea configuration GITEA_URL: ${GITEA_URL} GITEA_TOKEN: ${GITEA_TOKEN} GITEA_OWNER: ${GITEA_OWNER} GITEA_REPO: ${GITEA_REPO:-} # HTTP server HTTP_HOST: 0.0.0.0 HTTP_PORT: 8080 # Authentication AUTH_TOKEN: ${AUTH_TOKEN:-} MCP_AUTH_MODE: ${MCP_AUTH_MODE:-optional} # Tool filtering ENABLED_TOOLS: ${ENABLED_TOOLS:-} DISABLED_TOOLS: ${DISABLED_TOOLS:-} ports: - "8080:8080" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s networks: - mcp-network caddy: image: caddy:2-alpine container_name: gitea-mcp-remote-caddy restart: unless-stopped ports: - "443:443" - "80:80" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data - caddy-config:/config depends_on: app: condition: service_healthy networks: - mcp-network networks: mcp-network: driver: bridge volumes: caddy-data: caddy-config: ``` **Validation:** ```bash docker-compose -f docker/docker-compose.yml config # Should validate without errors ``` --- ### Issue #13: Create Dockerfile **Estimated Time:** 1 hour **Dependencies:** Issue #11 **File:** `docker/Dockerfile` **Complete file:** ```dockerfile FROM python:3.11-slim # Install system dependencies RUN apt-get update && apt-get install -y \ git \ curl \ && rm -rf /var/lib/apt/lists/* # Set working directory WORKDIR /app # Copy requirements first (for layer caching) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy project files COPY pyproject.toml . COPY README.md . COPY src/ src/ # Install project with marketplace dependency RUN pip install --no-cache-dir -e . # Create non-root user RUN useradd -m -u 1000 mcpuser && \ chown -R mcpuser:mcpuser /app USER mcpuser # Expose port EXPOSE 8080 # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 # Run server CMD ["gitea-mcp-remote"] ``` **Validation:** ```bash docker build -f docker/Dockerfile -t gitea-mcp-remote:test . # Should build successfully docker run --rm gitea-mcp-remote:test python3 -c "from gitea_mcp_remote.server_http import main" # Should import successfully ``` --- ### Issue #14: Create Caddyfile **Estimated Time:** 45 minutes **Dependencies:** Issue #11 **File:** `docker/Caddyfile` **Complete file:** ```caddyfile { # Global options admin off auto_https disable_redirects } :443 { # TLS configuration tls internal # MCP endpoint handle /mcp* { reverse_proxy app:8080 } # Health checks handle /health* { reverse_proxy app:8080 } handle /ping { reverse_proxy app:8080 } # Default response handle { respond "Gitea MCP Remote - Use /mcp endpoint" 200 } # Logging log { output stdout format console } } ``` **Validation:** ```bash # Validate Caddyfile syntax docker run --rm -v $(pwd)/docker/Caddyfile:/etc/caddy/Caddyfile caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile ``` --- ## Phase 7: Utility Scripts (Issue #15) ### Issue #15: Create Startup and Health Check Scripts **Estimated Time:** 1 hour **Dependencies:** None **Files:** `scripts/start.sh`, `scripts/healthcheck.sh` **`scripts/start.sh`:** ```bash #!/usr/bin/env bash # Production startup script for gitea-mcp-remote set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" echo "=== Gitea MCP Remote Startup ===" # Validate required environment variables required_vars=("GITEA_URL" "GITEA_TOKEN" "GITEA_OWNER") for var in "${required_vars[@]}"; do if [[ -z "${!var:-}" ]]; then echo "ERROR: $var is not set" exit 1 fi done echo "✓ Environment validated" # Optional: Load from .env if exists if [[ -f "$PROJECT_ROOT/.env" ]]; then echo "Loading environment from .env" set -a source "$PROJECT_ROOT/.env" set +a fi # Log configuration (sanitized) echo "Configuration:" echo " GITEA_URL: ${GITEA_URL}" echo " GITEA_OWNER: ${GITEA_OWNER}" echo " GITEA_REPO: ${GITEA_REPO:-}" echo " HTTP_HOST: ${HTTP_HOST:-0.0.0.0}" echo " HTTP_PORT: ${HTTP_PORT:-8080}" echo " MCP_AUTH_MODE: ${MCP_AUTH_MODE:-optional}" # Start server echo "Starting MCP HTTP server..." cd "$PROJECT_ROOT" exec gitea-mcp-remote ``` **`scripts/healthcheck.sh`:** ```bash #!/usr/bin/env bash # Docker healthcheck script set -euo pipefail HOST="${HTTP_HOST:-0.0.0.0}" PORT="${HTTP_PORT:-8080}" # Check health endpoint if curl -f -s "http://${HOST}:${PORT}/health" > /dev/null; then exit 0 else exit 1 fi ``` **Make executable:** ```bash chmod +x scripts/start.sh scripts/healthcheck.sh ``` **Validation:** ```bash # Test start script (will fail without env vars - expected) ./scripts/start.sh 2>&1 | grep "ERROR: GITEA_URL" # Test healthcheck script (needs server running) # export HTTP_HOST=localhost HTTP_PORT=8080 # ./scripts/healthcheck.sh ``` --- ## Phase 8: New Tests (Issue #16) ### Issue #16: Create MCP Server Tests **Estimated Time:** 2-3 hours **Dependencies:** Issue #8 **File:** `tests/test_server_http.py` **Complete file:** ```python """Tests for MCP HTTP server.""" import pytest from starlette.testclient import TestClient from gitea_mcp_remote.config import GiteaSettings from gitea_mcp_remote.server_http import create_app @pytest.fixture def settings(): """Test settings.""" return GiteaSettings( gitea_url="https://gitea.test.com", gitea_token="test_token", gitea_owner="test_owner", gitea_repo="test_repo", http_host="127.0.0.1", http_port=8080, auth_token="test_auth_token", mcp_auth_mode="optional", ) @pytest.fixture def client(settings): """Test client.""" app = create_app(settings) return TestClient(app) def test_health_endpoints(client): """Test health check endpoints.""" for path in ["/health", "/healthz", "/ping"]: response = client.get(path) assert response.status_code == 200 assert response.json() == {"status": "healthy"} def test_mcp_head_endpoint(client): """Test HEAD /mcp returns protocol version.""" response = client.head("/mcp") assert response.status_code == 200 assert "X-MCP-Version" in response.headers assert response.headers["X-MCP-Version"] == "2024-11-05" def test_mcp_post_initialize(client): """Test POST /mcp with initialize method.""" request_data = { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} } response = client.post("/mcp", json=request_data) assert response.status_code == 200 data = response.json() assert data["jsonrpc"] == "2.0" assert data["id"] == 1 assert "result" in data assert data["result"]["protocolVersion"] == "2024-11-05" def test_mcp_post_invalid_json(client): """Test POST /mcp with invalid JSON.""" response = client.post( "/mcp", content=b"not valid json", headers={"Content-Type": "application/json"} ) assert response.status_code == 400 data = response.json() assert "error" in data assert data["error"]["code"] == -32700 def test_mcp_post_unknown_method(client): """Test POST /mcp with unknown method.""" request_data = { "jsonrpc": "2.0", "id": 1, "method": "unknown/method", "params": {} } response = client.post("/mcp", json=request_data) assert response.status_code == 200 data = response.json() assert "error" in data assert data["error"]["code"] == -32000 @pytest.mark.asyncio async def test_tool_filtering(settings): """Test tool filtering integration.""" settings.disabled_tools = "create_issue,delete_issue" from gitea_mcp_remote.server_http import GiteaMCPServer server = GiteaMCPServer(settings) # List tools should exclude disabled result = await server.handle_list_tools({}) tool_names = [tool["name"] for tool in result["tools"]] assert "create_issue" not in tool_names assert "delete_issue" not in tool_names ``` **Validation:** ```bash pytest tests/test_server_http.py -v # All tests should pass ``` --- ## Phase 9: Documentation (Issue #17-18) ### Issue #17: Create CLAUDE.md **Estimated Time:** 1-2 hours **Dependencies:** All previous issues **File:** `CLAUDE.md` **Complete file:** ```markdown # CLAUDE.md - Gitea MCP Remote Project guidance for Claude Code when working with this repository. ## Project Overview **Type:** Python MCP HTTP Transport Server **Purpose:** Provide HTTP transport layer for Gitea MCP operations, enabling Claude Desktop integration **Architecture:** MCP Streamable HTTP protocol with JSON-RPC 2.0 ## Architecture ### Component Stack ``` Claude Desktop ↓ HTTP + JSON-RPC 2.0 Caddy (HTTPS proxy) ↓ server_http.py (MCP HTTP transport) ↓ Direct Python imports mcp_server (from marketplace) ↓ HTTPS API Gitea Instance ``` ### Key Components 1. **server_http.py** - MCP HTTP transport server - Implements MCP Streamable HTTP protocol - JSON-RPC 2.0 message handling - Routes: `POST /mcp`, `HEAD /mcp`, health endpoints 2. **config/settings.py** - Configuration management - Pydantic settings with environment variable loading - Gitea connection parameters - HTTP server configuration - Authentication and filtering options 3. **middleware/auth.py** - Authentication middleware - Bearer token authentication - Health check bypass 4. **filtering/filter.py** - Tool filtering - Whitelist/blacklist tool filtering - Claude Desktop compatibility layer 5. **mcp_server** (marketplace) - Core Gitea operations - GiteaClient for API operations - Tool definitions and dispatcher ## Development Workflows ### Local Development ```bash # Setup python3 -m venv .venv source .venv/bin/activate pip install -e ".[dev]" # Run tests pytest tests/ -v # Run server export GITEA_URL=https://gitea.test.com export GITEA_TOKEN=your_token export GITEA_OWNER=your_org gitea-mcp-remote ``` ### Docker Development ```bash # Build and run cd docker docker-compose up --build # View logs docker-compose logs -f app # Stop docker-compose down ``` ### Testing ```bash # All tests pytest tests/ -v # With coverage pytest tests/ --cov=gitea_mcp_remote --cov-report=html # Specific test file pytest tests/test_server_http.py -v ``` ## MCP Protocol Notes ### Streamable HTTP Transport This server implements MCP Streamable HTTP protocol: - **HEAD /mcp** - Protocol version check - Returns: `X-MCP-Version: 2024-11-05` header - **POST /mcp** - JSON-RPC 2.0 messages - Content-Type: `application/json` - Body: JSON-RPC 2.0 request/response ### JSON-RPC Methods 1. **initialize** - Client initialization ```json {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}} ``` 2. **tools/list** - List available tools ```json {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}} ``` 3. **tools/call** - Execute a tool ```json { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "list_issues", "arguments": {"owner": "org", "repo": "repo"} } } ``` ## Configuration ### Environment Variables **Required:** - `GITEA_URL` - Gitea instance URL - `GITEA_TOKEN` - Gitea API token - `GITEA_OWNER` - Default repository owner **Optional:** - `GITEA_REPO` - Default repository name - `HTTP_HOST` - Server bind address (default: 0.0.0.0) - `HTTP_PORT` - Server port (default: 8080) - `AUTH_TOKEN` - Bearer authentication token - `MCP_AUTH_MODE` - Auth mode: required/optional/none - `ENABLED_TOOLS` - Comma-separated whitelist - `DISABLED_TOOLS` - Comma-separated blacklist ### Authentication Modes - **none** - No authentication required - **optional** - Bearer token checked if provided - **required** - Bearer token mandatory ## Deployment ### Docker Compose (Production) ```bash # Setup environment cp .env.example .env nano .env # Configure # Deploy cd docker docker-compose up -d # Check health curl https://your-domain/health ``` ### Claude Desktop Configuration ```json { "mcpServers": { "gitea": { "url": "https://your-domain/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ``` ## Troubleshooting ### Import Errors If you see import errors from `gitea_http_wrapper`: - Package was renamed to `gitea_mcp_remote` - All imports should use new name - Check: `git grep -r gitea_http_wrapper` ### Marketplace Dependency Issues If marketplace install fails: - Check Git repository access: `https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace` - Ensure `git` is installed in Docker image - Check subdirectory path: `mcp-servers/gitea` ### MCP Protocol Errors If Claude Desktop can't connect: - Check protocol version: `curl -I https://your-domain/mcp` - Test JSON-RPC: `curl -X POST https://your-domain/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'` - Review server logs for errors ## Important Notes 1. **Do NOT use subprocess** - Import directly from `mcp_server` package 2. **Do NOT create custom REST API** - Use MCP protocol endpoints 3. **Do NOT skip marketplace dependency** - Must be in `pyproject.toml` 4. **Package name is gitea_mcp_remote** - Not gitea_http_wrapper 5. **Tests are at repo root** - Not in src/ directory ## References - MCP Spec: https://spec.modelcontextprotocol.io - Marketplace: https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace - JSON-RPC 2.0: https://www.jsonrpc.org/specification ``` --- ### Issue #18: Update DEPLOYMENT.md **Estimated Time:** 1 hour **Dependencies:** All previous issues **File:** `DEPLOYMENT.md` **Changes needed:** - Update all references from `/tools/list` and `/tools/call` to `/mcp` - Update Docker structure (two services, new directory) - Update marketplace dependency installation - Update Claude Desktop config example - Add MCP protocol version checking - Update health check endpoints **Key sections to update:** 1. **Quick Start** - Reference `docker/docker-compose.yml` 2. **Configuration** - Add `MCP_AUTH_MODE` variable 3. **HTTP Endpoints** - Replace with MCP protocol endpoints 4. **Claude Desktop Config** - Update URL to `/mcp` 5. **Troubleshooting** - Add MCP protocol debugging --- ## Final Validation (All Issues Complete) ### Complete Validation Checklist ```bash # 1. Package structure ls -la src/gitea_mcp_remote/ ls -la tests/ ! ls -la src/gitea_http_wrapper/ 2>/dev/null # 2. No old imports ! git grep -r "gitea_http_wrapper" --include="*.py" # 3. Config fields python3 -c "from gitea_mcp_remote.config import GiteaSettings; assert hasattr(GiteaSettings, 'mcp_auth_mode')" # 4. Server has MCP endpoints python3 -c "from gitea_mcp_remote.server_http import app; paths = [r.path for r in app.routes]; assert '/mcp' in paths" # 5. Dependencies installable pip install -e . # 6. Entry point works which gitea-mcp-remote # 7. Tests pass pytest tests/ -v # 8. Docker builds docker build -f docker/Dockerfile -t gitea-mcp-remote:test . # 9. Docker compose validates docker-compose -f docker/docker-compose.yml config # 10. Caddyfile validates docker run --rm -v $(pwd)/docker/Caddyfile:/etc/caddy/Caddyfile caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile ``` ## Timeline Summary | Phase | Issues | Est. Time | |-------|--------|-----------| | 1. Package Restructuring | #1-2 | 2-3 hours | | 2. Supporting Modules | #3-4 | 1-2 hours | | 3. Test Relocation | #5-6 | 2 hours | | 4. Core Server | #7-8 | 5-6 hours | | 5. Project Config | #9-10 | 1 hour | | 6. Docker Infrastructure | #11-14 | 4-5 hours | | 7. Utility Scripts | #15 | 1 hour | | 8. New Tests | #16 | 2-3 hours | | 9. Documentation | #17-18 | 3-4 hours | **Total:** 21-30 hours (~1 week sprint)