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>
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.pytests/test_config.pytests/test_middleware.pytests/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-05header
- Returns:
-
POST /mcp - JSON-RPC 2.0 messages
- Content-Type:
application/json - Body: JSON-RPC 2.0 request/response
- Content-Type:
JSON-RPC Methods
-
initialize - Client initialization
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}} -
tools/list - List available tools
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}} -
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 URLGITEA_TOKEN- Gitea API tokenGITEA_OWNER- Default repository owner
Optional:
GITEA_REPO- Default repository nameHTTP_HOST- Server bind address (default: 0.0.0.0)HTTP_PORT- Server port (default: 8080)AUTH_TOKEN- Bearer authentication tokenMCP_AUTH_MODE- Auth mode: required/optional/noneENABLED_TOOLS- Comma-separated whitelistDISABLED_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
gitis 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
- Do NOT use subprocess - Import directly from
mcp_serverpackage - Do NOT create custom REST API - Use MCP protocol endpoints
- Do NOT skip marketplace dependency - Must be in
pyproject.toml - Package name is gitea_mcp_remote - Not gitea_http_wrapper
- 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)