Files
gitea-mcp-remote/docs/sprint-proposals/sprint-01-implementation-guide.md
lmiranda 16436c847a docs: Add Sprint 01 planning documentation
Create comprehensive sprint planning documentation for Core Architecture
Correction sprint. This addresses three fatal architectural problems from
v1.0.0 release.

Sprint documents include:
- Executive proposal with architecture analysis
- Detailed implementation guide with code snippets
- Issue breakdown with dependencies
- Sprint summary with approval checklist

Sprint creates 10 issues in Gitea milestone 29:
- Issues #19-28 covering package rename, MCP protocol implementation,
  Docker infrastructure, testing, and documentation
- Total estimated effort: 19-28 hours (1 week sprint)
- All issues properly sized (S/M), labeled, and dependency-tracked

This is attempt #3 - all details from architectural correction prompt
have been captured.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 17:53:59 -05:00

33 KiB

Sprint 01: Implementation Guide - Core Architecture Correction

Related Proposal: 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:

cd /home/lmiranda/gitea-mcp-remote
git mv src/gitea_http_wrapper src/gitea_mcp_remote

Validation:

# 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:

# 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:

"""Configuration module for Gitea MCP HTTP transport."""

from gitea_mcp_remote.config.settings import GiteaSettings, load_settings

__all__ = ["GiteaSettings", "load_settings"]

Validation:

# 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:

"""Middleware components for MCP HTTP transport."""

from gitea_mcp_remote.middleware.auth import (
    BearerAuthMiddleware,
    HealthCheckBypassMiddleware,
)

__all__ = [
    "BearerAuthMiddleware",
    "HealthCheckBypassMiddleware",
]

Validation:

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:

# 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:

"""Tool filtering module for MCP HTTP transport."""

from gitea_mcp_remote.filtering.filter import ToolFilter

__all__ = ["ToolFilter"]

Validation:

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:

cd /home/lmiranda/gitea-mcp-remote
git mv src/gitea_mcp_remote/tests tests

Validation:

# 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:

# 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:

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:

git rm src/gitea_mcp_remote/server.py

Validation:

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:

"""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:

# 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:

[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:

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:

[pytest]
asyncio_mode = auto
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --strict-markers

Validation:

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:

mkdir -p docker

Validation:

ls -la docker/

Issue #12: Create Docker Compose Configuration

Estimated Time: 1-2 hours Dependencies: Issue #11 File: docker/docker-compose.yml

Complete file:

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:

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:

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:

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:

{
    # 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:

# 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:

#!/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:-<not set>}"
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:

#!/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:

chmod +x scripts/start.sh scripts/healthcheck.sh

Validation:

# 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:

"""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:

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:

# 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

# Build and run
cd docker
docker-compose up --build

# View logs
docker-compose logs -f app

# Stop
docker-compose down

Testing

# 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

    {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
    
  2. tools/list - List available tools

    {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
    
  3. tools/call - Execute a tool

    {
      "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)

# 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

{
  "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


---

### 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)