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

1455 lines
33 KiB
Markdown

# 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:-<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`:**
```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)