generated from personal-projects/leo-claude-mktplace
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30e28dd09e | |||
| 16ca5cd644 | |||
| 2dbb66deae | |||
| 88c16c840b | |||
| 9fea0683f7 | |||
| a5390a3086 | |||
| c3caf4e169 | |||
| cd8718c114 | |||
| 53a0006a3f | |||
| fb8cc08112 | |||
| 809eef132a | |||
| 1c55eed7c0 | |||
| 5075139841 | |||
| 16436c847a | |||
| 608b488763 | |||
| 49f2d0bdbb | |||
| f2cba079eb | |||
| 4e81b9bb96 | |||
| d21f85545b | |||
| 3d1fd2e2a6 | |||
| 2fc43ff5c3 | |||
| d11649071e | |||
| 42d625c27f | |||
| 6beb8026df | |||
| acacefeaed | |||
| 604661f096 |
@@ -1,3 +1,7 @@
|
|||||||
|
# --- Gitea MCP Server env vars (used by marketplace package) ---
|
||||||
|
GITEA_API_URL=https://gitea.hotserv.cloud/api/v1
|
||||||
|
GITEA_API_TOKEN=your_gitea_personal_access_token
|
||||||
|
|
||||||
# Gitea Configuration
|
# Gitea Configuration
|
||||||
GITEA_URL=https://gitea.example.com
|
GITEA_URL=https://gitea.example.com
|
||||||
GITEA_TOKEN=your_gitea_api_token_here
|
GITEA_TOKEN=your_gitea_api_token_here
|
||||||
@@ -8,6 +12,9 @@ GITEA_REPO=your_repo_name
|
|||||||
HTTP_HOST=127.0.0.1
|
HTTP_HOST=127.0.0.1
|
||||||
HTTP_PORT=8000
|
HTTP_PORT=8000
|
||||||
|
|
||||||
|
# --- Caddy / TLS ---
|
||||||
|
MCP_DOMAIN=mcp-gitea.hotserv.cloud
|
||||||
|
|
||||||
# Authentication Configuration (Optional)
|
# Authentication Configuration (Optional)
|
||||||
# AUTH_TOKEN=your_bearer_token_here
|
# AUTH_TOKEN=your_bearer_token_here
|
||||||
|
|
||||||
|
|||||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.0.0] - 2026-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- MCP Streamable HTTP protocol support (`/mcp` endpoint with POST/HEAD)
|
||||||
|
- Two-service Docker infrastructure with Caddy reverse proxy (`docker/`)
|
||||||
|
- Production startup script (`scripts/start.sh`) with environment validation
|
||||||
|
- Health check script (`scripts/healthcheck.sh`) for Docker
|
||||||
|
- CLAUDE.md with comprehensive project guidance for AI assistants
|
||||||
|
- Health endpoint tests with authentication bypass verification
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **BREAKING**: Package renamed from `gitea_http_wrapper` to `gitea_mcp_remote`
|
||||||
|
- **BREAKING**: Default HTTP port changed from 8000 to 8080
|
||||||
|
- **BREAKING**: Default HTTP host changed from 127.0.0.1 to 0.0.0.0
|
||||||
|
- Architecture changed from subprocess wrapper to direct marketplace import
|
||||||
|
- Server implementation uses `StreamableHTTPServerTransport` from MCP SDK
|
||||||
|
- Tests moved from `src/gitea_mcp_remote/tests/` to repository root `tests/`
|
||||||
|
- Middleware order fixed: HealthCheckBypass now wraps BearerAuth (runs first)
|
||||||
|
- Docker files moved to `docker/` directory with Caddy service added
|
||||||
|
- Updated README.md and DEPLOYMENT.md with correct architecture documentation
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Health endpoint returning 401 when AUTH_TOKEN was set (middleware order bug)
|
||||||
|
- pyproject.toml testpaths pointing to wrong directory
|
||||||
|
|
||||||
## [1.0.0] - 2025-02-03
|
## [1.0.0] - 2025-02-03
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
239
CLAUDE.md
Normal file
239
CLAUDE.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# CLAUDE.md - Gitea MCP Remote
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code when working with this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Name:** gitea-mcp-remote
|
||||||
|
**Type:** HTTP transport server for MCP (Model Context Protocol)
|
||||||
|
**Purpose:** Expose Gitea operations via MCP Streamable HTTP protocol for AI assistants
|
||||||
|
|
||||||
|
This is NOT a standalone MCP server. It imports tools from the `gitea-mcp-server` marketplace package and serves them over HTTP with authentication and tool filtering.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Client (Claude Desktop) ──HTTP──▶ gitea-mcp-remote ──imports──▶ gitea-mcp-server ──API──▶ Gitea
|
||||||
|
│
|
||||||
|
├── Authentication (Bearer token)
|
||||||
|
├── Tool Filtering (enable/disable)
|
||||||
|
└── MCP Streamable HTTP protocol
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Design Decision:** This server uses direct imports from the marketplace `gitea-mcp-server` package, NOT subprocess spawning. Tool definitions and dispatchers are imported and used directly.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
gitea-mcp-remote/
|
||||||
|
├── src/gitea_mcp_remote/ # Main package
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── server_http.py # MCP HTTP server (main module)
|
||||||
|
│ ├── config/ # Configuration module
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ └── settings.py # Pydantic settings loader
|
||||||
|
│ ├── middleware/ # HTTP middleware
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ └── auth.py # Bearer auth + health check bypass
|
||||||
|
│ └── filtering/ # Tool filtering
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── filter.py # Whitelist/blacklist filtering
|
||||||
|
├── tests/ # Test suite (at repo root)
|
||||||
|
│ ├── conftest.py
|
||||||
|
│ ├── test_config.py
|
||||||
|
│ ├── test_middleware.py
|
||||||
|
│ ├── test_filtering.py
|
||||||
|
│ └── test_mcp_endpoints.py
|
||||||
|
├── docker/ # Docker infrastructure
|
||||||
|
│ ├── Dockerfile # Multi-stage build
|
||||||
|
│ ├── docker-compose.yml # App + Caddy services
|
||||||
|
│ ├── Caddyfile # Reverse proxy config
|
||||||
|
│ └── .env.example # Environment template
|
||||||
|
├── scripts/ # Utility scripts
|
||||||
|
│ ├── start.sh # Production startup
|
||||||
|
│ └── healthcheck.sh # Docker health check
|
||||||
|
├── docs/ # Documentation
|
||||||
|
│ └── sprint-proposals/ # Sprint planning docs
|
||||||
|
├── pyproject.toml # Project configuration
|
||||||
|
├── requirements.txt # Dependencies
|
||||||
|
├── README.md # User documentation
|
||||||
|
├── DEPLOYMENT.md # Deployment guide
|
||||||
|
└── CLAUDE.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflows
|
||||||
|
|
||||||
|
### Setup Development Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create virtual environment
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
# Install with development dependencies
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All tests
|
||||||
|
pytest tests/ -v
|
||||||
|
|
||||||
|
# With coverage
|
||||||
|
pytest tests/ --cov=gitea_mcp_remote
|
||||||
|
|
||||||
|
# Specific test file
|
||||||
|
pytest tests/test_mcp_endpoints.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Server Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set required environment variables
|
||||||
|
export GITEA_URL=https://gitea.example.com
|
||||||
|
export GITEA_TOKEN=your_token
|
||||||
|
export GITEA_OWNER=your_org
|
||||||
|
|
||||||
|
# Run server
|
||||||
|
gitea-mcp-remote
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and run
|
||||||
|
docker-compose -f docker/docker-compose.yml up --build
|
||||||
|
|
||||||
|
# Validate configuration
|
||||||
|
docker-compose -f docker/docker-compose.yml config
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker-compose -f docker/docker-compose.yml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
### server_http.py
|
||||||
|
|
||||||
|
The main MCP server implementation:
|
||||||
|
- Imports tools from `mcp_server` (marketplace package)
|
||||||
|
- Creates `Server` instance with tool handlers
|
||||||
|
- Uses `StreamableHTTPServerTransport` for MCP protocol
|
||||||
|
- Health endpoint at `/health` (bypasses auth)
|
||||||
|
- MCP endpoint at `/mcp` (POST for requests, HEAD for protocol version)
|
||||||
|
|
||||||
|
### config/settings.py
|
||||||
|
|
||||||
|
Pydantic settings with:
|
||||||
|
- `gitea_url`, `gitea_token`, `gitea_owner` (required)
|
||||||
|
- `gitea_repo` (optional)
|
||||||
|
- `http_host` (default: 0.0.0.0), `http_port` (default: 8080)
|
||||||
|
- `auth_token` (optional Bearer token)
|
||||||
|
- `mcp_auth_mode` (optional/required/none)
|
||||||
|
- `enabled_tools`, `disabled_tools` (comma-separated)
|
||||||
|
|
||||||
|
### middleware/auth.py
|
||||||
|
|
||||||
|
Two middleware classes:
|
||||||
|
- `BearerAuthMiddleware`: Validates Authorization header
|
||||||
|
- `HealthCheckBypassMiddleware`: Sets `skip_auth` flag for health endpoints
|
||||||
|
|
||||||
|
**Important:** Middleware order matters. HealthCheckBypass must wrap BearerAuth (outermost) so it runs first.
|
||||||
|
|
||||||
|
## MCP Protocol Notes
|
||||||
|
|
||||||
|
This server implements the MCP Streamable HTTP protocol:
|
||||||
|
|
||||||
|
1. **HEAD /mcp** - Returns protocol version header (`x-mcp-protocol-version: 2024-11-05`)
|
||||||
|
2. **POST /mcp** - Accepts JSON-RPC 2.0 requests, returns SSE responses
|
||||||
|
|
||||||
|
Supported methods:
|
||||||
|
- `initialize` - Protocol handshake
|
||||||
|
- `tools/list` - List available tools
|
||||||
|
- `tools/call` - Execute a tool
|
||||||
|
|
||||||
|
### Request Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "list_issues",
|
||||||
|
"arguments": {"owner": "org", "repo": "repo", "state": "open"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Format (SSE)
|
||||||
|
|
||||||
|
```
|
||||||
|
event: message
|
||||||
|
data: {"jsonrpc":"2.0","id":1,"result":[...]}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| GITEA_URL | Yes | - | Gitea instance URL |
|
||||||
|
| GITEA_TOKEN | Yes | - | Gitea API token |
|
||||||
|
| GITEA_OWNER | Yes | - | Default owner/org |
|
||||||
|
| GITEA_REPO | No | None | Default repository |
|
||||||
|
| HTTP_HOST | No | 0.0.0.0 | Server bind address |
|
||||||
|
| HTTP_PORT | No | 8080 | Server port |
|
||||||
|
| AUTH_TOKEN | No | None | Bearer token for auth |
|
||||||
|
| MCP_AUTH_MODE | No | optional | Auth mode |
|
||||||
|
| ENABLED_TOOLS | No | None | Whitelist (comma-sep) |
|
||||||
|
| DISABLED_TOOLS | No | None | Blacklist (comma-sep) |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Import Errors from mcp_server
|
||||||
|
|
||||||
|
If `from mcp_server import ...` fails:
|
||||||
|
1. Verify `gitea-mcp-server` is installed: `pip list | grep gitea`
|
||||||
|
2. The package is installed from Git via pyproject.toml dependency
|
||||||
|
3. Reinstall: `pip install -e .`
|
||||||
|
|
||||||
|
### Health Endpoint Returns 401
|
||||||
|
|
||||||
|
Middleware order is wrong. HealthCheckBypassMiddleware must be outermost:
|
||||||
|
```python
|
||||||
|
if settings.auth_token:
|
||||||
|
app = BearerAuthMiddleware(app, auth_token=settings.auth_token)
|
||||||
|
app = HealthCheckBypassMiddleware(app) # Must be last (outermost)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests Not Found
|
||||||
|
|
||||||
|
Ensure pyproject.toml has correct testpaths:
|
||||||
|
```toml
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCP Requests Fail with 406
|
||||||
|
|
||||||
|
Missing Accept header. Requests must include:
|
||||||
|
```
|
||||||
|
Accept: application/json, text/event-stream
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**Runtime:**
|
||||||
|
- `gitea-mcp-server` - Marketplace package (Git dependency)
|
||||||
|
- `mcp` - MCP SDK
|
||||||
|
- `uvicorn` - ASGI server
|
||||||
|
- `starlette` - Web framework
|
||||||
|
- `pydantic-settings` - Configuration
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
- `pytest` - Testing
|
||||||
|
- `pytest-asyncio` - Async test support
|
||||||
|
- `pytest-cov` - Coverage
|
||||||
|
- `httpx` - HTTP client for tests
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Deployment Guide
|
# Deployment Guide
|
||||||
|
|
||||||
This guide covers production deployment of the Gitea HTTP MCP Wrapper in various environments.
|
This guide covers production deployment of Gitea MCP Remote in various environments.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
@@ -36,15 +36,15 @@ This guide covers production deployment of the Gitea HTTP MCP Wrapper in various
|
|||||||
1. **Clone the repository:**
|
1. **Clone the repository:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/lmiranda/gitea-mcp-remote.git
|
git clone https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote.git
|
||||||
cd gitea-mcp-remote
|
cd gitea-mcp-remote
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Create configuration:**
|
2. **Create configuration:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.docker.example .env
|
cp docker/.env.example docker/.env
|
||||||
nano .env # Edit with your values
|
nano docker/.env # Edit with your values
|
||||||
```
|
```
|
||||||
|
|
||||||
Required configuration:
|
Required configuration:
|
||||||
@@ -52,49 +52,49 @@ Required configuration:
|
|||||||
GITEA_URL=https://gitea.example.com
|
GITEA_URL=https://gitea.example.com
|
||||||
GITEA_TOKEN=your_gitea_api_token
|
GITEA_TOKEN=your_gitea_api_token
|
||||||
GITEA_OWNER=your_username_or_org
|
GITEA_OWNER=your_username_or_org
|
||||||
GITEA_REPO=your_default_repo
|
GITEA_REPO=your_default_repo # Optional
|
||||||
AUTH_TOKEN=your_bearer_token # Recommended
|
AUTH_TOKEN=your_bearer_token # Recommended
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Start the service:**
|
3. **Start the services (app + Caddy):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose -f docker/docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Verify deployment:**
|
4. **Verify deployment:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:8000/health
|
curl http://localhost/health # Via Caddy
|
||||||
|
curl http://localhost:8080/health # Direct to app
|
||||||
```
|
```
|
||||||
|
|
||||||
### Production Configuration
|
### Production Configuration
|
||||||
|
|
||||||
For production, use a more robust `docker-compose.yml`:
|
The default `docker/docker-compose.yml` includes both app and Caddy reverse proxy services. For customization:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
gitea-mcp-wrapper:
|
# Python MCP Server
|
||||||
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: docker/Dockerfile
|
||||||
image: gitea-mcp-wrapper:latest
|
image: gitea-mcp-remote:latest
|
||||||
container_name: gitea-mcp-wrapper
|
container_name: gitea-mcp-remote-app
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
expose:
|
||||||
- "127.0.0.1:8000:8000" # Bind to localhost only
|
- "8080"
|
||||||
environment:
|
environment:
|
||||||
- GITEA_URL=${GITEA_URL}
|
- GITEA_URL=${GITEA_URL}
|
||||||
- GITEA_TOKEN=${GITEA_TOKEN}
|
- GITEA_TOKEN=${GITEA_TOKEN}
|
||||||
- GITEA_OWNER=${GITEA_OWNER}
|
- GITEA_OWNER=${GITEA_OWNER}
|
||||||
- GITEA_REPO=${GITEA_REPO}
|
- GITEA_REPO=${GITEA_REPO:-}
|
||||||
- HTTP_HOST=0.0.0.0
|
- HTTP_HOST=0.0.0.0
|
||||||
- HTTP_PORT=8000
|
- HTTP_PORT=8080
|
||||||
- AUTH_TOKEN=${AUTH_TOKEN}
|
- AUTH_TOKEN=${AUTH_TOKEN:-}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()"]
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -107,9 +107,31 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- gitea-mcp-network
|
- gitea-mcp-network
|
||||||
|
|
||||||
|
# Caddy Reverse Proxy (HTTPS termination)
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
container_name: gitea-mcp-remote-caddy
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
|
depends_on:
|
||||||
|
app:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- gitea-mcp-network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
gitea-mcp-network:
|
gitea-mcp-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy_data:
|
||||||
|
caddy_config:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker Build Options
|
### Docker Build Options
|
||||||
@@ -117,20 +139,20 @@ networks:
|
|||||||
**Build the image:**
|
**Build the image:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t gitea-mcp-wrapper:latest .
|
docker build -f docker/Dockerfile -t gitea-mcp-remote:latest .
|
||||||
```
|
```
|
||||||
|
|
||||||
**Build with specific Python version:**
|
**Build with specific Python version:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build --build-arg PYTHON_VERSION=3.11 -t gitea-mcp-wrapper:latest .
|
docker build -f docker/Dockerfile --build-arg PYTHON_VERSION=3.11 -t gitea-mcp-remote:latest .
|
||||||
```
|
```
|
||||||
|
|
||||||
**Tag for registry:**
|
**Tag for registry:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker tag gitea-mcp-wrapper:latest registry.example.com/gitea-mcp-wrapper:latest
|
docker tag gitea-mcp-remote:latest registry.example.com/gitea-mcp-remote:latest
|
||||||
docker push registry.example.com/gitea-mcp-wrapper:latest
|
docker push registry.example.com/gitea-mcp-remote:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security Best Practices
|
## Security Best Practices
|
||||||
@@ -218,7 +240,7 @@ Docker automatically monitors the health check and can restart if unhealthy:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
183
README.md
183
README.md
@@ -1,18 +1,22 @@
|
|||||||
# Gitea HTTP MCP Wrapper
|
# Gitea MCP Remote
|
||||||
|
|
||||||
An HTTP transport wrapper around the official Gitea MCP server that enables AI assistants like Claude Desktop to interact with Gitea repositories via HTTP. This wrapper provides authentication, tool filtering, and HTTP transport while delegating Gitea operations to the official `gitea-mcp-server`.
|
An HTTP transport server that exposes Gitea operations via the MCP Streamable HTTP protocol for AI assistants like Claude Desktop. This server imports tools from the marketplace `gitea-mcp-server` package and serves them over HTTP with authentication and tool filtering.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
This is NOT a standalone MCP server. It's an HTTP wrapper that:
|
This is NOT a standalone MCP server. It imports tool definitions from the marketplace package and serves them:
|
||||||
1. Wraps the official `gitea-mcp-server` (stdio transport)
|
1. Imports tools from `gitea-mcp-server` marketplace package
|
||||||
2. Provides HTTP transport for Claude Desktop compatibility
|
2. Serves via MCP Streamable HTTP protocol (`/mcp` endpoint)
|
||||||
3. Adds Bearer token authentication
|
3. Adds Bearer token authentication (optional)
|
||||||
4. Filters tools for Claude Desktop compatibility
|
4. Provides tool filtering (whitelist/blacklist)
|
||||||
5. Proxies requests between HTTP and stdio transport
|
5. Health check endpoints for monitoring
|
||||||
|
|
||||||
```
|
```
|
||||||
Claude Desktop (HTTP) → HTTP Wrapper → Gitea MCP Server (stdio) → Gitea API
|
Claude Desktop (HTTP) ──▶ gitea-mcp-remote ──imports──▶ gitea-mcp-server ──API──▶ Gitea
|
||||||
|
│
|
||||||
|
├── Authentication (Bearer token)
|
||||||
|
├── Tool Filtering
|
||||||
|
└── MCP Streamable HTTP protocol
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -33,27 +37,27 @@ Claude Desktop (HTTP) → HTTP Wrapper → Gitea MCP Server (stdio) → Gitea AP
|
|||||||
|
|
||||||
## Quick Start with Docker
|
## Quick Start with Docker
|
||||||
|
|
||||||
The easiest way to deploy is using Docker Compose:
|
The easiest way to deploy is using Docker Compose with Caddy reverse proxy:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Clone the repository
|
# 1. Clone the repository
|
||||||
git clone https://github.com/lmiranda/gitea-mcp-remote.git
|
git clone https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote.git
|
||||||
cd gitea-mcp-remote
|
cd gitea-mcp-remote
|
||||||
|
|
||||||
# 2. Create .env file from template
|
# 2. Create .env file from template
|
||||||
cp .env.docker.example .env
|
cp docker/.env.example docker/.env
|
||||||
|
|
||||||
# 3. Edit .env with your Gitea credentials
|
# 3. Edit .env with your Gitea credentials
|
||||||
nano .env
|
nano docker/.env
|
||||||
|
|
||||||
# 4. Start the server
|
# 4. Start the services (app + Caddy)
|
||||||
docker-compose up -d
|
docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
|
||||||
# 5. Check health
|
# 5. Check health
|
||||||
curl http://localhost:8000/health
|
curl http://localhost/health
|
||||||
```
|
```
|
||||||
|
|
||||||
The server will be available at `http://localhost:8000`.
|
The server will be available at `http://localhost` (Caddy) or `http://localhost:8080` (direct).
|
||||||
|
|
||||||
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed deployment instructions.
|
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed deployment instructions.
|
||||||
|
|
||||||
@@ -96,11 +100,13 @@ The wrapper uses environment variables or a `.env` file for configuration.
|
|||||||
GITEA_URL=https://gitea.example.com
|
GITEA_URL=https://gitea.example.com
|
||||||
GITEA_TOKEN=your_gitea_api_token_here
|
GITEA_TOKEN=your_gitea_api_token_here
|
||||||
GITEA_OWNER=your_username_or_org
|
GITEA_OWNER=your_username_or_org
|
||||||
GITEA_REPO=your_repo_name
|
|
||||||
|
|
||||||
# HTTP Server
|
# Optional
|
||||||
HTTP_HOST=127.0.0.1 # Use 0.0.0.0 in Docker
|
GITEA_REPO=your_repo_name # Can be omitted, specified per-request
|
||||||
HTTP_PORT=8000
|
|
||||||
|
# HTTP Server (defaults)
|
||||||
|
HTTP_HOST=0.0.0.0
|
||||||
|
HTTP_PORT=8080
|
||||||
```
|
```
|
||||||
|
|
||||||
### Optional Configuration
|
### Optional Configuration
|
||||||
@@ -131,6 +137,7 @@ DISABLED_TOOLS=delete_issue,close_milestone # Blacklist mode
|
|||||||
#### With Docker
|
#### With Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd docker
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -143,46 +150,71 @@ cp .env.example .env
|
|||||||
nano .env
|
nano .env
|
||||||
|
|
||||||
# Run the server
|
# Run the server
|
||||||
gitea-http-wrapper
|
gitea-mcp-remote
|
||||||
|
# Or use the startup script
|
||||||
|
./scripts/start.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The server will start on the configured host/port (default: `http://127.0.0.1:8000`).
|
The server will start on the configured host/port (default: `http://0.0.0.0:8080`).
|
||||||
|
|
||||||
### HTTP Endpoints
|
### HTTP Endpoints
|
||||||
|
|
||||||
#### Health Check
|
#### Health Check
|
||||||
```bash
|
```bash
|
||||||
GET /health
|
GET /health
|
||||||
GET /healthz
|
|
||||||
GET /ping
|
|
||||||
|
|
||||||
Response: {"status": "healthy"}
|
Response: {"status": "ok"}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### List Tools
|
#### MCP Protocol Endpoint
|
||||||
```bash
|
|
||||||
POST /tools/list
|
|
||||||
|
|
||||||
Response: {
|
The server implements the MCP Streamable HTTP protocol:
|
||||||
"tools": [
|
|
||||||
{"name": "list_issues", "description": "...", "inputSchema": {...}},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Call Tool
|
|
||||||
```bash
|
```bash
|
||||||
POST /tools/call
|
# Check protocol version
|
||||||
|
HEAD /mcp
|
||||||
|
|
||||||
|
# Send MCP JSON-RPC requests
|
||||||
|
POST /mcp
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
Accept: application/json, text/event-stream
|
||||||
Authorization: Bearer YOUR_TOKEN # If auth enabled
|
Authorization: Bearer YOUR_TOKEN # If auth enabled
|
||||||
|
|
||||||
|
# Example: Initialize
|
||||||
{
|
{
|
||||||
"name": "list_issues",
|
"jsonrpc": "2.0",
|
||||||
"arguments": {
|
"id": 1,
|
||||||
"owner": "myorg",
|
"method": "initialize",
|
||||||
"repo": "myrepo",
|
"params": {
|
||||||
"state": "open"
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {},
|
||||||
|
"clientInfo": {
|
||||||
|
"name": "my-client",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Example: List tools
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 2,
|
||||||
|
"method": "tools/list",
|
||||||
|
"params": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Example: Call tool
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 3,
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "list_issues",
|
||||||
|
"arguments": {
|
||||||
|
"owner": "myorg",
|
||||||
|
"repo": "myrepo",
|
||||||
|
"state": "open"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -202,7 +234,7 @@ Configure Claude Desktop to use the HTTP wrapper:
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"gitea": {
|
"gitea": {
|
||||||
"url": "http://localhost:8000",
|
"url": "http://localhost:8080/mcp",
|
||||||
"headers": {
|
"headers": {
|
||||||
"Authorization": "Bearer YOUR_TOKEN"
|
"Authorization": "Bearer YOUR_TOKEN"
|
||||||
}
|
}
|
||||||
@@ -234,44 +266,49 @@ pip install -e ".[dev]"
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Run all tests
|
||||||
pytest
|
pytest tests/ -v
|
||||||
|
|
||||||
# Run with coverage
|
# Run with coverage
|
||||||
pytest --cov=gitea_http_wrapper
|
pytest tests/ --cov=gitea_mcp_remote
|
||||||
|
|
||||||
# Run specific test file
|
# Run specific test file
|
||||||
pytest src/gitea_http_wrapper/tests/test_config.py
|
pytest tests/test_mcp_endpoints.py -v
|
||||||
```
|
```
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
gitea-mcp-remote/
|
gitea-mcp-remote/
|
||||||
├── src/
|
├── src/gitea_mcp_remote/ # Main package
|
||||||
│ └── gitea_http_wrapper/
|
│ ├── __init__.py
|
||||||
|
│ ├── server_http.py # MCP HTTP server
|
||||||
|
│ ├── config/ # Configuration module
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ └── settings.py
|
||||||
|
│ ├── middleware/ # HTTP middleware
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ └── auth.py
|
||||||
|
│ └── filtering/ # Tool filtering
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ ├── server.py # Main HTTP server
|
│ └── filter.py
|
||||||
│ ├── config/
|
├── tests/ # Test suite (at repo root)
|
||||||
│ │ ├── __init__.py
|
│ ├── conftest.py
|
||||||
│ │ └── settings.py # Configuration loader
|
│ ├── test_config.py
|
||||||
│ ├── middleware/
|
│ ├── test_filtering.py
|
||||||
│ │ ├── __init__.py
|
│ ├── test_middleware.py
|
||||||
│ │ └── auth.py # HTTP authentication
|
│ └── test_mcp_endpoints.py
|
||||||
│ ├── filtering/
|
├── docker/ # Docker infrastructure
|
||||||
│ │ ├── __init__.py
|
│ ├── Dockerfile
|
||||||
│ │ └── filter.py # Tool filtering
|
│ ├── docker-compose.yml
|
||||||
│ └── tests/ # Test suite
|
│ ├── Caddyfile
|
||||||
│ ├── conftest.py
|
│ └── .env.example
|
||||||
│ ├── test_config.py
|
├── scripts/ # Utility scripts
|
||||||
│ ├── test_filtering.py
|
│ ├── start.sh
|
||||||
│ └── test_middleware.py
|
│ └── healthcheck.sh
|
||||||
├── Dockerfile # Docker image
|
|
||||||
├── docker-compose.yml # Docker orchestration
|
|
||||||
├── pyproject.toml # Project config
|
├── pyproject.toml # Project config
|
||||||
├── requirements.txt # Dependencies
|
├── requirements.txt # Dependencies
|
||||||
├── .env.example # Config template
|
|
||||||
├── .env.docker.example # Docker config template
|
|
||||||
├── README.md # This file
|
├── README.md # This file
|
||||||
|
├── CLAUDE.md # Claude Code guidance
|
||||||
└── DEPLOYMENT.md # Deployment guide
|
└── DEPLOYMENT.md # Deployment guide
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -330,7 +367,7 @@ If tool filtering is not applied:
|
|||||||
1. Check `.env` file syntax (no spaces around `=`)
|
1. Check `.env` file syntax (no spaces around `=`)
|
||||||
2. Verify comma-separated list format
|
2. Verify comma-separated list format
|
||||||
3. Check server logs for filter configuration
|
3. Check server logs for filter configuration
|
||||||
4. Query `POST /tools/list` to see filtered tools
|
4. Send `tools/list` MCP request to see filtered tools
|
||||||
|
|
||||||
## Security Considerations
|
## Security Considerations
|
||||||
|
|
||||||
@@ -351,7 +388,7 @@ MIT License - see LICENSE file for details
|
|||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
Current version: 0.1.0
|
Current version: 2.0.0
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
|
|
||||||
@@ -359,7 +396,7 @@ Leo Miranda
|
|||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- Repository: https://github.com/lmiranda/gitea-mcp-remote
|
- Repository: https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote
|
||||||
- Issues: https://github.com/lmiranda/gitea-mcp-remote/issues
|
- Issues: https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/issues
|
||||||
- Official Gitea MCP Server: https://github.com/modelcontextprotocol/servers/tree/main/src/gitea
|
- Marketplace Package: https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/mcp-servers/gitea
|
||||||
- MCP Documentation: https://modelcontextprotocol.io
|
- MCP Documentation: https://modelcontextprotocol.io
|
||||||
|
|||||||
38
docker/.env.example
Normal file
38
docker/.env.example
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Gitea MCP Remote - Docker Environment Configuration
|
||||||
|
#
|
||||||
|
# Copy this file to .env and fill in your values:
|
||||||
|
# cp .env.example .env
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Required Configuration
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Gitea instance URL (e.g., https://gitea.example.com)
|
||||||
|
GITEA_URL=https://gitea.example.com
|
||||||
|
|
||||||
|
# Gitea API token for authentication
|
||||||
|
# Generate at: Settings -> Applications -> Generate New Token
|
||||||
|
GITEA_TOKEN=your_gitea_api_token_here
|
||||||
|
|
||||||
|
# Default repository owner/organization
|
||||||
|
GITEA_OWNER=your_username_or_org
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Optional Configuration
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Default repository name (optional - can be specified per-request)
|
||||||
|
GITEA_REPO=
|
||||||
|
|
||||||
|
# Bearer token for MCP endpoint authentication (optional)
|
||||||
|
# If set, clients must include "Authorization: Bearer <token>" header
|
||||||
|
AUTH_TOKEN=
|
||||||
|
|
||||||
|
# MCP authentication mode: 'required', 'optional', or 'none'
|
||||||
|
MCP_AUTH_MODE=optional
|
||||||
|
|
||||||
|
# Tool filtering (optional, comma-separated)
|
||||||
|
# ENABLED_TOOLS=list_issues,create_issue,list_labels
|
||||||
|
# DISABLED_TOOLS=delete_issue,delete_label
|
||||||
|
ENABLED_TOOLS=
|
||||||
|
DISABLED_TOOLS=
|
||||||
46
docker/Caddyfile
Normal file
46
docker/Caddyfile
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Caddy reverse proxy configuration for Gitea MCP Remote
|
||||||
|
#
|
||||||
|
# This configuration provides:
|
||||||
|
# - HTTPS termination with automatic certificates
|
||||||
|
# - Reverse proxy to the Python MCP server
|
||||||
|
# - Health check endpoint passthrough
|
||||||
|
# - MCP protocol endpoint routing
|
||||||
|
{
|
||||||
|
# Global options
|
||||||
|
email admin@example.com
|
||||||
|
|
||||||
|
# For local development, disable HTTPS redirect
|
||||||
|
# auto_https off
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default site - adjust domain as needed
|
||||||
|
:443, :80 {
|
||||||
|
# Health check endpoint - no authentication
|
||||||
|
handle /health {
|
||||||
|
reverse_proxy app:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
# MCP protocol endpoint
|
||||||
|
handle /mcp {
|
||||||
|
reverse_proxy app:8080 {
|
||||||
|
# Pass through headers for MCP protocol
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
header_up X-Real-IP {remote_host}
|
||||||
|
|
||||||
|
# Ensure proper content type handling
|
||||||
|
flush_interval -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback - proxy all other requests
|
||||||
|
handle {
|
||||||
|
reverse_proxy app:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
docker/Dockerfile
Normal file
65
docker/Dockerfile
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Gitea MCP Remote — Dockerfile
|
||||||
|
# Multi-stage build for optimized image size
|
||||||
|
|
||||||
|
FROM python:3.11-slim as builder
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Install build dependencies including git for marketplace dependency
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --user --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY pyproject.toml .
|
||||||
|
COPY src/ src/
|
||||||
|
|
||||||
|
# Install package (includes marketplace dependency from git)
|
||||||
|
RUN pip install --user --no-cache-dir .
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy installed packages from builder
|
||||||
|
COPY --from=builder /root/.local /root/.local
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY src/ src/
|
||||||
|
COPY pyproject.toml .
|
||||||
|
|
||||||
|
# Make sure scripts in .local are usable
|
||||||
|
ENV PATH=/root/.local/bin:$PATH
|
||||||
|
|
||||||
|
# Set Python environment variables
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
# Default to port 8080 (Caddy proxies to this)
|
||||||
|
ENV HTTP_PORT=8080
|
||||||
|
ENV HTTP_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Health check using curl
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
|
# Run the MCP server
|
||||||
|
CMD ["gitea-mcp-remote"]
|
||||||
66
docker/docker-compose.yml
Normal file
66
docker/docker-compose.yml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
services:
|
||||||
|
# Python MCP Server
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
image: gitea-mcp-remote:latest
|
||||||
|
container_name: gitea-mcp-remote-app
|
||||||
|
restart: unless-stopped
|
||||||
|
expose:
|
||||||
|
- "8080"
|
||||||
|
environment:
|
||||||
|
# Gitea Configuration (required)
|
||||||
|
- GITEA_URL=${GITEA_URL}
|
||||||
|
- GITEA_TOKEN=${GITEA_TOKEN}
|
||||||
|
- GITEA_OWNER=${GITEA_OWNER}
|
||||||
|
# Optional Gitea config
|
||||||
|
- GITEA_REPO=${GITEA_REPO:-}
|
||||||
|
|
||||||
|
# HTTP Server Configuration
|
||||||
|
- HTTP_HOST=0.0.0.0
|
||||||
|
- HTTP_PORT=8080
|
||||||
|
|
||||||
|
# Authentication (optional - for MCP endpoint)
|
||||||
|
- AUTH_TOKEN=${AUTH_TOKEN:-}
|
||||||
|
- MCP_AUTH_MODE=${MCP_AUTH_MODE:-optional}
|
||||||
|
|
||||||
|
# Tool Filtering (optional)
|
||||||
|
- ENABLED_TOOLS=${ENABLED_TOOLS:-}
|
||||||
|
- DISABLED_TOOLS=${DISABLED_TOOLS:-}
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- gitea-mcp-network
|
||||||
|
|
||||||
|
# Caddy Reverse Proxy
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
container_name: gitea-mcp-remote-caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
|
depends_on:
|
||||||
|
app:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- gitea-mcp-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
gitea-mcp-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
caddy_data:
|
||||||
|
caddy_config:
|
||||||
244
docs/sprint-proposals/SPRINT-01-SUMMARY.md
Normal file
244
docs/sprint-proposals/SPRINT-01-SUMMARY.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# Sprint 01: Core Architecture Correction - SUMMARY
|
||||||
|
|
||||||
|
**Status:** 🟡 AWAITING APPROVAL
|
||||||
|
**Milestone:** [Sprint 01: Core Architecture Correction](https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/milestone/29)
|
||||||
|
**Sprint Duration:** 1 week (Feb 3-10, 2026)
|
||||||
|
**Total Estimated Effort:** 19-28 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sprint Overview
|
||||||
|
|
||||||
|
This sprint addresses **three fatal architectural problems** introduced in the v1.0.0 release. This is **surgical correction work**, not a rewrite - supporting modules (config, middleware, filtering, tests) are solid and only need import path updates.
|
||||||
|
|
||||||
|
### The Three Fatal Problems
|
||||||
|
|
||||||
|
1. **Subprocess Architecture → Direct Python Import**
|
||||||
|
- Current: Spawns gitea-mcp-server as subprocess
|
||||||
|
- Required: Direct Python import from marketplace package
|
||||||
|
|
||||||
|
2. **Custom REST API → MCP Streamable HTTP Protocol**
|
||||||
|
- Current: Custom endpoints `/tools/list` and `/tools/call`
|
||||||
|
- Required: MCP protocol `POST /mcp` with JSON-RPC 2.0
|
||||||
|
|
||||||
|
3. **Missing Marketplace Dependency**
|
||||||
|
- Current: Comment about installing separately
|
||||||
|
- Required: Actual pip dependency from marketplace Git repo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issues Created
|
||||||
|
|
||||||
|
All issues are in Gitea milestone: **Sprint 01: Core Architecture Correction**
|
||||||
|
|
||||||
|
| Issue | Title | Type | Size | Est. Time | Dependencies |
|
||||||
|
|-------|-------|------|------|-----------|--------------|
|
||||||
|
| [#19](https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/issues/19) | Rename package to gitea_mcp_remote and update configuration | Refactor | M | 2-3h | None |
|
||||||
|
| [#20](https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/issues/20) | Update middleware and filtering with new import paths | Refactor | S | 1h | #19 |
|
||||||
|
| [#21](https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/issues/21) | Move tests to repository root and update imports | Refactor | M | 1-2h | #19, #20 |
|
||||||
|
| [#22](https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/issues/22) | Add marketplace dependency and update project config | Build | S | 1h | #19 |
|
||||||
|
| [#23](https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/issues/23) | Remove old server and create MCP base server structure | Feature | M | 2-3h | #19, #20, #22 |
|
||||||
|
| [#24](https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/issues/24) | Implement MCP Streamable HTTP protocol endpoints | Feature | M | 2-3h | #23 |
|
||||||
|
| [#25](https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/issues/25) | Create Docker multi-service infrastructure with Caddy | Build | M | 3-4h | #22, #24 |
|
||||||
|
| [#26](https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/issues/26) | Create startup scripts and MCP server tests | Test | M | 2-3h | #24 |
|
||||||
|
| [#27](https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/issues/27) | Create CLAUDE.md and update deployment documentation | Docs | M | 2-3h | All |
|
||||||
|
| [#28](https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/issues/28) | Final validation and integration testing | Test | M | 2-3h | All |
|
||||||
|
|
||||||
|
**Total Issues:** 10 (was 9, split large task into 2 medium tasks)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Order
|
||||||
|
|
||||||
|
The dependency graph ensures proper execution order:
|
||||||
|
|
||||||
|
```
|
||||||
|
#19 (Rename + Config) ← FOUNDATION
|
||||||
|
├─→ #20 (Middleware + Filtering)
|
||||||
|
│ └─→ #21 (Tests)
|
||||||
|
│
|
||||||
|
├─→ #22 (pyproject.toml)
|
||||||
|
│ ├─→ #23 (MCP Base Server)
|
||||||
|
│ │ ├─→ #24 (MCP Protocol)
|
||||||
|
│ │ │ ├─→ #25 (Docker)
|
||||||
|
│ │ │ └─→ #26 (Scripts + Tests)
|
||||||
|
│ │ │
|
||||||
|
│ └─→ #21 (Tests - can run parallel)
|
||||||
|
│
|
||||||
|
└─→ All above
|
||||||
|
└─→ #27 (Documentation)
|
||||||
|
└─→ #28 (Final Validation)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended sequence:**
|
||||||
|
1. #19 → #20 → #22 → #21 (Foundation - Day 1-2)
|
||||||
|
2. #23 → #24 (Core server - Day 2-3)
|
||||||
|
3. #25 → #26 (Infrastructure - Day 3-4)
|
||||||
|
4. #27 → #28 (Documentation and validation - Day 4-5)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to KEEP (Rename Imports Only)
|
||||||
|
|
||||||
|
These modules are **well-tested and solid**:
|
||||||
|
|
||||||
|
- ✅ `config/settings.py` - Minor field changes only
|
||||||
|
- ✅ `middleware/auth.py` - Import paths only
|
||||||
|
- ✅ `filtering/filter.py` - Change ValueError to warning
|
||||||
|
- ✅ All tests - Move to root, update imports
|
||||||
|
- ✅ `DEPLOYMENT.md` - Update references
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to REPLACE
|
||||||
|
|
||||||
|
- ❌ `server.py` → ✅ `server_http.py` (new MCP implementation)
|
||||||
|
- ❌ `pyproject.toml` → ✅ Updated with marketplace dependency
|
||||||
|
- ❌ `docker-compose.yml` → ✅ `docker/docker-compose.yml` (two services)
|
||||||
|
- ❌ `Dockerfile` → ✅ `docker/Dockerfile` (git + port 8080)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New Files to CREATE
|
||||||
|
|
||||||
|
- 📄 `docker/Caddyfile` - Reverse proxy config
|
||||||
|
- 📄 `CLAUDE.md` - Project guidance for Claude Code
|
||||||
|
- 📄 `tests/test_server_http.py` - MCP server tests
|
||||||
|
- 📄 `scripts/start.sh` - Production startup
|
||||||
|
- 📄 `scripts/healthcheck.sh` - Docker healthcheck
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria (16 Validations)
|
||||||
|
|
||||||
|
### Package Structure (3)
|
||||||
|
- [ ] `src/gitea_mcp_remote/` exists (not `gitea_http_wrapper`)
|
||||||
|
- [ ] No imports reference `gitea_http_wrapper`
|
||||||
|
- [ ] `tests/` is at repository root (not in `src/`)
|
||||||
|
|
||||||
|
### Configuration (3)
|
||||||
|
- [ ] `config/settings.py` has `mcp_auth_mode` field
|
||||||
|
- [ ] `config/settings.py` has `gitea_repo: str | None`
|
||||||
|
- [ ] HTTP defaults are `0.0.0.0:8080`
|
||||||
|
|
||||||
|
### Server Implementation (4)
|
||||||
|
- [ ] `server_http.py` imports from `mcp_server` package
|
||||||
|
- [ ] MCP endpoints exist: `POST /mcp`, `HEAD /mcp`
|
||||||
|
- [ ] Health endpoints exist: `/health`, `/healthz`, `/ping`
|
||||||
|
- [ ] No subprocess spawning code
|
||||||
|
|
||||||
|
### Dependencies (3)
|
||||||
|
- [ ] `pyproject.toml` has marketplace Git dependency
|
||||||
|
- [ ] Entry point is `gitea-mcp-remote` (not `gitea-http-wrapper`)
|
||||||
|
- [ ] Can run: `pip install -e .` successfully
|
||||||
|
|
||||||
|
### Docker (3)
|
||||||
|
- [ ] `docker/docker-compose.yml` has two services (app + caddy)
|
||||||
|
- [ ] `docker/Dockerfile` installs git and uses port 8080
|
||||||
|
- [ ] `docker/Caddyfile` exists and proxies to app:8080
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
|
||||||
|
### Effort Distribution
|
||||||
|
- **Small (1-2h):** 2 issues (#20, #22) = 2-4 hours
|
||||||
|
- **Medium (2-4h):** 8 issues (#19, #21, #23-28) = 17-24 hours
|
||||||
|
- **Total:** 19-28 hours ≈ 23.5 hours average
|
||||||
|
|
||||||
|
### Sprint Schedule (1 week)
|
||||||
|
- **Day 1-2:** Foundation (Issues #19-22) - 5-7 hours
|
||||||
|
- **Day 2-3:** Core Server (Issues #23-24) - 4-6 hours
|
||||||
|
- **Day 3-4:** Infrastructure (Issues #25-26) - 5-7 hours
|
||||||
|
- **Day 4-5:** Docs & Validation (Issues #27-28) - 4-6 hours
|
||||||
|
- **Buffer:** 1-2 hours for unexpected issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
### Low Risk ✅
|
||||||
|
- Config, middleware, filtering: Well-tested, only import changes
|
||||||
|
- Test relocation: No logic changes
|
||||||
|
|
||||||
|
### Medium Risk ⚠️
|
||||||
|
- `server_http.py`: New file, but following MCP HTTP spec
|
||||||
|
- MCP protocol integration: Well-documented standard
|
||||||
|
|
||||||
|
### High Risk 🔴
|
||||||
|
- Docker multi-service: Requires Caddy configuration
|
||||||
|
- Marketplace Git dependency: Must be accessible during build
|
||||||
|
|
||||||
|
### Mitigation
|
||||||
|
1. Execute in exact dependency order
|
||||||
|
2. Test at each major milestone
|
||||||
|
3. Validate Docker build before deployment
|
||||||
|
4. Keep development branch for rollback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Created
|
||||||
|
|
||||||
|
1. **[sprint-01-core-architecture-correction.md](./sprint-01-core-architecture-correction.md)**
|
||||||
|
- Executive summary
|
||||||
|
- Three fatal problems explained
|
||||||
|
- What to keep vs replace
|
||||||
|
- Architecture diagram
|
||||||
|
- Risk assessment
|
||||||
|
|
||||||
|
2. **[sprint-01-implementation-guide.md](./sprint-01-implementation-guide.md)**
|
||||||
|
- Step-by-step technical implementation
|
||||||
|
- Code snippets for each change
|
||||||
|
- Validation commands
|
||||||
|
- Complete file replacements
|
||||||
|
|
||||||
|
3. **[sprint-01-issue-breakdown.md](./sprint-01-issue-breakdown.md)**
|
||||||
|
- Detailed issue descriptions
|
||||||
|
- Dependency graph
|
||||||
|
- Execution order
|
||||||
|
- Size distribution
|
||||||
|
|
||||||
|
4. **[SPRINT-01-SUMMARY.md](./SPRINT-01-SUMMARY.md)** (this file)
|
||||||
|
- Sprint overview
|
||||||
|
- Issue table with links
|
||||||
|
- Success criteria
|
||||||
|
- Approval checklist
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- **Milestone:** https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote/milestone/29
|
||||||
|
- **Repository:** https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote
|
||||||
|
- **Branch:** development
|
||||||
|
- **Marketplace:** https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Approval Checklist
|
||||||
|
|
||||||
|
Before execution begins, verify:
|
||||||
|
|
||||||
|
- [ ] All 10 issues created and assigned to milestone
|
||||||
|
- [ ] Dependencies correctly set between issues
|
||||||
|
- [ ] Labels applied correctly (Type, Priority, Component, Size)
|
||||||
|
- [ ] Implementation guide reviewed and accurate
|
||||||
|
- [ ] Timeline is realistic (1 week)
|
||||||
|
- [ ] Success criteria are clear and testable
|
||||||
|
- [ ] Rollback plan understood (development branch)
|
||||||
|
- [ ] User has reviewed and approved the plan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
**AWAITING USER APPROVAL** to begin execution.
|
||||||
|
|
||||||
|
Once approved:
|
||||||
|
1. Start with Issue #19 (Foundation)
|
||||||
|
2. Follow dependency order strictly
|
||||||
|
3. Update issue status as work progresses
|
||||||
|
4. Run validation after each major milestone
|
||||||
|
5. Complete sprint with Issue #28 (Final Validation)
|
||||||
|
|
||||||
|
**Note:** This is attempt #3. User emphasized paying close attention to details. All requirements from the architectural correction prompt have been captured in the issue breakdown.
|
||||||
329
docs/sprint-proposals/sprint-01-core-architecture-correction.md
Normal file
329
docs/sprint-proposals/sprint-01-core-architecture-correction.md
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
# Sprint 01: Core Architecture Correction
|
||||||
|
|
||||||
|
**Status:** Planning
|
||||||
|
**Sprint Duration:** 1 week (estimated 20-24 hours of work)
|
||||||
|
**Priority:** CRITICAL - Architectural Foundation
|
||||||
|
**Attempt:** #3 (Pay close attention to details)
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This sprint addresses three fatal architectural problems introduced in the v1.0.0 release that prevent the HTTP wrapper from functioning correctly with the MCP protocol. This is **surgical correction work**, not a rewrite. Supporting modules (config, middleware, filtering, tests) are solid and only need import path updates.
|
||||||
|
|
||||||
|
## The Three Fatal Problems
|
||||||
|
|
||||||
|
### 1. Subprocess Architecture → Direct Python Import
|
||||||
|
**Current (Wrong):** `server.py` spawns `gitea-mcp-server` as a subprocess
|
||||||
|
**Required (Correct):** Direct Python import from marketplace package
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG (current)
|
||||||
|
self.process = await asyncio.create_subprocess_exec("gitea-mcp-server", ...)
|
||||||
|
|
||||||
|
# CORRECT (target)
|
||||||
|
from mcp_server import get_tool_definitions, create_tool_dispatcher, GiteaClient, GiteaConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is fatal:** Cannot access marketplace code as subprocess, breaks MCP protocol contract.
|
||||||
|
|
||||||
|
### 2. Custom REST API → MCP Streamable HTTP Protocol
|
||||||
|
**Current (Wrong):** Custom endpoints `/tools/list` and `/tools/call`
|
||||||
|
**Required (Correct):** MCP Streamable HTTP protocol
|
||||||
|
|
||||||
|
```python
|
||||||
|
# WRONG (current)
|
||||||
|
POST /tools/list
|
||||||
|
POST /tools/call
|
||||||
|
|
||||||
|
# CORRECT (target)
|
||||||
|
POST /mcp # JSON-RPC 2.0 messages
|
||||||
|
HEAD /mcp # Protocol version header
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is fatal:** Not compatible with Claude Desktop's MCP client implementation.
|
||||||
|
|
||||||
|
### 3. Missing Marketplace Dependency
|
||||||
|
**Current (Wrong):** Comment in pyproject.toml about installing separately
|
||||||
|
**Required (Correct):** Actual pip dependency from marketplace Git repository
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# WRONG (current)
|
||||||
|
# gitea-mcp-server - installed separately (not on PyPI yet)
|
||||||
|
|
||||||
|
# CORRECT (target)
|
||||||
|
dependencies = [
|
||||||
|
"gitea-mcp-server @ git+https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git#subdirectory=mcp-servers/gitea",
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is fatal:** Dependency not installable, breaks Docker builds and deployment.
|
||||||
|
|
||||||
|
## What to KEEP (Rename Imports Only)
|
||||||
|
|
||||||
|
These modules are **solid and well-tested**. Only update import paths from `gitea_http_wrapper` to `gitea_mcp_remote`:
|
||||||
|
|
||||||
|
### config/settings.py
|
||||||
|
- **Keep:** Overall structure, Pydantic settings, validation logic
|
||||||
|
- **Minor changes:**
|
||||||
|
- Make `gitea_repo` optional (allow None)
|
||||||
|
- Add `mcp_auth_mode: str = "optional"` field
|
||||||
|
- Change HTTP defaults: `http_host="0.0.0.0"`, `http_port=8080`
|
||||||
|
- Remove `get_gitea_mcp_env()` method (no longer needed for subprocess)
|
||||||
|
|
||||||
|
### middleware/auth.py
|
||||||
|
- **Keep:** Entire file logic unchanged
|
||||||
|
- **Change:** Import paths only (`gitea_http_wrapper` → `gitea_mcp_remote`)
|
||||||
|
|
||||||
|
### filtering/filter.py
|
||||||
|
- **Keep:** Entire filtering logic
|
||||||
|
- **Changes:**
|
||||||
|
- Line 30: Change `raise ValueError(...)` to `logger.warning(...)` (non-fatal)
|
||||||
|
- Import paths: `gitea_http_wrapper` → `gitea_mcp_remote`
|
||||||
|
|
||||||
|
### Tests (all files)
|
||||||
|
- **Keep:** All test logic and fixtures
|
||||||
|
- **Move:** `src/gitea_http_wrapper/tests/` → `tests/` (top-level)
|
||||||
|
- **Change:** Import paths to reflect new structure
|
||||||
|
|
||||||
|
### DEPLOYMENT.md
|
||||||
|
- **Keep:** Overall deployment guide structure
|
||||||
|
- **Update:** References to new MCP endpoints, Docker structure, marketplace dependency
|
||||||
|
|
||||||
|
## What to REPLACE
|
||||||
|
|
||||||
|
### server.py → server_http.py
|
||||||
|
**Complete replacement** with:
|
||||||
|
- Direct Python imports from marketplace `mcp_server`
|
||||||
|
- MCP Streamable HTTP transport (`POST /mcp`, `HEAD /mcp`)
|
||||||
|
- JSON-RPC 2.0 message handling
|
||||||
|
- GiteaClient instantiation with GiteaConfig
|
||||||
|
- Tool dispatcher integration
|
||||||
|
- Keep health endpoints: `/health`, `/healthz`, `/ping`
|
||||||
|
|
||||||
|
### pyproject.toml
|
||||||
|
**Full replacement** with:
|
||||||
|
- Marketplace Git dependency
|
||||||
|
- Updated package name: `gitea-mcp-remote`
|
||||||
|
- New entry point: `gitea-mcp-remote = "gitea_mcp_remote.server_http:main"`
|
||||||
|
- Updated test paths: `testpaths = ["tests"]`
|
||||||
|
|
||||||
|
### docker-compose.yml → docker/docker-compose.yml
|
||||||
|
**Move and restructure** with:
|
||||||
|
- Two services: `app` (Python server) and `caddy` (reverse proxy)
|
||||||
|
- App listens on port 8080 (internal)
|
||||||
|
- Caddy exposes port 443 (external HTTPS)
|
||||||
|
- Volume for Caddy certs persistence
|
||||||
|
|
||||||
|
### Dockerfile → docker/Dockerfile
|
||||||
|
**Replace** with:
|
||||||
|
- Install `git` package (for Git dependency install)
|
||||||
|
- Expose port 8080 (not 8000)
|
||||||
|
- Use `curl` for healthcheck (not wget)
|
||||||
|
- Install from `requirements.txt` first, then marketplace dependency
|
||||||
|
|
||||||
|
## New Files to CREATE
|
||||||
|
|
||||||
|
### docker/Caddyfile
|
||||||
|
Reverse proxy configuration:
|
||||||
|
- HTTPS termination
|
||||||
|
- Proxy to app:8080
|
||||||
|
- MCP endpoint routing
|
||||||
|
|
||||||
|
### CLAUDE.md
|
||||||
|
Project guidance for Claude Code:
|
||||||
|
- Architecture explanation
|
||||||
|
- Development workflows
|
||||||
|
- Deployment procedures
|
||||||
|
- MCP protocol notes
|
||||||
|
|
||||||
|
### scripts/start.sh
|
||||||
|
Production startup script:
|
||||||
|
- Environment validation
|
||||||
|
- Graceful startup
|
||||||
|
- Logging configuration
|
||||||
|
|
||||||
|
### scripts/healthcheck.sh
|
||||||
|
Docker healthcheck script:
|
||||||
|
- Check `/health` endpoint
|
||||||
|
- Validate MCP endpoint
|
||||||
|
- Exit codes for Docker
|
||||||
|
|
||||||
|
### tests/test_server_http.py
|
||||||
|
New test file for HTTP server:
|
||||||
|
- MCP endpoint tests
|
||||||
|
- JSON-RPC 2.0 validation
|
||||||
|
- Protocol version tests
|
||||||
|
|
||||||
|
## Package Rename
|
||||||
|
|
||||||
|
**From:** `src/gitea_http_wrapper/`
|
||||||
|
**To:** `src/gitea_mcp_remote/`
|
||||||
|
|
||||||
|
All imports throughout codebase must be updated:
|
||||||
|
```python
|
||||||
|
# OLD
|
||||||
|
from gitea_http_wrapper.config import GiteaSettings
|
||||||
|
|
||||||
|
# NEW
|
||||||
|
from gitea_mcp_remote.config import GiteaSettings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Execution Order (18 Steps)
|
||||||
|
|
||||||
|
This is the **exact sequence** that must be followed:
|
||||||
|
|
||||||
|
1. Rename package directory: `gitea_http_wrapper` → `gitea_mcp_remote`
|
||||||
|
2. Update `config/settings.py` (fields + imports)
|
||||||
|
3. Update `middleware/auth.py` (imports only)
|
||||||
|
4. Update `filtering/filter.py` (warning + imports)
|
||||||
|
5. Move tests: `src/gitea_mcp_remote/tests/` → `tests/`
|
||||||
|
6. Update all test imports
|
||||||
|
7. Delete old `server.py`
|
||||||
|
8. Create new `server_http.py` with MCP protocol
|
||||||
|
9. Replace `pyproject.toml` with marketplace dependency
|
||||||
|
10. Update `pytest.ini` test paths
|
||||||
|
11. Create `docker/` directory
|
||||||
|
12. Move and update `docker-compose.yml` → `docker/docker-compose.yml`
|
||||||
|
13. Replace `Dockerfile` → `docker/Dockerfile`
|
||||||
|
14. Create `docker/Caddyfile`
|
||||||
|
15. Create `scripts/start.sh` and `scripts/healthcheck.sh`
|
||||||
|
16. Create `tests/test_server_http.py`
|
||||||
|
17. Create `CLAUDE.md`
|
||||||
|
18. Update `DEPLOYMENT.md` references
|
||||||
|
|
||||||
|
## Validation Checklist (16 Items)
|
||||||
|
|
||||||
|
After implementation, ALL must pass:
|
||||||
|
|
||||||
|
### Package Structure
|
||||||
|
- [ ] `src/gitea_mcp_remote/` exists (not `gitea_http_wrapper`)
|
||||||
|
- [ ] No imports reference `gitea_http_wrapper`
|
||||||
|
- [ ] `tests/` is at repository root (not in `src/`)
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- [ ] `config/settings.py` has `mcp_auth_mode` field
|
||||||
|
- [ ] `config/settings.py` has `gitea_repo: str | None`
|
||||||
|
- [ ] HTTP defaults are `0.0.0.0:8080`
|
||||||
|
|
||||||
|
### Server Implementation
|
||||||
|
- [ ] `server_http.py` imports from `mcp_server` package
|
||||||
|
- [ ] MCP endpoints exist: `POST /mcp`, `HEAD /mcp`
|
||||||
|
- [ ] Health endpoints exist: `/health`, `/healthz`, `/ping`
|
||||||
|
- [ ] No subprocess spawning code
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- [ ] `pyproject.toml` has marketplace Git dependency
|
||||||
|
- [ ] Entry point is `gitea-mcp-remote` (not `gitea-http-wrapper`)
|
||||||
|
- [ ] Can run: `pip install -e .` successfully
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
- [ ] `docker/docker-compose.yml` has two services (app + caddy)
|
||||||
|
- [ ] `docker/Dockerfile` installs git and uses port 8080
|
||||||
|
- [ ] `docker/Caddyfile` exists and proxies to app:8080
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- [ ] All tests pass: `pytest tests/`
|
||||||
|
|
||||||
|
## Architecture Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Claude Desktop (MCP Client) │
|
||||||
|
└───────────────────────┬─────────────────────────────────────┘
|
||||||
|
│ JSON-RPC 2.0 over HTTP
|
||||||
|
│ POST /mcp
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Caddy (HTTPS Termination) │
|
||||||
|
│ - TLS/SSL │
|
||||||
|
│ - Reverse proxy to :8080 │
|
||||||
|
└───────────────────────┬─────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ server_http.py (MCP HTTP Transport) │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Starlette App │ │
|
||||||
|
│ │ - POST /mcp (JSON-RPC handler) │ │
|
||||||
|
│ │ - HEAD /mcp (protocol version) │ │
|
||||||
|
│ │ - /health endpoints │ │
|
||||||
|
│ └────────────────────┬────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ↓ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Middleware Stack │ │
|
||||||
|
│ │ - BearerAuthMiddleware (auth.py) ✓ Keep │ │
|
||||||
|
│ │ - HealthCheckBypassMiddleware ✓ Keep │ │
|
||||||
|
│ └────────────────────┬────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ↓ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Tool Dispatcher │ │
|
||||||
|
│ │ - create_tool_dispatcher() from mcp_server │ │
|
||||||
|
│ │ - Tool filtering (filter.py) ✓ Keep │ │
|
||||||
|
│ └────────────────────┬────────────────────────────────────┘ │
|
||||||
|
└──────────────────────┼──────────────────────────────────────┘
|
||||||
|
│ Direct Python calls
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Marketplace: mcp_server (gitea-mcp-server) │
|
||||||
|
│ - GiteaClient │
|
||||||
|
│ - GiteaConfig │
|
||||||
|
│ - get_tool_definitions() │
|
||||||
|
│ - create_tool_dispatcher() │
|
||||||
|
└───────────────────────┬─────────────────────────────────────┘
|
||||||
|
│ HTTPS API calls
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Gitea Instance (gitea.hotserv.cloud) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
### Low Risk (Supporting Modules)
|
||||||
|
- Config, middleware, filtering: Well-tested, only import changes
|
||||||
|
- Tests: Moving location, no logic changes
|
||||||
|
|
||||||
|
### Medium Risk (New Server Implementation)
|
||||||
|
- `server_http.py`: New file, but following MCP HTTP spec closely
|
||||||
|
- MCP protocol integration: Well-documented standard
|
||||||
|
|
||||||
|
### High Risk (Deployment Changes)
|
||||||
|
- Docker multi-service setup: Requires Caddy configuration
|
||||||
|
- Marketplace Git dependency: Must be accessible during build
|
||||||
|
|
||||||
|
### Mitigation Strategy
|
||||||
|
1. Execute in exact order (dependencies first, server last)
|
||||||
|
2. Test at each major milestone (config → middleware → server)
|
||||||
|
3. Validate Docker build before final deployment
|
||||||
|
4. Keep development branch for rollback if needed
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
1. ✅ All 16 validation items pass
|
||||||
|
2. ✅ Can install via `pip install -e .`
|
||||||
|
3. ✅ Can build Docker image successfully
|
||||||
|
4. ✅ Can start via `docker-compose up`
|
||||||
|
5. ✅ MCP endpoint responds to `POST /mcp` with protocol version
|
||||||
|
6. ✅ Claude Desktop can connect and list tools
|
||||||
|
7. ✅ Can create Gitea issue via MCP protocol
|
||||||
|
8. ✅ All tests pass
|
||||||
|
|
||||||
|
## Timeline Estimate
|
||||||
|
|
||||||
|
- **Setup & Config Changes:** 2-3 hours
|
||||||
|
- **Server Rewrite:** 4-6 hours
|
||||||
|
- **Docker Restructure:** 3-4 hours
|
||||||
|
- **Testing & Validation:** 4-5 hours
|
||||||
|
- **Documentation:** 2-3 hours
|
||||||
|
- **Buffer for Issues:** 4-5 hours
|
||||||
|
|
||||||
|
**Total:** 19-26 hours → 1 week sprint
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- MCP Streamable HTTP Spec: https://spec.modelcontextprotocol.io/specification/basic/transports/
|
||||||
|
- JSON-RPC 2.0 Spec: https://www.jsonrpc.org/specification
|
||||||
|
- Marketplace Repository: https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace
|
||||||
|
- Original Issue: (To be created in this sprint)
|
||||||
1454
docs/sprint-proposals/sprint-01-implementation-guide.md
Normal file
1454
docs/sprint-proposals/sprint-01-implementation-guide.md
Normal file
File diff suppressed because it is too large
Load Diff
489
docs/sprint-proposals/sprint-01-issue-breakdown.md
Normal file
489
docs/sprint-proposals/sprint-01-issue-breakdown.md
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
# Sprint 01: Issue Breakdown
|
||||||
|
|
||||||
|
## Issue Structure
|
||||||
|
|
||||||
|
Each issue is sized for 1-4 hours of work and includes:
|
||||||
|
- Clear acceptance criteria
|
||||||
|
- Dependencies on other issues
|
||||||
|
- Reference to implementation guide
|
||||||
|
- Appropriate labels
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue #1: Rename Package Directory and Update Config
|
||||||
|
|
||||||
|
**Title:** `[Sprint 01] refactor: Rename package to gitea_mcp_remote and update configuration`
|
||||||
|
|
||||||
|
**Estimated Time:** 2-3 hours
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
- `Type/Refactor`
|
||||||
|
- `Priority/High`
|
||||||
|
- `Component/Core`
|
||||||
|
- `Size/M`
|
||||||
|
|
||||||
|
**Dependencies:** None
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
Rename the package directory from `gitea_http_wrapper` to `gitea_mcp_remote` and update the configuration module with new fields required for MCP protocol.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Rename `src/gitea_http_wrapper/` to `src/gitea_mcp_remote/`
|
||||||
|
- [ ] Update `config/settings.py`:
|
||||||
|
- Make `gitea_repo` optional (allow None)
|
||||||
|
- Add `mcp_auth_mode: str = "optional"` field
|
||||||
|
- Change HTTP defaults: `http_host="0.0.0.0"`, `http_port=8080`
|
||||||
|
- Remove `get_gitea_mcp_env()` method
|
||||||
|
- [ ] Update `config/__init__.py` imports
|
||||||
|
- [ ] Verify imports work: `from gitea_mcp_remote.config import GiteaSettings`
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- Package directory is `src/gitea_mcp_remote/`
|
||||||
|
- Config has `mcp_auth_mode` field
|
||||||
|
- Config has optional `gitea_repo` field
|
||||||
|
- HTTP defaults are `0.0.0.0:8080`
|
||||||
|
- Can import: `from gitea_mcp_remote.config import GiteaSettings`
|
||||||
|
|
||||||
|
**Implementation Reference:**
|
||||||
|
See `docs/sprint-proposals/sprint-01-implementation-guide.md` - Phase 1, Issues #1-2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue #2: Update Middleware and Filtering Modules
|
||||||
|
|
||||||
|
**Title:** `[Sprint 01] refactor: Update middleware and filtering with new import paths`
|
||||||
|
|
||||||
|
**Estimated Time:** 1 hour
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
- `Type/Refactor`
|
||||||
|
- `Priority/High`
|
||||||
|
- `Component/Core`
|
||||||
|
- `Size/S`
|
||||||
|
|
||||||
|
**Dependencies:** Issue #1
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
Update middleware and filtering modules to use new package name. Middleware requires only import changes, filtering changes ValueError to warning.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Update `middleware/__init__.py` imports
|
||||||
|
- [ ] Update `middleware/auth.py` - imports only
|
||||||
|
- [ ] Update `filtering/__init__.py` imports
|
||||||
|
- [ ] Update `filtering/filter.py`:
|
||||||
|
- Add logging import
|
||||||
|
- Change line 29-32 ValueError to logger.warning
|
||||||
|
- [ ] Verify imports work
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- Middleware imports from `gitea_mcp_remote.middleware`
|
||||||
|
- Filtering imports from `gitea_mcp_remote.filtering`
|
||||||
|
- ToolFilter logs warning instead of raising ValueError when both filter types specified
|
||||||
|
- Can import both modules successfully
|
||||||
|
|
||||||
|
**Implementation Reference:**
|
||||||
|
See `docs/sprint-proposals/sprint-01-implementation-guide.md` - Phase 2, Issues #3-4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue #3: Relocate Tests and Update Imports
|
||||||
|
|
||||||
|
**Title:** `[Sprint 01] refactor: Move tests to repository root and update imports`
|
||||||
|
|
||||||
|
**Estimated Time:** 1-2 hours
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
- `Type/Refactor`
|
||||||
|
- `Priority/High`
|
||||||
|
- `Component/Tests`
|
||||||
|
- `Size/M`
|
||||||
|
|
||||||
|
**Dependencies:** Issue #1, Issue #2
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
Move test suite from `src/gitea_mcp_remote/tests/` to repository root `tests/` directory and update all test imports to use new package name.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Move `src/gitea_mcp_remote/tests/` to `tests/`
|
||||||
|
- [ ] Update imports in `tests/conftest.py`
|
||||||
|
- [ ] Update imports in `tests/test_config.py`
|
||||||
|
- [ ] Update imports in `tests/test_middleware.py`
|
||||||
|
- [ ] Update imports in `tests/test_filtering.py`
|
||||||
|
- [ ] Update `pytest.ini` to use `testpaths = tests`
|
||||||
|
- [ ] Run pytest and verify all tests pass
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- Tests located at repository root: `tests/`
|
||||||
|
- No tests in `src/gitea_mcp_remote/tests/`
|
||||||
|
- All test imports use `gitea_mcp_remote` package name
|
||||||
|
- All existing tests pass: `pytest tests/ -v`
|
||||||
|
- pytest.ini references `testpaths = tests`
|
||||||
|
|
||||||
|
**Implementation Reference:**
|
||||||
|
See `docs/sprint-proposals/sprint-01-implementation-guide.md` - Phase 3, Issues #5-6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue #4: Replace pyproject.toml with Marketplace Dependency
|
||||||
|
|
||||||
|
**Title:** `[Sprint 01] build: Add marketplace dependency and update project configuration`
|
||||||
|
|
||||||
|
**Estimated Time:** 1 hour
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
- `Type/Build`
|
||||||
|
- `Priority/Critical`
|
||||||
|
- `Component/Dependencies`
|
||||||
|
- `Size/S`
|
||||||
|
|
||||||
|
**Dependencies:** Issue #1
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
Replace pyproject.toml with new configuration including the marketplace Git dependency for gitea-mcp-server.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Update `pyproject.toml`:
|
||||||
|
- Add marketplace Git dependency
|
||||||
|
- Update package name to `gitea-mcp-remote`
|
||||||
|
- Change entry point to `gitea-mcp-remote`
|
||||||
|
- Update version to 1.1.0
|
||||||
|
- Update test paths to `testpaths = ["tests"]`
|
||||||
|
- [ ] Test installation: `pip install -e .`
|
||||||
|
- [ ] Verify marketplace dependency installs
|
||||||
|
- [ ] Verify entry point exists: `which gitea-mcp-remote`
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- pyproject.toml includes marketplace Git dependency
|
||||||
|
- Entry point is `gitea-mcp-remote` (not `gitea-http-wrapper`)
|
||||||
|
- Can run: `pip install -e .` successfully
|
||||||
|
- Marketplace dependency installs from Git repository
|
||||||
|
- Command `gitea-mcp-remote` is available
|
||||||
|
|
||||||
|
**Implementation Reference:**
|
||||||
|
See `docs/sprint-proposals/sprint-01-implementation-guide.md` - Phase 5, Issue #9
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue #5: Implement MCP HTTP Server
|
||||||
|
|
||||||
|
**Title:** `[Sprint 01] feat: Implement MCP Streamable HTTP protocol server`
|
||||||
|
|
||||||
|
**Estimated Time:** 4-6 hours
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
- `Type/Feature`
|
||||||
|
- `Priority/Critical`
|
||||||
|
- `Component/Core`
|
||||||
|
- `Size/L` → **BREAKDOWN REQUIRED**
|
||||||
|
|
||||||
|
**Dependencies:** Issue #1, Issue #2, Issue #4
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
**NOTE:** This is a Large (L) task that should be broken down into Medium (M) subtasks:
|
||||||
|
|
||||||
|
### Subtask 5.1: Remove Old Server and Create MCP Base Server (2-3 hours)
|
||||||
|
- Delete `src/gitea_mcp_remote/server.py`
|
||||||
|
- Create `src/gitea_mcp_remote/server_http.py` with:
|
||||||
|
- Imports from marketplace `mcp_server`
|
||||||
|
- GiteaMCPServer class with GiteaClient initialization
|
||||||
|
- Startup/shutdown handlers
|
||||||
|
- Basic route structure
|
||||||
|
|
||||||
|
### Subtask 5.2: Implement MCP Protocol Endpoints (2-3 hours)
|
||||||
|
- Add HEAD /mcp endpoint (protocol version)
|
||||||
|
- Add POST /mcp endpoint (JSON-RPC 2.0 handler)
|
||||||
|
- Implement MCP methods:
|
||||||
|
- `initialize`
|
||||||
|
- `tools/list`
|
||||||
|
- `tools/call`
|
||||||
|
- Add error handling for JSON-RPC
|
||||||
|
|
||||||
|
**Combined Tasks:**
|
||||||
|
- [ ] Delete old `server.py`
|
||||||
|
- [ ] Create new `server_http.py`
|
||||||
|
- [ ] Import from marketplace: `from mcp_server import ...`
|
||||||
|
- [ ] Implement GiteaMCPServer class
|
||||||
|
- [ ] Implement HEAD /mcp (protocol version)
|
||||||
|
- [ ] Implement POST /mcp (JSON-RPC handler)
|
||||||
|
- [ ] Implement initialize method
|
||||||
|
- [ ] Implement tools/list method with filtering
|
||||||
|
- [ ] Implement tools/call method with dispatcher
|
||||||
|
- [ ] Keep health endpoints: /health, /healthz, /ping
|
||||||
|
- [ ] Add JSON-RPC error handling
|
||||||
|
- [ ] Verify imports: `from gitea_mcp_remote.server_http import GiteaMCPServer`
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- Old `server.py` deleted
|
||||||
|
- New `server_http.py` exists
|
||||||
|
- Imports from marketplace `mcp_server` package
|
||||||
|
- MCP endpoints exist: `POST /mcp`, `HEAD /mcp`
|
||||||
|
- Health endpoints exist: `/health`, `/healthz`, `/ping`
|
||||||
|
- No subprocess spawning code
|
||||||
|
- Can import server module successfully
|
||||||
|
- JSON-RPC 2.0 request/response handling works
|
||||||
|
|
||||||
|
**Implementation Reference:**
|
||||||
|
See `docs/sprint-proposals/sprint-01-implementation-guide.md` - Phase 4, Issues #7-8
|
||||||
|
|
||||||
|
**Recommendation:** Create two separate issues (5.1 and 5.2) to keep within M size.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue #6: Create Docker Infrastructure
|
||||||
|
|
||||||
|
**Title:** `[Sprint 01] build: Create Docker multi-service infrastructure with Caddy`
|
||||||
|
|
||||||
|
**Estimated Time:** 3-4 hours
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
- `Type/Build`
|
||||||
|
- `Priority/High`
|
||||||
|
- `Component/Docker`
|
||||||
|
- `Size/M`
|
||||||
|
|
||||||
|
**Dependencies:** Issue #4, Issue #5
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
Create Docker infrastructure with two-service architecture: Python app and Caddy reverse proxy.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Create `docker/` directory
|
||||||
|
- [ ] Create `docker/docker-compose.yml` with two services (app + caddy)
|
||||||
|
- [ ] Create `docker/Dockerfile`:
|
||||||
|
- Install git package
|
||||||
|
- Expose port 8080
|
||||||
|
- Use curl for healthcheck
|
||||||
|
- Install marketplace dependency
|
||||||
|
- [ ] Create `docker/Caddyfile`:
|
||||||
|
- HTTPS termination
|
||||||
|
- Proxy to app:8080
|
||||||
|
- MCP endpoint routing
|
||||||
|
- [ ] Validate Dockerfile builds
|
||||||
|
- [ ] Validate docker-compose configuration
|
||||||
|
- [ ] Validate Caddyfile syntax
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- `docker/docker-compose.yml` has two services (app + caddy)
|
||||||
|
- `docker/Dockerfile` installs git and uses port 8080
|
||||||
|
- `docker/Caddyfile` exists and proxies to app:8080
|
||||||
|
- Can build: `docker build -f docker/Dockerfile -t test .`
|
||||||
|
- Can validate: `docker-compose -f docker/docker-compose.yml config`
|
||||||
|
- Caddy config validates successfully
|
||||||
|
|
||||||
|
**Implementation Reference:**
|
||||||
|
See `docs/sprint-proposals/sprint-01-implementation-guide.md` - Phase 6, Issues #11-14
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue #7: Create Utility Scripts and Server Tests
|
||||||
|
|
||||||
|
**Title:** `[Sprint 01] test: Create startup scripts and MCP server tests`
|
||||||
|
|
||||||
|
**Estimated Time:** 2-3 hours
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
- `Type/Test`
|
||||||
|
- `Priority/Medium`
|
||||||
|
- `Component/Tests`
|
||||||
|
- `Size/M`
|
||||||
|
|
||||||
|
**Dependencies:** Issue #5
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
Create production utility scripts and comprehensive tests for the new MCP HTTP server.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Create `scripts/start.sh` (production startup)
|
||||||
|
- [ ] Create `scripts/healthcheck.sh` (Docker healthcheck)
|
||||||
|
- [ ] Make scripts executable
|
||||||
|
- [ ] Create `tests/test_server_http.py`:
|
||||||
|
- Health endpoint tests
|
||||||
|
- MCP HEAD endpoint test (protocol version)
|
||||||
|
- MCP POST endpoint tests (initialize, tools/list, tools/call)
|
||||||
|
- JSON-RPC error handling tests
|
||||||
|
- Tool filtering integration test
|
||||||
|
- [ ] Run new tests and verify they pass
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- `scripts/start.sh` validates environment and starts server
|
||||||
|
- `scripts/healthcheck.sh` checks health endpoint
|
||||||
|
- Both scripts are executable
|
||||||
|
- `tests/test_server_http.py` exists with comprehensive coverage
|
||||||
|
- All new tests pass: `pytest tests/test_server_http.py -v`
|
||||||
|
- All existing tests still pass: `pytest tests/ -v`
|
||||||
|
|
||||||
|
**Implementation Reference:**
|
||||||
|
See `docs/sprint-proposals/sprint-01-implementation-guide.md` - Phase 7-8, Issues #15-16
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue #8: Create Documentation
|
||||||
|
|
||||||
|
**Title:** `[Sprint 01] docs: Create CLAUDE.md and update deployment documentation`
|
||||||
|
|
||||||
|
**Estimated Time:** 2-3 hours
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
- `Type/Documentation`
|
||||||
|
- `Priority/Medium`
|
||||||
|
- `Component/Documentation`
|
||||||
|
- `Size/M`
|
||||||
|
|
||||||
|
**Dependencies:** All previous issues
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
Create comprehensive project documentation for Claude Code and update deployment guide with new MCP protocol and Docker structure.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Create `CLAUDE.md`:
|
||||||
|
- Project overview
|
||||||
|
- Architecture diagram
|
||||||
|
- Development workflows
|
||||||
|
- MCP protocol notes
|
||||||
|
- Configuration reference
|
||||||
|
- Deployment instructions
|
||||||
|
- Troubleshooting guide
|
||||||
|
- [ ] Update `DEPLOYMENT.md`:
|
||||||
|
- Replace custom REST API refs with MCP protocol
|
||||||
|
- Update Docker structure (docker/ directory, two services)
|
||||||
|
- Update marketplace dependency installation
|
||||||
|
- Update Claude Desktop config example
|
||||||
|
- Add MCP protocol debugging section
|
||||||
|
- [ ] Verify documentation accuracy
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- `CLAUDE.md` exists with complete project guidance
|
||||||
|
- `DEPLOYMENT.md` updated with MCP protocol references
|
||||||
|
- No references to old `/tools/list` or `/tools/call` endpoints
|
||||||
|
- Docker paths reference `docker/docker-compose.yml`
|
||||||
|
- Claude Desktop config shows `/mcp` endpoint
|
||||||
|
- All code examples are accurate
|
||||||
|
|
||||||
|
**Implementation Reference:**
|
||||||
|
See `docs/sprint-proposals/sprint-01-implementation-guide.md` - Phase 9, Issues #17-18
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue #9: Final Validation and Integration Testing
|
||||||
|
|
||||||
|
**Title:** `[Sprint 01] test: Final validation and integration testing`
|
||||||
|
|
||||||
|
**Estimated Time:** 2-3 hours
|
||||||
|
|
||||||
|
**Labels:**
|
||||||
|
- `Type/Test`
|
||||||
|
- `Priority/Critical`
|
||||||
|
- `Component/Integration`
|
||||||
|
- `Size/M`
|
||||||
|
|
||||||
|
**Dependencies:** All previous issues
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
|
||||||
|
Run complete validation checklist to ensure all architectural corrections are in place and working correctly.
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] Verify package structure (no gitea_http_wrapper)
|
||||||
|
- [ ] Verify no old imports remain
|
||||||
|
- [ ] Verify config has all new fields
|
||||||
|
- [ ] Verify server has MCP endpoints
|
||||||
|
- [ ] Run: `pip install -e .` successfully
|
||||||
|
- [ ] Run: `pytest tests/ -v` - all tests pass
|
||||||
|
- [ ] Build Docker image successfully
|
||||||
|
- [ ] Validate docker-compose configuration
|
||||||
|
- [ ] Validate Caddyfile syntax
|
||||||
|
- [ ] Test MCP endpoint responds to protocol version request
|
||||||
|
- [ ] Test MCP endpoint handles JSON-RPC messages
|
||||||
|
- [ ] Document any issues found
|
||||||
|
- [ ] Create follow-up issues if needed
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
All 16 validation items pass:
|
||||||
|
|
||||||
|
**Package Structure:**
|
||||||
|
- [ ] `src/gitea_mcp_remote/` exists (not `gitea_http_wrapper`)
|
||||||
|
- [ ] No imports reference `gitea_http_wrapper`
|
||||||
|
- [ ] `tests/` is at repository root (not in `src/`)
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
- [ ] `config/settings.py` has `mcp_auth_mode` field
|
||||||
|
- [ ] `config/settings.py` has `gitea_repo: str | None`
|
||||||
|
- [ ] HTTP defaults are `0.0.0.0:8080`
|
||||||
|
|
||||||
|
**Server Implementation:**
|
||||||
|
- [ ] `server_http.py` imports from `mcp_server` package
|
||||||
|
- [ ] MCP endpoints exist: `POST /mcp`, `HEAD /mcp`
|
||||||
|
- [ ] Health endpoints exist: `/health`, `/healthz`, `/ping`
|
||||||
|
- [ ] No subprocess spawning code
|
||||||
|
|
||||||
|
**Dependencies:**
|
||||||
|
- [ ] `pyproject.toml` has marketplace Git dependency
|
||||||
|
- [ ] Entry point is `gitea-mcp-remote` (not `gitea-http-wrapper`)
|
||||||
|
- [ ] Can run: `pip install -e .` successfully
|
||||||
|
|
||||||
|
**Docker:**
|
||||||
|
- [ ] `docker/docker-compose.yml` has two services (app + caddy)
|
||||||
|
- [ ] `docker/Dockerfile` installs git and uses port 8080
|
||||||
|
- [ ] `docker/Caddyfile` exists and proxies to app:8080
|
||||||
|
|
||||||
|
**Tests:**
|
||||||
|
- [ ] All tests pass: `pytest tests/`
|
||||||
|
|
||||||
|
**Implementation Reference:**
|
||||||
|
See `docs/sprint-proposals/sprint-01-implementation-guide.md` - Final Validation section
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue Dependencies Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
Issue #1 (Rename + Config)
|
||||||
|
├─→ Issue #2 (Middleware + Filtering)
|
||||||
|
│ └─→ Issue #3 (Tests)
|
||||||
|
│
|
||||||
|
├─→ Issue #4 (pyproject.toml)
|
||||||
|
│ ├─→ Issue #5 (MCP Server)
|
||||||
|
│ │ ├─→ Issue #6 (Docker)
|
||||||
|
│ │ └─→ Issue #7 (Scripts + Tests)
|
||||||
|
│ │
|
||||||
|
│ └─→ Issue #3 (Tests)
|
||||||
|
│
|
||||||
|
└─→ All above
|
||||||
|
└─→ Issue #8 (Documentation)
|
||||||
|
└─→ Issue #9 (Final Validation)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Execution Order
|
||||||
|
|
||||||
|
1. Issue #1 - Rename + Config (Foundation)
|
||||||
|
2. Issue #2 - Middleware + Filtering (Supporting modules)
|
||||||
|
3. Issue #4 - pyproject.toml (Dependencies before server)
|
||||||
|
4. Issue #3 - Tests (Can run in parallel with #4)
|
||||||
|
5. Issue #5 - MCP Server (Core implementation) **Consider splitting into 5.1 and 5.2**
|
||||||
|
6. Issue #6 - Docker (Deployment infrastructure)
|
||||||
|
7. Issue #7 - Scripts + Tests (Validation tools)
|
||||||
|
8. Issue #8 - Documentation (After implementation complete)
|
||||||
|
9. Issue #9 - Final Validation (Sprint completion)
|
||||||
|
|
||||||
|
## Size Distribution
|
||||||
|
|
||||||
|
- **Small (1-2h):** Issues #2, #4 (2 issues)
|
||||||
|
- **Medium (2-4h):** Issues #1, #3, #6, #7, #8, #9 (6 issues)
|
||||||
|
- **Large (4-6h):** Issue #5 (1 issue - SHOULD BE SPLIT)
|
||||||
|
|
||||||
|
**Recommendation:** Split Issue #5 into two Medium issues for better tracking and clearer completion criteria.
|
||||||
|
|
||||||
|
## Total Estimated Time
|
||||||
|
|
||||||
|
- Minimum: 19 hours
|
||||||
|
- Maximum: 28 hours
|
||||||
|
- Average: 23.5 hours
|
||||||
|
- **Sprint Duration:** 1 week (5 working days)
|
||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "gitea-mcp-remote"
|
name = "gitea-mcp-remote"
|
||||||
version = "1.0.0"
|
version = "2.0.0"
|
||||||
description = "HTTP transport wrapper for Gitea MCP server"
|
description = "HTTP transport wrapper for Gitea MCP server"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@@ -24,14 +24,19 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
# THE MARKETPLACE PACKAGE — this is the whole point of this repo
|
||||||
|
"gitea-mcp-server @ git+https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git#subdirectory=mcp-servers/gitea",
|
||||||
|
# MCP SDK
|
||||||
"mcp>=0.9.0",
|
"mcp>=0.9.0",
|
||||||
"uvicorn>=0.27.0",
|
# HTTP server
|
||||||
|
"uvicorn>=0.30.0",
|
||||||
|
"starlette>=0.38.0",
|
||||||
|
# Config (already used by existing modules)
|
||||||
"pydantic>=2.0.0",
|
"pydantic>=2.0.0",
|
||||||
"pydantic-settings>=2.0.0",
|
"pydantic-settings>=2.0.0",
|
||||||
"python-dotenv>=1.0.0",
|
"python-dotenv>=1.0.0",
|
||||||
"starlette>=0.36.0",
|
# Auth
|
||||||
# gitea-mcp-server - installed separately (not on PyPI yet)
|
"pyjwt>=2.8.0",
|
||||||
# See: https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@@ -44,7 +49,7 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
gitea-http-wrapper = "gitea_http_wrapper.server:main"
|
gitea-mcp-remote = "gitea_mcp_remote.server_http:main"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/lmiranda/gitea-mcp-remote"
|
Homepage = "https://github.com/lmiranda/gitea-mcp-remote"
|
||||||
@@ -55,7 +60,7 @@ where = ["src"]
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
testpaths = ["src/gitea_http_wrapper/tests"]
|
testpaths = ["tests"]
|
||||||
python_files = ["test_*.py"]
|
python_files = ["test_*.py"]
|
||||||
python_classes = ["Test*"]
|
python_classes = ["Test*"]
|
||||||
python_functions = ["test_*"]
|
python_functions = ["test_*"]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
testpaths = src/gitea_http_wrapper/tests
|
testpaths = tests
|
||||||
python_files = test_*.py
|
python_files = test_*.py
|
||||||
python_classes = Test*
|
python_classes = Test*
|
||||||
python_functions = test_*
|
python_functions = test_*
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
# HTTP Transport Wrapper Dependencies
|
# THE MARKETPLACE PACKAGE — this is the whole point of this repo
|
||||||
|
gitea-mcp-server @ git+https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git#subdirectory=mcp-servers/gitea
|
||||||
|
|
||||||
|
# MCP SDK
|
||||||
mcp>=0.9.0
|
mcp>=0.9.0
|
||||||
uvicorn>=0.27.0
|
|
||||||
starlette>=0.36.0
|
# HTTP server
|
||||||
|
uvicorn>=0.30.0
|
||||||
|
starlette>=0.38.0
|
||||||
|
|
||||||
|
# Config (already used by existing modules)
|
||||||
pydantic>=2.0.0
|
pydantic>=2.0.0
|
||||||
pydantic-settings>=2.0.0
|
pydantic-settings>=2.0.0
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
|
|
||||||
# Official Gitea MCP Server (to be wrapped)
|
# Auth
|
||||||
# Install separately - not on PyPI yet
|
pyjwt>=2.8.0
|
||||||
# See: https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace
|
|
||||||
|
|||||||
21
scripts/healthcheck.sh
Executable file
21
scripts/healthcheck.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Health check script for Gitea MCP Remote
|
||||||
|
#
|
||||||
|
# Used by Docker healthcheck and monitoring systems.
|
||||||
|
# Returns exit code 0 if healthy, 1 if unhealthy.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
HOST="${HTTP_HOST:-localhost}"
|
||||||
|
PORT="${HTTP_PORT:-8080}"
|
||||||
|
ENDPOINT="http://${HOST}:${PORT}/health"
|
||||||
|
|
||||||
|
# Make request and check response
|
||||||
|
response=$(curl -sf "$ENDPOINT" 2>/dev/null) || exit 1
|
||||||
|
|
||||||
|
# Verify JSON response contains status: ok
|
||||||
|
if echo "$response" | grep -q '"status".*"ok"'; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
60
scripts/start.sh
Executable file
60
scripts/start.sh
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Production startup script for Gitea MCP Remote
|
||||||
|
#
|
||||||
|
# This script validates the environment and starts the server.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${GREEN}Starting Gitea MCP Remote...${NC}"
|
||||||
|
|
||||||
|
# Check required environment variables
|
||||||
|
check_required_env() {
|
||||||
|
local var_name=$1
|
||||||
|
if [ -z "${!var_name}" ]; then
|
||||||
|
echo -e "${RED}ERROR: Required environment variable $var_name is not set${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN} $var_name is set${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Checking required environment variables..."
|
||||||
|
MISSING=0
|
||||||
|
|
||||||
|
check_required_env "GITEA_URL" || MISSING=1
|
||||||
|
check_required_env "GITEA_TOKEN" || MISSING=1
|
||||||
|
check_required_env "GITEA_OWNER" || MISSING=1
|
||||||
|
|
||||||
|
if [ $MISSING -eq 1 ]; then
|
||||||
|
echo -e "${RED}Missing required environment variables. Exiting.${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Required variables:"
|
||||||
|
echo " GITEA_URL - Gitea server URL (e.g., https://gitea.example.com)"
|
||||||
|
echo " GITEA_TOKEN - Gitea API token"
|
||||||
|
echo " GITEA_OWNER - Default repository owner"
|
||||||
|
echo ""
|
||||||
|
echo "Optional variables:"
|
||||||
|
echo " GITEA_REPO - Default repository name"
|
||||||
|
echo " AUTH_TOKEN - Bearer token for MCP endpoint authentication"
|
||||||
|
echo " HTTP_HOST - Server host (default: 0.0.0.0)"
|
||||||
|
echo " HTTP_PORT - Server port (default: 8080)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show optional configuration
|
||||||
|
echo ""
|
||||||
|
echo "Optional configuration:"
|
||||||
|
[ -n "$GITEA_REPO" ] && echo -e " ${GREEN}GITEA_REPO: $GITEA_REPO${NC}" || echo -e " ${YELLOW}GITEA_REPO: not set (will use per-request)${NC}"
|
||||||
|
[ -n "$AUTH_TOKEN" ] && echo -e " ${GREEN}AUTH_TOKEN: (set)${NC}" || echo -e " ${YELLOW}AUTH_TOKEN: not set (no auth required)${NC}"
|
||||||
|
echo " HTTP_HOST: ${HTTP_HOST:-0.0.0.0}"
|
||||||
|
echo " HTTP_PORT: ${HTTP_PORT:-8080}"
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}Starting server...${NC}"
|
||||||
|
exec gitea-mcp-remote
|
||||||
11
src/gitea_mcp_remote/__init__.py
Normal file
11
src/gitea_mcp_remote/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Gitea MCP Remote — HTTP deployment wrapper for marketplace Gitea MCP server.
|
||||||
|
|
||||||
|
Imports tool definitions from gitea-mcp-server (marketplace) and serves them
|
||||||
|
over Streamable HTTP transport with authentication and TLS via Caddy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .server_http import create_app, main
|
||||||
|
|
||||||
|
__version__ = "0.2.0"
|
||||||
|
__all__ = ["__version__", "create_app", "main"]
|
||||||
5
src/gitea_mcp_remote/config/__init__.py
Normal file
5
src/gitea_mcp_remote/config/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Configuration module for Gitea MCP HTTP transport."""
|
||||||
|
|
||||||
|
from gitea_mcp_remote.config.settings import GiteaSettings, load_settings
|
||||||
|
|
||||||
|
__all__ = ["GiteaSettings", "load_settings"]
|
||||||
108
src/gitea_mcp_remote/config/settings.py
Normal file
108
src/gitea_mcp_remote/config/settings.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"""Configuration settings for Gitea MCP HTTP transport."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import Field, field_validator
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class GiteaSettings(BaseSettings):
|
||||||
|
"""Configuration settings loaded from environment or .env file."""
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
case_sensitive=False,
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Gitea Configuration
|
||||||
|
gitea_url: str = Field(
|
||||||
|
...,
|
||||||
|
description="Gitea instance URL (e.g., https://git.example.com)",
|
||||||
|
)
|
||||||
|
gitea_token: str = Field(
|
||||||
|
...,
|
||||||
|
description="Gitea API token for authentication",
|
||||||
|
)
|
||||||
|
gitea_owner: str = Field(
|
||||||
|
...,
|
||||||
|
description="Default repository owner/organization",
|
||||||
|
)
|
||||||
|
gitea_repo: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Default repository name (optional)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# HTTP Server Configuration
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Authentication Configuration
|
||||||
|
auth_token: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Bearer token for HTTP authentication (optional)",
|
||||||
|
)
|
||||||
|
mcp_auth_mode: str = Field(
|
||||||
|
default="optional",
|
||||||
|
description="MCP authentication mode: 'required', 'optional', or 'none'",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tool Filtering Configuration
|
||||||
|
enabled_tools: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Comma-separated list of enabled tools (optional, enables all if not set)",
|
||||||
|
)
|
||||||
|
disabled_tools: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Comma-separated list of disabled tools (optional)",
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator("gitea_url")
|
||||||
|
@classmethod
|
||||||
|
def validate_gitea_url(cls, v: str) -> str:
|
||||||
|
"""Ensure Gitea URL is properly formatted."""
|
||||||
|
if not v.startswith(("http://", "https://")):
|
||||||
|
raise ValueError("gitea_url must start with http:// or https://")
|
||||||
|
return v.rstrip("/")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled_tools_list(self) -> Optional[list[str]]:
|
||||||
|
"""Parse enabled_tools into a list."""
|
||||||
|
if not self.enabled_tools:
|
||||||
|
return None
|
||||||
|
return [tool.strip() for tool in self.enabled_tools.split(",") if tool.strip()]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def disabled_tools_list(self) -> Optional[list[str]]:
|
||||||
|
"""Parse disabled_tools into a list."""
|
||||||
|
if not self.disabled_tools:
|
||||||
|
return None
|
||||||
|
return [tool.strip() for tool in self.disabled_tools.split(",") if tool.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def load_settings(env_file: Optional[Path] = None) -> GiteaSettings:
|
||||||
|
"""
|
||||||
|
Load settings from environment or .env file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
env_file: Optional path to .env file. If not provided, searches for .env in current directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GiteaSettings instance with loaded configuration.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If required settings are missing or invalid.
|
||||||
|
"""
|
||||||
|
if env_file:
|
||||||
|
return GiteaSettings(_env_file=env_file)
|
||||||
|
return GiteaSettings()
|
||||||
5
src/gitea_mcp_remote/filtering/__init__.py
Normal file
5
src/gitea_mcp_remote/filtering/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Tool filtering module for Claude Desktop compatibility."""
|
||||||
|
|
||||||
|
from .filter import ToolFilter
|
||||||
|
|
||||||
|
__all__ = ["ToolFilter"]
|
||||||
109
src/gitea_mcp_remote/filtering/filter.py
Normal file
109
src/gitea_mcp_remote/filtering/filter.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"""Tool filtering for Claude Desktop compatibility."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ToolFilter:
|
||||||
|
"""
|
||||||
|
Filter MCP tools based on enabled/disabled lists.
|
||||||
|
|
||||||
|
This class handles tool filtering to ensure only compatible tools are exposed
|
||||||
|
to Claude Desktop, preventing crashes from unsupported tool schemas.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
enabled_tools: list[str] | None = None,
|
||||||
|
disabled_tools: list[str] | None = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize tool filter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enabled_tools: List of tool names to enable. If None, all tools are enabled.
|
||||||
|
disabled_tools: List of tool names to disable. Takes precedence over enabled_tools.
|
||||||
|
"""
|
||||||
|
if enabled_tools is not None and disabled_tools is not None:
|
||||||
|
logger.warning(
|
||||||
|
"Both enabled_tools and disabled_tools specified. "
|
||||||
|
"Disabled list takes precedence over enabled list."
|
||||||
|
)
|
||||||
|
|
||||||
|
self.enabled_tools = set(enabled_tools) if enabled_tools else None
|
||||||
|
self.disabled_tools = set(disabled_tools) if disabled_tools else None
|
||||||
|
|
||||||
|
def should_include_tool(self, tool_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if a tool should be included based on filter rules.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_name: Name of the tool to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if tool should be included, False otherwise.
|
||||||
|
"""
|
||||||
|
# If disabled list is specified, exclude disabled tools
|
||||||
|
if self.disabled_tools is not None:
|
||||||
|
return tool_name not in self.disabled_tools
|
||||||
|
|
||||||
|
# If enabled list is specified, only include enabled tools
|
||||||
|
if self.enabled_tools is not None:
|
||||||
|
return tool_name in self.enabled_tools
|
||||||
|
|
||||||
|
# If no filters specified, include all tools
|
||||||
|
return True
|
||||||
|
|
||||||
|
def filter_tools_list(self, tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Filter a list of tool definitions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tools: List of tool definitions (dicts with at least a 'name' field).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered list of tool definitions.
|
||||||
|
"""
|
||||||
|
return [tool for tool in tools if self.should_include_tool(tool.get("name", ""))]
|
||||||
|
|
||||||
|
def filter_tools_response(self, response: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Filter tools from an MCP list_tools response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: MCP response dict containing 'tools' list.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered response with tools list updated.
|
||||||
|
"""
|
||||||
|
if "tools" in response and isinstance(response["tools"], list):
|
||||||
|
response = response.copy()
|
||||||
|
response["tools"] = self.filter_tools_list(response["tools"])
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_filter_stats(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get statistics about the filter configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict containing filter mode and tool counts.
|
||||||
|
"""
|
||||||
|
if self.disabled_tools is not None:
|
||||||
|
return {
|
||||||
|
"mode": "blacklist",
|
||||||
|
"disabled_count": len(self.disabled_tools),
|
||||||
|
"disabled_tools": sorted(self.disabled_tools),
|
||||||
|
}
|
||||||
|
elif self.enabled_tools is not None:
|
||||||
|
return {
|
||||||
|
"mode": "whitelist",
|
||||||
|
"enabled_count": len(self.enabled_tools),
|
||||||
|
"enabled_tools": sorted(self.enabled_tools),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"mode": "passthrough",
|
||||||
|
"message": "All tools enabled",
|
||||||
|
}
|
||||||
5
src/gitea_mcp_remote/middleware/__init__.py
Normal file
5
src/gitea_mcp_remote/middleware/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""HTTP authentication middleware module."""
|
||||||
|
|
||||||
|
from .auth import BearerAuthMiddleware, HealthCheckBypassMiddleware
|
||||||
|
|
||||||
|
__all__ = ["BearerAuthMiddleware", "HealthCheckBypassMiddleware"]
|
||||||
144
src/gitea_mcp_remote/middleware/auth.py
Normal file
144
src/gitea_mcp_remote/middleware/auth.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
"""HTTP authentication middleware for MCP server."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Awaitable, Callable
|
||||||
|
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse, Response
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BearerAuthMiddleware(BaseHTTPMiddleware):
|
||||||
|
"""
|
||||||
|
Middleware to enforce Bearer token authentication on HTTP requests.
|
||||||
|
|
||||||
|
This middleware validates the Authorization header for all requests.
|
||||||
|
If a token is configured, requests must include "Authorization: Bearer <token>".
|
||||||
|
If no token is configured, all requests are allowed (open access).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app, auth_token: str | None = None):
|
||||||
|
"""
|
||||||
|
Initialize authentication middleware.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: ASGI application to wrap.
|
||||||
|
auth_token: Optional Bearer token for authentication.
|
||||||
|
If None, authentication is disabled.
|
||||||
|
"""
|
||||||
|
super().__init__(app)
|
||||||
|
self.auth_token = auth_token
|
||||||
|
self.auth_enabled = auth_token is not None
|
||||||
|
|
||||||
|
if self.auth_enabled:
|
||||||
|
logger.info("Bearer authentication enabled")
|
||||||
|
else:
|
||||||
|
logger.warning("Bearer authentication disabled - server is open access")
|
||||||
|
|
||||||
|
async def dispatch(
|
||||||
|
self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
||||||
|
) -> Response:
|
||||||
|
"""
|
||||||
|
Process request and enforce authentication if enabled.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Incoming HTTP request.
|
||||||
|
call_next: Next middleware or route handler.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response from downstream handler or 401/403 error.
|
||||||
|
"""
|
||||||
|
# Skip authentication if disabled
|
||||||
|
if not self.auth_enabled:
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
# Skip authentication if marked by HealthCheckBypassMiddleware
|
||||||
|
if getattr(request.state, "skip_auth", False):
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
# Extract Authorization header
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
|
||||||
|
# Check if header is present
|
||||||
|
if not auth_header:
|
||||||
|
logger.warning(f"Missing Authorization header from {request.client.host}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=401,
|
||||||
|
content={
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"message": "Missing Authorization header",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if header format is correct
|
||||||
|
if not auth_header.startswith("Bearer "):
|
||||||
|
logger.warning(f"Invalid Authorization format from {request.client.host}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=401,
|
||||||
|
content={
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"message": "Authorization header must use Bearer scheme",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract token
|
||||||
|
provided_token = auth_header[7:] # Remove "Bearer " prefix
|
||||||
|
|
||||||
|
# Validate token
|
||||||
|
if provided_token != self.auth_token:
|
||||||
|
logger.warning(f"Invalid token from {request.client.host}")
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=403,
|
||||||
|
content={
|
||||||
|
"error": "Forbidden",
|
||||||
|
"message": "Invalid authentication token",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Token is valid, proceed to next handler
|
||||||
|
logger.debug(f"Authenticated request from {request.client.host}")
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
|
||||||
|
class HealthCheckBypassMiddleware(BaseHTTPMiddleware):
|
||||||
|
"""
|
||||||
|
Middleware to bypass authentication for health check endpoints.
|
||||||
|
|
||||||
|
This allows monitoring systems to check server health without authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app, health_check_paths: list[str] | None = None):
|
||||||
|
"""
|
||||||
|
Initialize health check bypass middleware.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: ASGI application to wrap.
|
||||||
|
health_check_paths: List of paths to bypass authentication.
|
||||||
|
Defaults to ["/health", "/healthz", "/ping"].
|
||||||
|
"""
|
||||||
|
super().__init__(app)
|
||||||
|
self.health_check_paths = health_check_paths or ["/health", "/healthz", "/ping"]
|
||||||
|
|
||||||
|
async def dispatch(
|
||||||
|
self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
||||||
|
) -> Response:
|
||||||
|
"""
|
||||||
|
Process request and bypass authentication for health checks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Incoming HTTP request.
|
||||||
|
call_next: Next middleware or route handler.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response from downstream handler.
|
||||||
|
"""
|
||||||
|
# Check if request is for a health check endpoint
|
||||||
|
if request.url.path in self.health_check_paths:
|
||||||
|
logger.debug(f"Bypassing auth for health check: {request.url.path}")
|
||||||
|
# Mark request to skip authentication in BearerAuthMiddleware
|
||||||
|
request.state.skip_auth = True
|
||||||
|
|
||||||
|
# Continue to next middleware
|
||||||
|
return await call_next(request)
|
||||||
157
src/gitea_mcp_remote/server_http.py
Normal file
157
src/gitea_mcp_remote/server_http.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
"""
|
||||||
|
Gitea MCP Remote — HTTP server with MCP Streamable HTTP protocol.
|
||||||
|
|
||||||
|
This module imports tool definitions from the marketplace gitea-mcp-server
|
||||||
|
package and serves them over HTTP with authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
from typing import Any
|
||||||
|
import uvicorn
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
from starlette.responses import JSONResponse, Response
|
||||||
|
from starlette.routing import Route, Mount
|
||||||
|
from mcp.server import Server
|
||||||
|
from mcp.server.streamable_http import StreamableHTTPServerTransport
|
||||||
|
from mcp.types import Tool, TextContent
|
||||||
|
|
||||||
|
from gitea_mcp_remote.config import load_settings
|
||||||
|
from gitea_mcp_remote.middleware import BearerAuthMiddleware, HealthCheckBypassMiddleware
|
||||||
|
from gitea_mcp_remote.filtering import ToolFilter
|
||||||
|
|
||||||
|
# Import marketplace package
|
||||||
|
from mcp_server import get_tool_definitions, create_tool_dispatcher, GiteaClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def health_check(request):
|
||||||
|
"""Health check endpoint - bypasses authentication."""
|
||||||
|
return JSONResponse({"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
|
def create_mcp_server(tool_names: list[str] | None = None) -> Server:
|
||||||
|
"""Create and configure the MCP server with tools from the marketplace."""
|
||||||
|
mcp_server = Server("gitea-mcp-remote")
|
||||||
|
|
||||||
|
# Get tool definitions from marketplace
|
||||||
|
tools = get_tool_definitions(tool_filter=tool_names)
|
||||||
|
|
||||||
|
# Create Gitea client and dispatcher
|
||||||
|
gitea_client = GiteaClient()
|
||||||
|
dispatcher = create_tool_dispatcher(gitea_client, tool_filter=tool_names)
|
||||||
|
|
||||||
|
@mcp_server.list_tools()
|
||||||
|
async def list_tools() -> list[Tool]:
|
||||||
|
"""Return available Gitea tools."""
|
||||||
|
return tools
|
||||||
|
|
||||||
|
@mcp_server.call_tool()
|
||||||
|
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||||
|
"""Execute a tool with the given arguments."""
|
||||||
|
return await dispatcher(name, arguments)
|
||||||
|
|
||||||
|
return mcp_server
|
||||||
|
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
"""Create the Starlette application with middleware."""
|
||||||
|
settings = load_settings()
|
||||||
|
|
||||||
|
# Set up tool filtering
|
||||||
|
tool_filter = ToolFilter(
|
||||||
|
enabled_tools=settings.enabled_tools_list,
|
||||||
|
disabled_tools=settings.disabled_tools_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert to list for marketplace API
|
||||||
|
tool_names = None # means "all"
|
||||||
|
if tool_filter.enabled_tools:
|
||||||
|
tool_names = list(tool_filter.enabled_tools)
|
||||||
|
|
||||||
|
# Create MCP server with filtered tools
|
||||||
|
mcp_server = create_mcp_server(tool_names)
|
||||||
|
|
||||||
|
# Store server for endpoint access
|
||||||
|
app_state = {"mcp_server": mcp_server}
|
||||||
|
|
||||||
|
class MCPEndpoint:
|
||||||
|
"""ASGI app wrapper for MCP protocol endpoint."""
|
||||||
|
|
||||||
|
async def __call__(self, scope, receive, send):
|
||||||
|
"""Handle MCP requests - both POST and HEAD."""
|
||||||
|
method = scope.get("method", "")
|
||||||
|
|
||||||
|
if method == "HEAD":
|
||||||
|
# Return protocol version header
|
||||||
|
await send({
|
||||||
|
"type": "http.response.start",
|
||||||
|
"status": 200,
|
||||||
|
"headers": [
|
||||||
|
[b"x-mcp-protocol-version", b"2024-11-05"],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
await send({
|
||||||
|
"type": "http.response.body",
|
||||||
|
"body": b"",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
# For POST requests, use the StreamableHTTPServerTransport
|
||||||
|
# Create transport for this request
|
||||||
|
transport = StreamableHTTPServerTransport(mcp_session_id=None)
|
||||||
|
|
||||||
|
# Run the MCP server with this transport
|
||||||
|
async with transport.connect() as (read_stream, write_stream):
|
||||||
|
# Start server task
|
||||||
|
server_task = asyncio.create_task(
|
||||||
|
app_state["mcp_server"].run(
|
||||||
|
read_stream,
|
||||||
|
write_stream,
|
||||||
|
app_state["mcp_server"].create_initialization_options()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle the HTTP request
|
||||||
|
try:
|
||||||
|
await transport.handle_request(scope, receive, send)
|
||||||
|
finally:
|
||||||
|
# Cancel server task if still running
|
||||||
|
if not server_task.done():
|
||||||
|
server_task.cancel()
|
||||||
|
try:
|
||||||
|
await server_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create MCP endpoint instance
|
||||||
|
mcp_endpoint = MCPEndpoint()
|
||||||
|
|
||||||
|
routes = [
|
||||||
|
Route("/health", health_check, methods=["GET"]),
|
||||||
|
Route("/mcp", mcp_endpoint, methods=["GET", "POST", "HEAD"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
app = Starlette(routes=routes)
|
||||||
|
|
||||||
|
# Apply middleware (order matters - outermost runs first)
|
||||||
|
# HealthCheckBypass must wrap BearerAuth so it sets skip_auth flag first
|
||||||
|
if settings.auth_token:
|
||||||
|
app = BearerAuthMiddleware(app, auth_token=settings.auth_token)
|
||||||
|
app = HealthCheckBypassMiddleware(app)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Entry point for the gitea-mcp-remote command."""
|
||||||
|
settings = load_settings()
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
logger.info(f"Starting Gitea MCP Remote on {settings.http_host}:{settings.http_port}")
|
||||||
|
uvicorn.run(app, host=settings.http_host, port=settings.http_port)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
9
tests/__init__.py
Normal file
9
tests/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Test suite for HTTP wrapper functionality."""
|
||||||
|
|
||||||
|
# This package contains tests for:
|
||||||
|
# - config: Configuration loader and validation
|
||||||
|
# - filtering: Tool filtering for Claude Desktop compatibility
|
||||||
|
# - middleware: HTTP authentication middleware
|
||||||
|
# - server: Core HTTP MCP server (integration tests would go here)
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
59
tests/conftest.py
Normal file
59
tests/conftest.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""Pytest configuration and shared fixtures for test suite."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_gitea_config():
|
||||||
|
"""Provide sample Gitea configuration for tests."""
|
||||||
|
return {
|
||||||
|
"gitea_url": "https://gitea.test.com",
|
||||||
|
"gitea_token": "test_token_123",
|
||||||
|
"gitea_owner": "test_owner",
|
||||||
|
"gitea_repo": "test_repo",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_tools_list():
|
||||||
|
"""Provide sample MCP tools list for testing."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "list_issues",
|
||||||
|
"description": "List issues in repository",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"state": {"type": "string", "enum": ["open", "closed", "all"]},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "create_issue",
|
||||||
|
"description": "Create a new issue",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {"type": "string"},
|
||||||
|
"body": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["title"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "list_labels",
|
||||||
|
"description": "List labels in repository",
|
||||||
|
"inputSchema": {"type": "object", "properties": {}},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_mcp_response(sample_tools_list):
|
||||||
|
"""Provide sample MCP list_tools response."""
|
||||||
|
return {
|
||||||
|
"tools": sample_tools_list,
|
||||||
|
"meta": {
|
||||||
|
"version": "1.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
215
tests/test_config.py
Normal file
215
tests/test_config.py
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
"""Tests for configuration loader module."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from gitea_mcp_remote.config import GiteaSettings, load_settings
|
||||||
|
|
||||||
|
|
||||||
|
class TestGiteaSettings:
|
||||||
|
"""Test GiteaSettings configuration class."""
|
||||||
|
|
||||||
|
def test_required_fields(self):
|
||||||
|
"""Test that required fields are enforced."""
|
||||||
|
with pytest.raises(ValidationError) as exc_info:
|
||||||
|
GiteaSettings()
|
||||||
|
|
||||||
|
errors = exc_info.value.errors()
|
||||||
|
# Note: gitea_repo is optional (for PMO mode)
|
||||||
|
required_fields = {"gitea_url", "gitea_token", "gitea_owner"}
|
||||||
|
error_fields = {error["loc"][0] for error in errors}
|
||||||
|
assert required_fields.issubset(error_fields)
|
||||||
|
|
||||||
|
def test_valid_configuration(self):
|
||||||
|
"""Test valid configuration creation."""
|
||||||
|
settings = GiteaSettings(
|
||||||
|
gitea_url="https://gitea.example.com",
|
||||||
|
gitea_token="test_token",
|
||||||
|
gitea_owner="test_owner",
|
||||||
|
gitea_repo="test_repo",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert settings.gitea_url == "https://gitea.example.com"
|
||||||
|
assert settings.gitea_token == "test_token"
|
||||||
|
assert settings.gitea_owner == "test_owner"
|
||||||
|
assert settings.gitea_repo == "test_repo"
|
||||||
|
assert settings.http_host == "0.0.0.0"
|
||||||
|
assert settings.http_port == 8080
|
||||||
|
assert settings.auth_token is None
|
||||||
|
assert settings.mcp_auth_mode == "optional"
|
||||||
|
|
||||||
|
def test_gitea_url_validation(self):
|
||||||
|
"""Test Gitea URL validation."""
|
||||||
|
# Valid URLs
|
||||||
|
valid_urls = [
|
||||||
|
"http://gitea.local",
|
||||||
|
"https://gitea.example.com",
|
||||||
|
"http://192.168.1.1:3000",
|
||||||
|
]
|
||||||
|
|
||||||
|
for url in valid_urls:
|
||||||
|
settings = GiteaSettings(
|
||||||
|
gitea_url=url,
|
||||||
|
gitea_token="token",
|
||||||
|
gitea_owner="owner",
|
||||||
|
gitea_repo="repo",
|
||||||
|
)
|
||||||
|
assert settings.gitea_url == url.rstrip("/")
|
||||||
|
|
||||||
|
# Invalid URL (no protocol)
|
||||||
|
with pytest.raises(ValidationError) as exc_info:
|
||||||
|
GiteaSettings(
|
||||||
|
gitea_url="gitea.example.com",
|
||||||
|
gitea_token="token",
|
||||||
|
gitea_owner="owner",
|
||||||
|
gitea_repo="repo",
|
||||||
|
)
|
||||||
|
assert "must start with http://" in str(exc_info.value)
|
||||||
|
|
||||||
|
def test_gitea_url_trailing_slash_removed(self):
|
||||||
|
"""Test that trailing slashes are removed from Gitea URL."""
|
||||||
|
settings = GiteaSettings(
|
||||||
|
gitea_url="https://gitea.example.com/",
|
||||||
|
gitea_token="token",
|
||||||
|
gitea_owner="owner",
|
||||||
|
gitea_repo="repo",
|
||||||
|
)
|
||||||
|
assert settings.gitea_url == "https://gitea.example.com"
|
||||||
|
|
||||||
|
def test_http_port_validation(self):
|
||||||
|
"""Test HTTP port validation."""
|
||||||
|
# Valid port
|
||||||
|
settings = GiteaSettings(
|
||||||
|
gitea_url="https://gitea.example.com",
|
||||||
|
gitea_token="token",
|
||||||
|
gitea_owner="owner",
|
||||||
|
gitea_repo="repo",
|
||||||
|
http_port=9000,
|
||||||
|
)
|
||||||
|
assert settings.http_port == 9000
|
||||||
|
|
||||||
|
# Invalid port (too high)
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
GiteaSettings(
|
||||||
|
gitea_url="https://gitea.example.com",
|
||||||
|
gitea_token="token",
|
||||||
|
gitea_owner="owner",
|
||||||
|
gitea_repo="repo",
|
||||||
|
http_port=70000,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invalid port (too low)
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
GiteaSettings(
|
||||||
|
gitea_url="https://gitea.example.com",
|
||||||
|
gitea_token="token",
|
||||||
|
gitea_owner="owner",
|
||||||
|
gitea_repo="repo",
|
||||||
|
http_port=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_enabled_tools_list_parsing(self):
|
||||||
|
"""Test enabled_tools string parsing to list."""
|
||||||
|
settings = GiteaSettings(
|
||||||
|
gitea_url="https://gitea.example.com",
|
||||||
|
gitea_token="token",
|
||||||
|
gitea_owner="owner",
|
||||||
|
gitea_repo="repo",
|
||||||
|
enabled_tools="tool1,tool2,tool3",
|
||||||
|
)
|
||||||
|
assert settings.enabled_tools_list == ["tool1", "tool2", "tool3"]
|
||||||
|
|
||||||
|
# Test with spaces
|
||||||
|
settings = GiteaSettings(
|
||||||
|
gitea_url="https://gitea.example.com",
|
||||||
|
gitea_token="token",
|
||||||
|
gitea_owner="owner",
|
||||||
|
gitea_repo="repo",
|
||||||
|
enabled_tools="tool1, tool2 , tool3",
|
||||||
|
)
|
||||||
|
assert settings.enabled_tools_list == ["tool1", "tool2", "tool3"]
|
||||||
|
|
||||||
|
# Test empty string
|
||||||
|
settings = GiteaSettings(
|
||||||
|
gitea_url="https://gitea.example.com",
|
||||||
|
gitea_token="token",
|
||||||
|
gitea_owner="owner",
|
||||||
|
gitea_repo="repo",
|
||||||
|
enabled_tools="",
|
||||||
|
)
|
||||||
|
assert settings.enabled_tools_list is None
|
||||||
|
|
||||||
|
def test_disabled_tools_list_parsing(self):
|
||||||
|
"""Test disabled_tools string parsing to list."""
|
||||||
|
settings = GiteaSettings(
|
||||||
|
gitea_url="https://gitea.example.com",
|
||||||
|
gitea_token="token",
|
||||||
|
gitea_owner="owner",
|
||||||
|
gitea_repo="repo",
|
||||||
|
disabled_tools="tool1,tool2",
|
||||||
|
)
|
||||||
|
assert settings.disabled_tools_list == ["tool1", "tool2"]
|
||||||
|
|
||||||
|
def test_mcp_auth_mode_field(self):
|
||||||
|
"""Test mcp_auth_mode field with different values."""
|
||||||
|
settings = GiteaSettings(
|
||||||
|
gitea_url="https://gitea.example.com",
|
||||||
|
gitea_token="test_token",
|
||||||
|
gitea_owner="test_owner",
|
||||||
|
mcp_auth_mode="required",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert settings.mcp_auth_mode == "required"
|
||||||
|
|
||||||
|
# Default value
|
||||||
|
settings_default = GiteaSettings(
|
||||||
|
gitea_url="https://gitea.example.com",
|
||||||
|
gitea_token="test_token",
|
||||||
|
gitea_owner="test_owner",
|
||||||
|
)
|
||||||
|
assert settings_default.mcp_auth_mode == "optional"
|
||||||
|
|
||||||
|
|
||||||
|
class TestLoadSettings:
|
||||||
|
"""Test load_settings factory function."""
|
||||||
|
|
||||||
|
def test_load_from_env_file(self, tmp_path):
|
||||||
|
"""Test loading settings from a .env file."""
|
||||||
|
env_file = tmp_path / ".env"
|
||||||
|
env_file.write_text(
|
||||||
|
"""
|
||||||
|
GITEA_URL=https://gitea.test.com
|
||||||
|
GITEA_TOKEN=test_token_123
|
||||||
|
GITEA_OWNER=test_owner
|
||||||
|
GITEA_REPO=test_repo
|
||||||
|
HTTP_PORT=9000
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
settings = load_settings(env_file)
|
||||||
|
|
||||||
|
assert settings.gitea_url == "https://gitea.test.com"
|
||||||
|
assert settings.gitea_token == "test_token_123"
|
||||||
|
assert settings.gitea_owner == "test_owner"
|
||||||
|
assert settings.gitea_repo == "test_repo"
|
||||||
|
assert settings.http_port == 9000
|
||||||
|
|
||||||
|
def test_load_from_environment(self, monkeypatch):
|
||||||
|
"""Test loading settings from environment variables."""
|
||||||
|
monkeypatch.setenv("GITEA_URL", "https://env.gitea.com")
|
||||||
|
monkeypatch.setenv("GITEA_TOKEN", "env_token")
|
||||||
|
monkeypatch.setenv("GITEA_OWNER", "env_owner")
|
||||||
|
monkeypatch.setenv("GITEA_REPO", "env_repo")
|
||||||
|
monkeypatch.setenv("HTTP_PORT", "8080")
|
||||||
|
|
||||||
|
# Mock _env_file to prevent loading actual .env
|
||||||
|
settings = GiteaSettings()
|
||||||
|
|
||||||
|
assert settings.gitea_url == "https://env.gitea.com"
|
||||||
|
assert settings.gitea_token == "env_token"
|
||||||
|
assert settings.gitea_owner == "env_owner"
|
||||||
|
assert settings.gitea_repo == "env_repo"
|
||||||
|
assert settings.http_port == 8080
|
||||||
149
tests/test_filtering.py
Normal file
149
tests/test_filtering.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
"""Tests for tool filtering module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from gitea_mcp_remote.filtering import ToolFilter
|
||||||
|
|
||||||
|
|
||||||
|
class TestToolFilter:
|
||||||
|
"""Test ToolFilter class."""
|
||||||
|
|
||||||
|
def test_init_with_both_lists_logs_warning(self, caplog):
|
||||||
|
"""Test that specifying both enabled and disabled lists logs warning."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
with caplog.at_level(logging.WARNING):
|
||||||
|
filter = ToolFilter(enabled_tools=["tool1"], disabled_tools=["tool2"])
|
||||||
|
|
||||||
|
assert "Both enabled_tools and disabled_tools specified" in caplog.text
|
||||||
|
assert "Disabled list takes precedence" in caplog.text
|
||||||
|
|
||||||
|
# Verify disabled list takes precedence
|
||||||
|
assert not filter.should_include_tool("tool2")
|
||||||
|
|
||||||
|
def test_passthrough_mode(self):
|
||||||
|
"""Test passthrough mode (no filtering)."""
|
||||||
|
filter = ToolFilter()
|
||||||
|
|
||||||
|
assert filter.should_include_tool("any_tool")
|
||||||
|
assert filter.should_include_tool("another_tool")
|
||||||
|
|
||||||
|
stats = filter.get_filter_stats()
|
||||||
|
assert stats["mode"] == "passthrough"
|
||||||
|
|
||||||
|
def test_whitelist_mode(self):
|
||||||
|
"""Test whitelist mode (enabled_tools)."""
|
||||||
|
filter = ToolFilter(enabled_tools=["tool1", "tool2"])
|
||||||
|
|
||||||
|
assert filter.should_include_tool("tool1")
|
||||||
|
assert filter.should_include_tool("tool2")
|
||||||
|
assert not filter.should_include_tool("tool3")
|
||||||
|
assert not filter.should_include_tool("tool4")
|
||||||
|
|
||||||
|
stats = filter.get_filter_stats()
|
||||||
|
assert stats["mode"] == "whitelist"
|
||||||
|
assert stats["enabled_count"] == 2
|
||||||
|
assert "tool1" in stats["enabled_tools"]
|
||||||
|
assert "tool2" in stats["enabled_tools"]
|
||||||
|
|
||||||
|
def test_blacklist_mode(self):
|
||||||
|
"""Test blacklist mode (disabled_tools)."""
|
||||||
|
filter = ToolFilter(disabled_tools=["tool1", "tool2"])
|
||||||
|
|
||||||
|
assert not filter.should_include_tool("tool1")
|
||||||
|
assert not filter.should_include_tool("tool2")
|
||||||
|
assert filter.should_include_tool("tool3")
|
||||||
|
assert filter.should_include_tool("tool4")
|
||||||
|
|
||||||
|
stats = filter.get_filter_stats()
|
||||||
|
assert stats["mode"] == "blacklist"
|
||||||
|
assert stats["disabled_count"] == 2
|
||||||
|
assert "tool1" in stats["disabled_tools"]
|
||||||
|
assert "tool2" in stats["disabled_tools"]
|
||||||
|
|
||||||
|
def test_filter_tools_list(self):
|
||||||
|
"""Test filtering a list of tool definitions."""
|
||||||
|
filter = ToolFilter(enabled_tools=["tool1", "tool3"])
|
||||||
|
|
||||||
|
tools = [
|
||||||
|
{"name": "tool1", "description": "First tool"},
|
||||||
|
{"name": "tool2", "description": "Second tool"},
|
||||||
|
{"name": "tool3", "description": "Third tool"},
|
||||||
|
{"name": "tool4", "description": "Fourth tool"},
|
||||||
|
]
|
||||||
|
|
||||||
|
filtered = filter.filter_tools_list(tools)
|
||||||
|
|
||||||
|
assert len(filtered) == 2
|
||||||
|
assert filtered[0]["name"] == "tool1"
|
||||||
|
assert filtered[1]["name"] == "tool3"
|
||||||
|
|
||||||
|
def test_filter_tools_response(self):
|
||||||
|
"""Test filtering an MCP list_tools response."""
|
||||||
|
filter = ToolFilter(disabled_tools=["tool2"])
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"tools": [
|
||||||
|
{"name": "tool1", "description": "First tool"},
|
||||||
|
{"name": "tool2", "description": "Second tool"},
|
||||||
|
{"name": "tool3", "description": "Third tool"},
|
||||||
|
],
|
||||||
|
"other_data": "preserved",
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = filter.filter_tools_response(response)
|
||||||
|
|
||||||
|
assert len(filtered["tools"]) == 2
|
||||||
|
assert filtered["tools"][0]["name"] == "tool1"
|
||||||
|
assert filtered["tools"][1]["name"] == "tool3"
|
||||||
|
assert filtered["other_data"] == "preserved"
|
||||||
|
|
||||||
|
def test_filter_tools_response_no_tools_key(self):
|
||||||
|
"""Test filtering response without 'tools' key."""
|
||||||
|
filter = ToolFilter(enabled_tools=["tool1"])
|
||||||
|
|
||||||
|
response = {"other_data": "value"}
|
||||||
|
filtered = filter.filter_tools_response(response)
|
||||||
|
|
||||||
|
assert filtered == response
|
||||||
|
|
||||||
|
def test_filter_tools_response_immutable(self):
|
||||||
|
"""Test that original response is not mutated."""
|
||||||
|
filter = ToolFilter(enabled_tools=["tool1"])
|
||||||
|
|
||||||
|
original = {
|
||||||
|
"tools": [
|
||||||
|
{"name": "tool1"},
|
||||||
|
{"name": "tool2"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = filter.filter_tools_response(original)
|
||||||
|
|
||||||
|
# Original should still have 2 tools
|
||||||
|
assert len(original["tools"]) == 2
|
||||||
|
# Filtered should have 1 tool
|
||||||
|
assert len(filtered["tools"]) == 1
|
||||||
|
|
||||||
|
def test_empty_tool_list(self):
|
||||||
|
"""Test filtering empty tool list."""
|
||||||
|
filter = ToolFilter(enabled_tools=["tool1"])
|
||||||
|
|
||||||
|
result = filter.filter_tools_list([])
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_tool_with_no_name(self):
|
||||||
|
"""Test handling tool without name field."""
|
||||||
|
filter = ToolFilter(enabled_tools=["tool1"])
|
||||||
|
|
||||||
|
tools = [
|
||||||
|
{"name": "tool1"},
|
||||||
|
{"description": "No name"},
|
||||||
|
{"name": "tool2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
filtered = filter.filter_tools_list(tools)
|
||||||
|
|
||||||
|
# Only tool1 should match, tool without name is excluded
|
||||||
|
assert len(filtered) == 1
|
||||||
|
assert filtered[0]["name"] == "tool1"
|
||||||
137
tests/test_mcp_endpoints.py
Normal file
137
tests/test_mcp_endpoints.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
"""Tests for MCP protocol endpoints and health checks."""
|
||||||
|
import pytest
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from starlette.testclient import TestClient
|
||||||
|
from gitea_mcp_remote.server_http import create_app
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_env():
|
||||||
|
"""Mock environment variables for testing."""
|
||||||
|
env = {
|
||||||
|
"GITEA_URL": "https://gitea.example.com",
|
||||||
|
"GITEA_TOKEN": "test_token",
|
||||||
|
"GITEA_OWNER": "test_owner",
|
||||||
|
}
|
||||||
|
with patch.dict("os.environ", env):
|
||||||
|
yield env
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(mock_env):
|
||||||
|
"""Create test client."""
|
||||||
|
app = create_app()
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Health Endpoint Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_endpoint(client):
|
||||||
|
"""Test GET /health returns status ok."""
|
||||||
|
response = client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_endpoint_no_auth_required(mock_env):
|
||||||
|
"""Test health endpoint works even with AUTH_TOKEN set."""
|
||||||
|
with patch.dict("os.environ", {**mock_env, "AUTH_TOKEN": "secret123"}):
|
||||||
|
app = create_app()
|
||||||
|
client = TestClient(app)
|
||||||
|
# Health should bypass auth
|
||||||
|
response = client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MCP Protocol Tests
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def parse_sse_message(sse_text: str) -> dict:
|
||||||
|
"""Parse SSE message data."""
|
||||||
|
data_match = re.search(r'data: (.+)', sse_text)
|
||||||
|
if data_match:
|
||||||
|
return json.loads(data_match.group(1))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_head_endpoint(client):
|
||||||
|
"""Test HEAD /mcp returns protocol version header."""
|
||||||
|
response = client.head("/mcp")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "x-mcp-protocol-version" in response.headers
|
||||||
|
assert response.headers["x-mcp-protocol-version"] == "2024-11-05"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_initialize(client):
|
||||||
|
"""Test MCP initialize request."""
|
||||||
|
initialize_request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "initialize",
|
||||||
|
"params": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {},
|
||||||
|
"clientInfo": {
|
||||||
|
"name": "test-client",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/mcp",
|
||||||
|
json=initialize_request,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json, text/event-stream"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Parse SSE response
|
||||||
|
data = parse_sse_message(response.text)
|
||||||
|
assert data is not None
|
||||||
|
assert data.get("jsonrpc") == "2.0"
|
||||||
|
assert "result" in data
|
||||||
|
assert data["result"].get("protocolVersion") == "2024-11-05"
|
||||||
|
assert "serverInfo" in data["result"]
|
||||||
|
assert data["result"]["serverInfo"]["name"] == "gitea-mcp-remote"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_missing_accept_header(client):
|
||||||
|
"""Test MCP request without required Accept header."""
|
||||||
|
initialize_request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "initialize",
|
||||||
|
"params": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {},
|
||||||
|
"clientInfo": {
|
||||||
|
"name": "test-client",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/mcp",
|
||||||
|
json=initialize_request,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json" # Missing text/event-stream
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return error about missing accept header
|
||||||
|
assert response.status_code == 406
|
||||||
162
tests/test_middleware.py
Normal file
162
tests/test_middleware.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
"""Tests for HTTP authentication middleware."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from starlette.applications import Starlette
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from starlette.routing import Route
|
||||||
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
from gitea_mcp_remote.middleware import (
|
||||||
|
BearerAuthMiddleware,
|
||||||
|
HealthCheckBypassMiddleware,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Test application endpoint
|
||||||
|
async def test_endpoint(request):
|
||||||
|
return JSONResponse({"message": "success"})
|
||||||
|
|
||||||
|
|
||||||
|
class TestBearerAuthMiddleware:
|
||||||
|
"""Test BearerAuthMiddleware."""
|
||||||
|
|
||||||
|
def test_no_auth_configured(self):
|
||||||
|
"""Test that requests pass through when no auth token is configured."""
|
||||||
|
app = Starlette(routes=[Route("/test", test_endpoint)])
|
||||||
|
app.add_middleware(BearerAuthMiddleware, auth_token=None)
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.get("/test")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["message"] == "success"
|
||||||
|
|
||||||
|
def test_auth_configured_valid_token(self):
|
||||||
|
"""Test successful authentication with valid token."""
|
||||||
|
app = Starlette(routes=[Route("/test", test_endpoint)])
|
||||||
|
app.add_middleware(BearerAuthMiddleware, auth_token="secret_token")
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.get("/test", headers={"Authorization": "Bearer secret_token"})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["message"] == "success"
|
||||||
|
|
||||||
|
def test_auth_configured_missing_header(self):
|
||||||
|
"""Test rejection when Authorization header is missing."""
|
||||||
|
app = Starlette(routes=[Route("/test", test_endpoint)])
|
||||||
|
app.add_middleware(BearerAuthMiddleware, auth_token="secret_token")
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.get("/test")
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert "Missing Authorization header" in response.json()["message"]
|
||||||
|
|
||||||
|
def test_auth_configured_invalid_format(self):
|
||||||
|
"""Test rejection when Authorization header has wrong format."""
|
||||||
|
app = Starlette(routes=[Route("/test", test_endpoint)])
|
||||||
|
app.add_middleware(BearerAuthMiddleware, auth_token="secret_token")
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
# Test with wrong scheme
|
||||||
|
response = client.get("/test", headers={"Authorization": "Basic secret_token"})
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert "Bearer scheme" in response.json()["message"]
|
||||||
|
|
||||||
|
# Test with no scheme
|
||||||
|
response = client.get("/test", headers={"Authorization": "secret_token"})
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
def test_auth_configured_invalid_token(self):
|
||||||
|
"""Test rejection when token is invalid."""
|
||||||
|
app = Starlette(routes=[Route("/test", test_endpoint)])
|
||||||
|
app.add_middleware(BearerAuthMiddleware, auth_token="secret_token")
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.get("/test", headers={"Authorization": "Bearer wrong_token"})
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
assert "Invalid authentication token" in response.json()["message"]
|
||||||
|
|
||||||
|
def test_auth_case_sensitive_token(self):
|
||||||
|
"""Test that token comparison is case-sensitive."""
|
||||||
|
app = Starlette(routes=[Route("/test", test_endpoint)])
|
||||||
|
app.add_middleware(BearerAuthMiddleware, auth_token="Secret_Token")
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
# Correct case
|
||||||
|
response = client.get("/test", headers={"Authorization": "Bearer Secret_Token"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Wrong case
|
||||||
|
response = client.get("/test", headers={"Authorization": "Bearer secret_token"})
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
class TestHealthCheckBypassMiddleware:
|
||||||
|
"""Test HealthCheckBypassMiddleware."""
|
||||||
|
|
||||||
|
def test_default_health_check_paths(self):
|
||||||
|
"""Test that default health check paths bypass auth."""
|
||||||
|
app = Starlette(
|
||||||
|
routes=[
|
||||||
|
Route("/health", test_endpoint),
|
||||||
|
Route("/healthz", test_endpoint),
|
||||||
|
Route("/ping", test_endpoint),
|
||||||
|
Route("/test", test_endpoint),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
app.add_middleware(BearerAuthMiddleware, auth_token="secret_token")
|
||||||
|
app.add_middleware(HealthCheckBypassMiddleware)
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
# Health checks should work without auth
|
||||||
|
assert client.get("/health").status_code == 200
|
||||||
|
assert client.get("/healthz").status_code == 200
|
||||||
|
assert client.get("/ping").status_code == 200
|
||||||
|
|
||||||
|
# Regular endpoint should require auth
|
||||||
|
assert client.get("/test").status_code == 401
|
||||||
|
|
||||||
|
def test_custom_health_check_paths(self):
|
||||||
|
"""Test custom health check paths."""
|
||||||
|
app = Starlette(
|
||||||
|
routes=[
|
||||||
|
Route("/custom-health", test_endpoint),
|
||||||
|
Route("/test", test_endpoint),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
app.add_middleware(BearerAuthMiddleware, auth_token="secret_token")
|
||||||
|
app.add_middleware(
|
||||||
|
HealthCheckBypassMiddleware,
|
||||||
|
health_check_paths=["/custom-health"],
|
||||||
|
)
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
# Custom health check should work without auth
|
||||||
|
assert client.get("/custom-health").status_code == 200
|
||||||
|
|
||||||
|
# Regular endpoint should require auth
|
||||||
|
assert client.get("/test").status_code == 401
|
||||||
|
|
||||||
|
def test_middleware_order(self):
|
||||||
|
"""Test that middleware order is correct."""
|
||||||
|
# HealthCheckBypass should be added BEFORE BearerAuth
|
||||||
|
# so it can bypass the auth check
|
||||||
|
|
||||||
|
app = Starlette(routes=[Route("/health", test_endpoint)])
|
||||||
|
|
||||||
|
# Correct order: HealthCheck bypass first, then Auth
|
||||||
|
app.add_middleware(BearerAuthMiddleware, auth_token="secret_token")
|
||||||
|
app.add_middleware(HealthCheckBypassMiddleware)
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.get("/health")
|
||||||
|
|
||||||
|
# Should succeed without auth
|
||||||
|
assert response.status_code == 200
|
||||||
Reference in New Issue
Block a user