generated from personal-projects/leo-claude-mktplace
Compare commits
9 Commits
1c55eed7c0
...
2dbb66deae
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dbb66deae | |||
| 88c16c840b | |||
| 9fea0683f7 | |||
| a5390a3086 | |||
| c3caf4e169 | |||
| cd8718c114 | |||
| 53a0006a3f | |||
| fb8cc08112 | |||
| 809eef132a |
@@ -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
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
|
|||||||
161
README.md
161
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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -144,40 +151,64 @@ nano .env
|
|||||||
|
|
||||||
# Run the server
|
# Run the server
|
||||||
gitea-mcp-remote
|
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
|
||||||
{
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "initialize",
|
||||||
|
"params": {
|
||||||
|
"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",
|
"name": "list_issues",
|
||||||
"arguments": {
|
"arguments": {
|
||||||
"owner": "myorg",
|
"owner": "myorg",
|
||||||
@@ -185,6 +216,7 @@ Authorization: Bearer YOUR_TOKEN # If auth enabled
|
|||||||
"state": "open"
|
"state": "open"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### With Claude Desktop
|
### With Claude Desktop
|
||||||
@@ -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_mcp_remote
|
pytest tests/ --cov=gitea_mcp_remote
|
||||||
|
|
||||||
# Run specific test file
|
# Run specific test file
|
||||||
pytest src/gitea_mcp_remote/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_mcp_remote/
|
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ ├── server.py # Main HTTP server
|
│ ├── server_http.py # MCP HTTP server
|
||||||
│ ├── config/
|
│ ├── config/ # Configuration module
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ └── settings.py # Configuration loader
|
│ │ └── settings.py
|
||||||
│ ├── middleware/
|
│ ├── middleware/ # HTTP middleware
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ └── auth.py # HTTP authentication
|
│ │ └── auth.py
|
||||||
│ ├── filtering/
|
│ └── filtering/ # Tool filtering
|
||||||
│ │ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ │ └── filter.py # Tool filtering
|
│ └── filter.py
|
||||||
│ └── tests/ # Test suite
|
├── tests/ # Test suite (at repo root)
|
||||||
│ ├── conftest.py
|
│ ├── conftest.py
|
||||||
│ ├── test_config.py
|
│ ├── test_config.py
|
||||||
│ ├── test_filtering.py
|
│ ├── test_filtering.py
|
||||||
│ └── test_middleware.py
|
│ ├── test_middleware.py
|
||||||
├── Dockerfile # Docker image
|
│ └── test_mcp_endpoints.py
|
||||||
├── docker-compose.yml # Docker orchestration
|
├── docker/ # Docker infrastructure
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── Caddyfile
|
||||||
|
│ └── .env.example
|
||||||
|
├── scripts/ # Utility scripts
|
||||||
|
│ ├── start.sh
|
||||||
|
│ └── healthcheck.sh
|
||||||
├── 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: 0.2.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:
|
||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "gitea-mcp-remote"
|
name = "gitea-mcp-remote"
|
||||||
version = "1.0.0"
|
version = "0.2.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-mcp-remote = "gitea_mcp_remote.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_mcp_remote/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_mcp_remote/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
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Gitea HTTP MCP Wrapper
|
Gitea MCP Remote — HTTP deployment wrapper for marketplace Gitea MCP server.
|
||||||
|
|
||||||
This package provides an HTTP transport wrapper around the official Gitea MCP server.
|
Imports tool definitions from gitea-mcp-server (marketplace) and serves them
|
||||||
It handles configuration loading, tool filtering, and HTTP authentication middleware.
|
over Streamable HTTP transport with authentication and TLS via Caddy.
|
||||||
|
|
||||||
Architecture:
|
|
||||||
- config/: Configuration loader module
|
|
||||||
- middleware/: HTTP authentication middleware
|
|
||||||
- filtering/: Tool filtering for Claude Desktop compatibility
|
|
||||||
- server.py: Main HTTP MCP server implementation
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .server import GiteaMCPWrapper, create_app, main
|
from .server_http import create_app, main
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.2.0"
|
||||||
__all__ = ["__version__", "GiteaMCPWrapper", "create_app", "main"]
|
__all__ = ["__version__", "create_app", "main"]
|
||||||
|
|||||||
@@ -1,309 +0,0 @@
|
|||||||
"""HTTP MCP server implementation wrapping Gitea MCP."""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import uvicorn
|
|
||||||
from mcp.server import Server
|
|
||||||
from mcp.server.stdio import stdio_server
|
|
||||||
from starlette.applications import Starlette
|
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.responses import JSONResponse
|
|
||||||
from starlette.routing import Route
|
|
||||||
|
|
||||||
from gitea_mcp_remote.config import GiteaSettings, load_settings
|
|
||||||
from gitea_mcp_remote.filtering import ToolFilter
|
|
||||||
from gitea_mcp_remote.middleware import (
|
|
||||||
BearerAuthMiddleware,
|
|
||||||
HealthCheckBypassMiddleware,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
||||||
)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaMCPWrapper:
|
|
||||||
"""
|
|
||||||
HTTP wrapper around the official Gitea MCP server.
|
|
||||||
|
|
||||||
This class manages:
|
|
||||||
1. Starting the Gitea MCP server as a subprocess with stdio transport
|
|
||||||
2. Proxying HTTP requests to the MCP server
|
|
||||||
3. Filtering tools based on configuration
|
|
||||||
4. Handling responses and errors
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, settings: GiteaSettings):
|
|
||||||
"""
|
|
||||||
Initialize the MCP wrapper.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
settings: Configuration settings for Gitea and HTTP server.
|
|
||||||
"""
|
|
||||||
self.settings = settings
|
|
||||||
self.tool_filter = ToolFilter(
|
|
||||||
enabled_tools=settings.enabled_tools_list,
|
|
||||||
disabled_tools=settings.disabled_tools_list,
|
|
||||||
)
|
|
||||||
self.process = None
|
|
||||||
self.reader = None
|
|
||||||
self.writer = None
|
|
||||||
|
|
||||||
async def start_gitea_mcp(self) -> None:
|
|
||||||
"""
|
|
||||||
Start the Gitea MCP server as a subprocess.
|
|
||||||
|
|
||||||
The server runs with stdio transport, and we communicate via stdin/stdout.
|
|
||||||
"""
|
|
||||||
logger.info("Starting Gitea MCP server subprocess")
|
|
||||||
|
|
||||||
# Set environment variables for Gitea MCP
|
|
||||||
env = os.environ.copy()
|
|
||||||
env.update(self.settings.get_gitea_mcp_env())
|
|
||||||
|
|
||||||
# Start the process
|
|
||||||
# Note: This assumes gitea-mcp-server is installed and on PATH
|
|
||||||
# In production Docker, this should be guaranteed
|
|
||||||
try:
|
|
||||||
self.process = await asyncio.create_subprocess_exec(
|
|
||||||
"gitea-mcp-server",
|
|
||||||
stdin=asyncio.subprocess.PIPE,
|
|
||||||
stdout=asyncio.subprocess.PIPE,
|
|
||||||
stderr=asyncio.subprocess.PIPE,
|
|
||||||
env=env,
|
|
||||||
)
|
|
||||||
self.reader = self.process.stdout
|
|
||||||
self.writer = self.process.stdin
|
|
||||||
logger.info("Gitea MCP server started successfully")
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.error("gitea-mcp-server not found in PATH")
|
|
||||||
raise RuntimeError(
|
|
||||||
"gitea-mcp-server not found. Ensure it's installed: pip install gitea-mcp-server"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def stop_gitea_mcp(self) -> None:
|
|
||||||
"""Stop the Gitea MCP server subprocess."""
|
|
||||||
if self.process:
|
|
||||||
logger.info("Stopping Gitea MCP server subprocess")
|
|
||||||
self.process.terminate()
|
|
||||||
await self.process.wait()
|
|
||||||
logger.info("Gitea MCP server stopped")
|
|
||||||
|
|
||||||
async def send_mcp_request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Send a JSON-RPC request to the MCP server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
method: MCP method name (e.g., "tools/list", "tools/call").
|
|
||||||
params: Method parameters.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON-RPC response from MCP server.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If MCP server is not running or communication fails.
|
|
||||||
"""
|
|
||||||
if not self.writer or not self.reader:
|
|
||||||
raise RuntimeError("MCP server not started")
|
|
||||||
|
|
||||||
# Build JSON-RPC request
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": method,
|
|
||||||
"params": params,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Send request
|
|
||||||
request_json = json.dumps(request) + "\n"
|
|
||||||
self.writer.write(request_json.encode())
|
|
||||||
await self.writer.drain()
|
|
||||||
|
|
||||||
# Read response
|
|
||||||
response_line = await self.reader.readline()
|
|
||||||
response = json.loads(response_line.decode())
|
|
||||||
|
|
||||||
# Check for JSON-RPC error
|
|
||||||
if "error" in response:
|
|
||||||
logger.error(f"MCP error: {response['error']}")
|
|
||||||
raise RuntimeError(f"MCP error: {response['error']}")
|
|
||||||
|
|
||||||
return response.get("result", {})
|
|
||||||
|
|
||||||
async def list_tools(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
List available tools from MCP server with filtering applied.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Filtered tools list response.
|
|
||||||
"""
|
|
||||||
response = await self.send_mcp_request("tools/list", {})
|
|
||||||
filtered_response = self.tool_filter.filter_tools_response(response)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"Listed {len(filtered_response.get('tools', []))} tools "
|
|
||||||
f"(filter: {self.tool_filter.get_filter_stats()['mode']})"
|
|
||||||
)
|
|
||||||
return filtered_response
|
|
||||||
|
|
||||||
async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Call a tool on the MCP server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tool_name: Name of tool to call.
|
|
||||||
arguments: Tool arguments.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tool execution result.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If tool is filtered out.
|
|
||||||
"""
|
|
||||||
# Check if tool is allowed
|
|
||||||
if not self.tool_filter.should_include_tool(tool_name):
|
|
||||||
raise ValueError(f"Tool '{tool_name}' is not available (filtered)")
|
|
||||||
|
|
||||||
logger.info(f"Calling tool: {tool_name}")
|
|
||||||
result = await self.send_mcp_request(
|
|
||||||
"tools/call",
|
|
||||||
{"name": tool_name, "arguments": arguments},
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# Global wrapper instance
|
|
||||||
wrapper: GiteaMCPWrapper | None = None
|
|
||||||
|
|
||||||
|
|
||||||
async def health_check(request: Request) -> JSONResponse:
|
|
||||||
"""Health check endpoint."""
|
|
||||||
return JSONResponse({"status": "healthy"})
|
|
||||||
|
|
||||||
|
|
||||||
async def list_tools_endpoint(request: Request) -> JSONResponse:
|
|
||||||
"""List available tools."""
|
|
||||||
try:
|
|
||||||
tools = await wrapper.list_tools()
|
|
||||||
return JSONResponse(tools)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception("Error listing tools")
|
|
||||||
return JSONResponse(
|
|
||||||
{"error": str(e)},
|
|
||||||
status_code=500,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def call_tool_endpoint(request: Request) -> JSONResponse:
|
|
||||||
"""Call a tool."""
|
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
tool_name = body.get("name")
|
|
||||||
arguments = body.get("arguments", {})
|
|
||||||
|
|
||||||
if not tool_name:
|
|
||||||
return JSONResponse(
|
|
||||||
{"error": "Missing 'name' field"},
|
|
||||||
status_code=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
result = await wrapper.call_tool(tool_name, arguments)
|
|
||||||
return JSONResponse(result)
|
|
||||||
except ValueError as e:
|
|
||||||
# Tool filtered
|
|
||||||
return JSONResponse(
|
|
||||||
{"error": str(e)},
|
|
||||||
status_code=403,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception("Error calling tool")
|
|
||||||
return JSONResponse(
|
|
||||||
{"error": str(e)},
|
|
||||||
status_code=500,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def startup() -> None:
|
|
||||||
"""Application startup handler."""
|
|
||||||
global wrapper
|
|
||||||
settings = load_settings()
|
|
||||||
wrapper = GiteaMCPWrapper(settings)
|
|
||||||
await wrapper.start_gitea_mcp()
|
|
||||||
logger.info(f"HTTP MCP server starting on {settings.http_host}:{settings.http_port}")
|
|
||||||
|
|
||||||
|
|
||||||
async def shutdown() -> None:
|
|
||||||
"""Application shutdown handler."""
|
|
||||||
global wrapper
|
|
||||||
if wrapper:
|
|
||||||
await wrapper.stop_gitea_mcp()
|
|
||||||
|
|
||||||
|
|
||||||
# Define routes
|
|
||||||
routes = [
|
|
||||||
Route("/health", health_check, methods=["GET"]),
|
|
||||||
Route("/healthz", health_check, methods=["GET"]),
|
|
||||||
Route("/ping", health_check, methods=["GET"]),
|
|
||||||
Route("/tools/list", list_tools_endpoint, methods=["POST"]),
|
|
||||||
Route("/tools/call", call_tool_endpoint, methods=["POST"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create Starlette app
|
|
||||||
app = Starlette(
|
|
||||||
routes=routes,
|
|
||||||
on_startup=[startup],
|
|
||||||
on_shutdown=[shutdown],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_app(settings: GiteaSettings | None = None) -> Starlette:
|
|
||||||
"""
|
|
||||||
Create and configure the Starlette application.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
settings: Optional settings override for testing.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Configured Starlette application.
|
|
||||||
"""
|
|
||||||
if settings is None:
|
|
||||||
settings = load_settings()
|
|
||||||
|
|
||||||
# Add middleware
|
|
||||||
app.add_middleware(HealthCheckBypassMiddleware)
|
|
||||||
app.add_middleware(BearerAuthMiddleware, auth_token=settings.auth_token)
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""Main entry point for the HTTP MCP server."""
|
|
||||||
settings = load_settings()
|
|
||||||
|
|
||||||
# Log filter configuration
|
|
||||||
filter_stats = ToolFilter(
|
|
||||||
enabled_tools=settings.enabled_tools_list,
|
|
||||||
disabled_tools=settings.disabled_tools_list,
|
|
||||||
).get_filter_stats()
|
|
||||||
logger.info(f"Tool filtering: {filter_stats}")
|
|
||||||
|
|
||||||
# Run server
|
|
||||||
uvicorn.run(
|
|
||||||
"gitea_mcp_remote.server:app",
|
|
||||||
host=settings.http_host,
|
|
||||||
port=settings.http_port,
|
|
||||||
log_level="info",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
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()
|
||||||
@@ -36,9 +36,10 @@ class TestGiteaSettings:
|
|||||||
assert settings.gitea_token == "test_token"
|
assert settings.gitea_token == "test_token"
|
||||||
assert settings.gitea_owner == "test_owner"
|
assert settings.gitea_owner == "test_owner"
|
||||||
assert settings.gitea_repo == "test_repo"
|
assert settings.gitea_repo == "test_repo"
|
||||||
assert settings.http_host == "127.0.0.1"
|
assert settings.http_host == "0.0.0.0"
|
||||||
assert settings.http_port == 8000
|
assert settings.http_port == 8080
|
||||||
assert settings.auth_token is None
|
assert settings.auth_token is None
|
||||||
|
assert settings.mcp_auth_mode == "optional"
|
||||||
|
|
||||||
def test_gitea_url_validation(self):
|
def test_gitea_url_validation(self):
|
||||||
"""Test Gitea URL validation."""
|
"""Test Gitea URL validation."""
|
||||||
@@ -152,21 +153,24 @@ class TestGiteaSettings:
|
|||||||
)
|
)
|
||||||
assert settings.disabled_tools_list == ["tool1", "tool2"]
|
assert settings.disabled_tools_list == ["tool1", "tool2"]
|
||||||
|
|
||||||
def test_get_gitea_mcp_env(self):
|
def test_mcp_auth_mode_field(self):
|
||||||
"""Test environment variable generation for wrapped MCP server."""
|
"""Test mcp_auth_mode field with different values."""
|
||||||
settings = GiteaSettings(
|
settings = GiteaSettings(
|
||||||
gitea_url="https://gitea.example.com",
|
gitea_url="https://gitea.example.com",
|
||||||
gitea_token="test_token",
|
gitea_token="test_token",
|
||||||
gitea_owner="test_owner",
|
gitea_owner="test_owner",
|
||||||
gitea_repo="test_repo",
|
mcp_auth_mode="required",
|
||||||
)
|
)
|
||||||
|
|
||||||
env = settings.get_gitea_mcp_env()
|
assert settings.mcp_auth_mode == "required"
|
||||||
|
|
||||||
assert env["GITEA_BASE_URL"] == "https://gitea.example.com"
|
# Default value
|
||||||
assert env["GITEA_API_TOKEN"] == "test_token"
|
settings_default = GiteaSettings(
|
||||||
assert env["GITEA_DEFAULT_OWNER"] == "test_owner"
|
gitea_url="https://gitea.example.com",
|
||||||
assert env["GITEA_DEFAULT_REPO"] == "test_repo"
|
gitea_token="test_token",
|
||||||
|
gitea_owner="test_owner",
|
||||||
|
)
|
||||||
|
assert settings_default.mcp_auth_mode == "optional"
|
||||||
|
|
||||||
|
|
||||||
class TestLoadSettings:
|
class TestLoadSettings:
|
||||||
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
|
||||||
Reference in New Issue
Block a user