diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..afa6551 --- /dev/null +++ b/.env.example @@ -0,0 +1,47 @@ +# Yargı MCP Server Environment Configuration +# Copy this file to .env and customize as needed + +# Server Configuration +HOST=0.0.0.0 +PORT=8000 +LOG_LEVEL=info + +# CORS Configuration +# Comma-separated list of allowed origins +# Use * to allow all origins (not recommended for production) +ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080,https://yourdomain.com + +# Authentication (optional) +# Uncomment and set to enable token-based authentication +# API_TOKEN=your-secret-token-here + +# Worker Configuration +# Number of worker processes (for production) +# WORKERS=4 + +# SSL Configuration (optional) +# SSL_CERT_FILE=/path/to/cert.pem +# SSL_KEY_FILE=/path/to/key.pem + +# Database Timeouts (seconds) +# Adjust based on your network conditions +YARGITAY_TIMEOUT=60 +DANISTAY_TIMEOUT=60 +BEDESTEN_TIMEOUT=60 +ANAYASA_TIMEOUT=90 +KIK_TIMEOUT=45 +REKABET_TIMEOUT=45 +UYUSMAZLIK_TIMEOUT=30 +EMSAL_TIMEOUT=60 + +# Development Settings +# Enable debug mode (not for production) +# DEBUG=false + +# Monitoring (optional) +# Sentry DSN for error tracking +# SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id + +# OpenTelemetry Configuration (optional) +# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 +# OTEL_SERVICE_NAME=yargi-mcp-server \ No newline at end of file diff --git a/.gitignore b/.gitignore index e4b1f28..db012ff 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,15 @@ fast-mcp-docs.md debug_* test_* CLAUDE.md + +# ASGI/Deployment files +ssl/ +*.pem +*.key +*.crt + +# Docker volumes +redis-data/ + +# Production logs +logs/*.log.* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..15627bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,86 @@ +# Multi-stage Dockerfile for Yargı MCP Server + +# Build stage +FROM python:3.12-slim as builder + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy requirements first for better caching +COPY pyproject.toml ./ +COPY README.md ./ + +# Install dependencies +RUN pip install --no-cache-dir uv && \ + uv pip install --system --no-cache-dir . + +# Runtime stage +FROM python:3.12-slim + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + # Required for Playwright + libnss3 \ + libnspr4 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libdbus-1-3 \ + libatspi2.0-0 \ + libx11-6 \ + libxcomposite1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxrandr2 \ + libgbm1 \ + libxcb1 \ + libxkbcommon0 \ + libpango-1.0-0 \ + libcairo2 \ + libasound2 \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN useradd -m -u 1000 mcp && \ + mkdir -p /app && \ + chown -R mcp:mcp /app + +# Set working directory +WORKDIR /app + +# Copy from builder +COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin + +# Copy application code +COPY --chown=mcp:mcp . . + +# Install Playwright browsers +RUN playwright install chromium + +# Switch to non-root user +USER mcp + +# Expose port +EXPOSE 8000 + +# Set environment variables +ENV PYTHONUNBUFFERED=1 +ENV HOST=0.0.0.0 +ENV PORT=8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD python -c "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()" + +# Run the ASGI server +CMD ["uvicorn", "asgi_app:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..2b6b125 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: uvicorn asgi_app:app --host 0.0.0.0 --port $PORT \ No newline at end of file diff --git a/README.md b/README.md index 8f658b7..98bff84 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,32 @@ Bedesten API Bedesten API Dual/Triple API Norm+Bireysel API - Kesin arama: `"\"mülkiyet kararı\""` (tam cümle olarak) - Daha kesin sonuçlar için hukuki terimler ve kavramlar +--- + +🌐 **Web Service / ASGI Deployment** + +Yargı MCP artık web servisi olarak da çalıştırılabilir! ASGI desteği sayesinde: + +- **Web API olarak erişim**: HTTP endpoint'leri üzerinden MCP araçlarına erişim +- **Cloud deployment**: Heroku, Railway, Google Cloud Run, AWS Lambda desteği +- **Docker desteği**: Production-ready Docker container +- **FastAPI entegrasyonu**: REST API ve interaktif dokümantasyon + +**Hızlı başlangıç:** +```bash +# ASGI dependencies yükle +pip install yargi-mcp[asgi] + +# Web servisi olarak başlat +python run_asgi.py +# veya +uvicorn asgi_app:app --host 0.0.0.0 --port 8000 +``` + +Detaylı deployment rehberi için: [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) + +--- + 📜 **Lisans** Bu proje MIT Lisansı altında lisanslanmıştır. Detaylar için `LICENSE` dosyasına bakınız. diff --git a/asgi_app.py b/asgi_app.py new file mode 100644 index 0000000..1f9aafa --- /dev/null +++ b/asgi_app.py @@ -0,0 +1,102 @@ +""" +ASGI application for Yargı MCP Server + +This module provides ASGI/HTTP access to the Yargı MCP server, +allowing it to be deployed as a web service. + +Usage: + uvicorn asgi_app:app --host 0.0.0.0 --port 8000 + +Or with custom transport: + uvicorn asgi_app:sse_app --host 0.0.0.0 --port 8000 +""" + +import os +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware +from starlette.requests import Request +from starlette.responses import JSONResponse, PlainTextResponse + +# Import the main MCP app +from mcp_server_main import app as mcp_server + +# Add a health check endpoint +@mcp_server.custom_route("/health", methods=["GET"]) +async def health_check(request: Request) -> JSONResponse: + """Health check endpoint for monitoring""" + return JSONResponse({ + "status": "healthy", + "service": "Yargı MCP Server", + "version": "0.1.0", + "tools_count": len(mcp_server._tool_manager.tools) + }) + +@mcp_server.custom_route("/", methods=["GET"]) +async def root(request: Request) -> JSONResponse: + """Root endpoint with service information""" + return JSONResponse({ + "service": "Yargı MCP Server", + "description": "MCP server for Turkish legal databases", + "endpoints": { + "mcp": "/mcp/", + "health": "/health", + "status": "/status" + }, + "supported_databases": [ + "Yargıtay (Court of Cassation)", + "Danıştay (Council of State)", + "Emsal (Precedent)", + "Uyuşmazlık Mahkemesi (Court of Jurisdictional Disputes)", + "Anayasa Mahkemesi (Constitutional Court)", + "Kamu İhale Kurulu (Public Procurement Authority)", + "Rekabet Kurumu (Competition Authority)", + "Bedesten API (Multiple courts)" + ] + }) + +@mcp_server.custom_route("/status", methods=["GET"]) +async def status(request: Request) -> JSONResponse: + """Status endpoint with detailed information""" + tools = [] + for tool in mcp_server._tool_manager.tools.values(): + tools.append({ + "name": tool.name, + "description": tool.description[:100] + "..." if len(tool.description) > 100 else tool.description + }) + + return JSONResponse({ + "status": "operational", + "tools": tools, + "total_tools": len(tools), + "transport": "streamable_http" + }) + +# Configure CORS middleware +cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",") +custom_middleware = [ + Middleware( + CORSMiddleware, + allow_origins=cors_origins, + allow_credentials=True, + allow_methods=["GET", "POST", "OPTIONS"], + allow_headers=["Content-Type", "Authorization", "X-Request-ID"], + ), +] + +# Create ASGI apps with different transports + +# Recommended: Streamable HTTP transport +app = mcp_server.http_app( + path="/mcp", + middleware=custom_middleware +) + +# Alternative: SSE transport (for compatibility) +sse_app = mcp_server.http_app( + path="/sse", + transport="sse", + middleware=custom_middleware +) + +# Export for uvicorn +__all__ = ["app", "sse_app"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d36eee7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +version: '3.8' + +services: + yargi-mcp: + build: . + image: yargi-mcp:latest + container_name: yargi-mcp-server + ports: + - "${PORT:-8000}:8000" + environment: + - HOST=0.0.0.0 + - PORT=8000 + - LOG_LEVEL=${LOG_LEVEL:-info} + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*} + - API_TOKEN=${API_TOKEN:-} + - PYTHONUNBUFFERED=1 + volumes: + # Mount logs directory + - ./logs:/app/logs + # Mount .env file if it exists + - ./.env:/app/.env:ro + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + networks: + - yargi-network + + # Optional: Nginx reverse proxy + nginx: + image: nginx:alpine + container_name: yargi-nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./ssl:/etc/nginx/ssl:ro + depends_on: + - yargi-mcp + networks: + - yargi-network + profiles: + - production + + # Optional: Redis for caching (future enhancement) + redis: + image: redis:alpine + container_name: yargi-redis + command: redis-server --appendonly yes + volumes: + - redis-data:/data + networks: + - yargi-network + profiles: + - with-cache + +networks: + yargi-network: + driver: bridge + +volumes: + redis-data: \ No newline at end of file diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..db5f361 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,428 @@ +# Yargı MCP Server Dağıtım Rehberi + +Bu rehber, Yargı MCP Server'ın ASGI web servisi olarak çeşitli dağıtım seçeneklerini kapsar. + +## İçindekiler + +- [Hızlı Başlangıç](#hızlı-başlangıç) +- [Yerel Geliştirme](#yerel-geliştirme) +- [Production Dağıtımı](#production-dağıtımı) +- [Cloud Dağıtımı](#cloud-dağıtımı) +- [Docker Dağıtımı](#docker-dağıtımı) +- [Güvenlik Hususları](#güvenlik-hususları) +- [İzleme](#izleme) + +## Hızlı Başlangıç + +### 1. Bağımlılıkları Yükleyin + +```bash +# ASGI sunucusu için uvicorn yükleyin +pip install uvicorn + +# Veya tüm bağımlılıklarla birlikte yükleyin +pip install -e . +pip install uvicorn +``` + +### 2. Sunucuyu Çalıştırın + +```bash +# Temel başlatma +python run_asgi.py + +# Veya doğrudan uvicorn ile +uvicorn asgi_app:app --host 0.0.0.0 --port 8000 +``` + +Sunucu şu adreslerde kullanılabilir olacak: +- MCP Endpoint: `http://localhost:8000/mcp/` +- Sağlık Kontrolü: `http://localhost:8000/health` +- API Durumu: `http://localhost:8000/status` + +## Yerel Geliştirme + +### Otomatik Yeniden Yükleme ile Geliştirme Sunucusu + +```bash +python run_asgi.py --reload --log-level debug +``` + +### FastAPI Entegrasyonunu Kullanma + +Ek REST API endpoint'leri için: + +```bash +uvicorn fastapi_app:app --reload +``` + +Bu şunları sağlar: +- `/docs` adresinde interaktif API dokümantasyonu +- `/api/tools` adresinde araç listesi +- `/api/databases` adresinde veritabanı bilgileri + +### Ortam Değişkenleri + +`.env.example` dosyasını temel alarak bir `.env` dosyası oluşturun: + +```bash +cp .env.example .env +``` + +Temel değişkenler: +- `HOST`: Sunucu host adresi (varsayılan: 127.0.0.1) +- `PORT`: Sunucu portu (varsayılan: 8000) +- `ALLOWED_ORIGINS`: CORS kökenleri (virgülle ayrılmış) +- `LOG_LEVEL`: Log seviyesi (debug, info, warning, error) + +## Production Dağıtımı + +### 1. Uvicorn ile Çoklu Worker Kullanımı + +```bash +python run_asgi.py --host 0.0.0.0 --port 8000 --workers 4 +``` + +### 2. Gunicorn Kullanımı + +```bash +pip install gunicorn +gunicorn asgi_app:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 +``` + +### 3. Nginx Reverse Proxy ile + +1. Nginx'i yükleyin +2. Sağlanan `nginx.conf` dosyasını kullanın: + +```bash +sudo cp nginx.conf /etc/nginx/sites-available/yargi-mcp +sudo ln -s /etc/nginx/sites-available/yargi-mcp /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl reload nginx +``` + +### 4. Systemd Servisi + +`/etc/systemd/system/yargi-mcp.service` dosyasını oluşturun: + +```ini +[Unit] +Description=Yargı MCP Server +After=network.target + +[Service] +Type=exec +User=www-data +WorkingDirectory=/opt/yargi-mcp +Environment="PATH=/opt/yargi-mcp/venv/bin" +ExecStart=/opt/yargi-mcp/venv/bin/uvicorn asgi_app:app --host 0.0.0.0 --port 8000 --workers 4 +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +Etkinleştirin ve başlatın: + +```bash +sudo systemctl enable yargi-mcp +sudo systemctl start yargi-mcp +``` + +## Cloud Dağıtımı + +### Heroku + +1. `Procfile` oluşturun: +``` +web: uvicorn asgi_app:app --host 0.0.0.0 --port $PORT +``` + +2. Dağıtın: +```bash +heroku create uygulama-isminiz +git push heroku main +``` + +### Railway + +1. `railway.json` ekleyin: +```json +{ + "build": { + "builder": "NIXPACKS" + }, + "deploy": { + "startCommand": "uvicorn asgi_app:app --host 0.0.0.0 --port $PORT" + } +} +``` + +2. Railway CLI veya GitHub entegrasyonu ile dağıtın + +### Google Cloud Run + +1. Container oluşturun: +```bash +docker build -t yargi-mcp . +docker tag yargi-mcp gcr.io/PROJE_ADINIZ/yargi-mcp +docker push gcr.io/PROJE_ADINIZ/yargi-mcp +``` + +2. Dağıtın: +```bash +gcloud run deploy yargi-mcp \ + --image gcr.io/PROJE_ADINIZ/yargi-mcp \ + --platform managed \ + --region us-central1 \ + --allow-unauthenticated +``` + +### AWS Lambda (Mangum kullanarak) + +1. Mangum'u yükleyin: +```bash +pip install mangum +``` + +2. `lambda_handler.py` oluşturun: +```python +from mangum import Mangum +from asgi_app import app + +handler = Mangum(app, lifespan="off") +``` + +3. AWS SAM veya Serverless Framework kullanarak dağıtın + +## Docker Dağıtımı + +### Tek Container + +```bash +# Oluşturun +docker build -t yargi-mcp . + +# Çalıştırın +docker run -p 8000:8000 --env-file .env yargi-mcp +``` + +### Docker Compose + +```bash +# Geliştirme +docker-compose up + +# Nginx ile Production +docker-compose --profile production up + +# Redis önbellekleme ile +docker-compose --profile with-cache up +``` + +### Kubernetes + +Deployment YAML oluşturun: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: yargi-mcp +spec: + replicas: 3 + selector: + matchLabels: + app: yargi-mcp + template: + metadata: + labels: + app: yargi-mcp + spec: + containers: + - name: yargi-mcp + image: yargi-mcp:latest + ports: + - containerPort: 8000 + env: + - name: HOST + value: "0.0.0.0" + - name: PORT + value: "8000" + livenessProbe: + httpGet: + path: /health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: yargi-mcp-service +spec: + selector: + app: yargi-mcp + ports: + - port: 80 + targetPort: 8000 + type: LoadBalancer +``` + +## Güvenlik Hususları + +### 1. Kimlik Doğrulama + +`API_TOKEN` ortam değişkenini ayarlayarak token kimlik doğrulamasını etkinleştirin: + +```bash +export API_TOKEN=gizli-token-degeri +``` + +Ardından isteklere ekleyin: +```bash +curl -H "Authorization: Bearer gizli-token-degeri" http://localhost:8000/api/tools +``` + +### 2. HTTPS/SSL + +Production için her zaman HTTPS kullanın: + +1. SSL sertifikası edinin (Let's Encrypt vb.) +2. Nginx veya cloud sağlayıcıda yapılandırın +3. `ALLOWED_ORIGINS` değerini https:// kullanacak şekilde güncelleyin + +### 3. Rate Limiting (Hız Sınırlama) + +Sağlanan Nginx yapılandırması rate limiting içerir: +- API endpoint'leri: 10 istek/saniye +- MCP endpoint: 100 istek/saniye + +### 4. CORS Yapılandırması + +Production için belirli kaynaklara izin verin: + +```bash +ALLOWED_ORIGINS=https://app.sizindomain.com,https://www.sizindomain.com +``` + +## İzleme + +### Sağlık Kontrolleri + +`/health` endpoint'ini izleyin: + +```bash +curl http://localhost:8000/health +``` + +Yanıt: +```json +{ + "status": "healthy", + "timestamp": "2024-12-26T10:00:00", + "uptime_seconds": 3600, + "tools_operational": true +} +``` + +### Loglama + +Ortam değişkeni ile log seviyesini yapılandırın: + +```bash +LOG_LEVEL=info # veya debug, warning, error +``` + +Loglar şuraya yazılır: +- Konsol (stdout) +- `logs/mcp_server.log` dosyası + +### Metrikler (Opsiyonel) + +OpenTelemetry desteği için: + +```bash +pip install opentelemetry-instrumentation-fastapi +``` + +Ortam değişkenlerini ayarlayın: +```bash +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 +OTEL_SERVICE_NAME=yargi-mcp-server +``` + +## Sorun Giderme + +### Port Zaten Kullanımda + +```bash +# 8000 portunu kullanan işlemi bulun +lsof -i :8000 + +# İşlemi sonlandırın +kill -9 +``` + +### İzin Hataları + +Dosya izinlerinin doğru olduğundan emin olun: + +```bash +chmod +x run_asgi.py +chown -R www-data:www-data /opt/yargi-mcp +``` + +### Bellek Sorunları + +Büyük belge işleme için worker belleğini artırın: + +```bash +# systemd servisinde +Environment="PYTHONMALLOC=malloc" +LimitNOFILE=65536 +``` + +### Zaman Aşımı Sorunları + +Zaman aşımlarını ayarlayın: +1. Uvicorn: `--timeout-keep-alive 75` +2. Nginx: `proxy_read_timeout 300s;` +3. Cloud sağlayıcılar: Platform özel zaman aşımı ayarlarını kontrol edin + +## Performans Ayarlama + +### 1. Worker İşlemleri + +- Geliştirme: 1 worker +- Production: CPU çekirdeği başına 2-4 worker + +### 2. Bağlantı Havuzlama + +Sunucu varsayılan olarak httpx ile bağlantı havuzlama kullanır. + +### 3. Önbellekleme (Gelecek Geliştirme) + +Redis önbellekleme docker-compose ile etkinleştirilebilir: + +```bash +docker-compose --profile with-cache up +``` + +### 4. Veritabanı Zaman Aşımları + +`.env` dosyasında veritabanı başına zaman aşımlarını ayarlayın: + +```bash +YARGITAY_TIMEOUT=60 +DANISTAY_TIMEOUT=60 +ANAYASA_TIMEOUT=90 +``` + +## Destek + +Sorunlar ve sorular için: +- GitHub Issues: https://github.com/saidsurucu/yargi-mcp/issues +- Dokümantasyon: README.md dosyasına bakın \ No newline at end of file diff --git a/fastapi_app.py b/fastapi_app.py new file mode 100644 index 0000000..b63d0fc --- /dev/null +++ b/fastapi_app.py @@ -0,0 +1,274 @@ +""" +FastAPI integration for Yargı MCP Server + +This module demonstrates how to integrate the Yargı MCP server +with a FastAPI application, providing additional REST API endpoints +alongside the MCP functionality. + +Usage: + uvicorn fastapi_app:app --host 0.0.0.0 --port 8000 +""" + +import os +from typing import List, Dict, Any, Optional +from datetime import datetime + +from fastapi import FastAPI, HTTPException, Query, Depends +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from pydantic import BaseModel + +# Import the main MCP app +from mcp_server_main import app as mcp_server +from asgi_app import custom_middleware + +# Create MCP ASGI app +mcp_asgi_app = mcp_server.http_app(path="/mcp") + +# Create FastAPI app with MCP lifespan +app = FastAPI( + title="Yargı MCP API", + description="Turkish Legal Database MCP Server with REST API", + version="0.1.0", + lifespan=mcp_asgi_app.lifespan +) + +# Add CORS middleware +cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",") +app.add_middleware( + CORSMiddleware, + allow_origins=cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Mount MCP server +app.mount("/mcp-server", mcp_asgi_app) + +# Response models +class ToolInfo(BaseModel): + name: str + description: str + parameters: Dict[str, Any] + +class ServerInfo(BaseModel): + name: str + version: str + description: str + tools_count: int + databases: List[str] + mcp_endpoint: str + api_docs: str + +class HealthCheck(BaseModel): + status: str + timestamp: datetime + uptime_seconds: Optional[float] = None + tools_operational: bool + +# Track server start time +SERVER_START_TIME = datetime.now() + +@app.get("/", response_model=ServerInfo) +async def root(): + """Get server information""" + return ServerInfo( + name="Yargı MCP Server", + version="0.1.0", + description="MCP server for Turkish legal databases", + tools_count=len(mcp_server._tool_manager.tools), + databases=[ + "Yargıtay (Court of Cassation)", + "Danıştay (Council of State)", + "Emsal (Precedent)", + "Uyuşmazlık Mahkemesi (Court of Jurisdictional Disputes)", + "Anayasa Mahkemesi (Constitutional Court)", + "Kamu İhale Kurulu (Public Procurement Authority)", + "Rekabet Kurumu (Competition Authority)", + "Bedesten API (Multiple courts)" + ], + mcp_endpoint="/mcp-server/mcp/", + api_docs="/docs" + ) + +@app.get("/health", response_model=HealthCheck) +async def health_check(): + """Health check endpoint""" + uptime = (datetime.now() - SERVER_START_TIME).total_seconds() + + return HealthCheck( + status="healthy", + timestamp=datetime.now(), + uptime_seconds=uptime, + tools_operational=len(mcp_server._tool_manager.tools) > 0 + ) + +@app.get("/api/tools", response_model=List[ToolInfo]) +async def list_tools( + search: Optional[str] = Query(None, description="Search tools by name or description"), + database: Optional[str] = Query(None, description="Filter by database name") +): + """List all available MCP tools""" + tools = [] + + for tool in mcp_server._tool_manager.tools.values(): + # Apply filters if provided + if search and search.lower() not in tool.name.lower() and search.lower() not in tool.description.lower(): + continue + + if database: + db_lower = database.lower() + if db_lower not in tool.name.lower() and db_lower not in tool.description.lower(): + continue + + # Extract parameter schema + params = {} + if hasattr(tool, 'schema') and tool.schema: + if hasattr(tool.schema, 'parameters'): + params = tool.schema.parameters + elif hasattr(tool.schema, '__annotations__'): + params = {k: str(v) for k, v in tool.schema.__annotations__.items()} + + tools.append(ToolInfo( + name=tool.name, + description=tool.description, + parameters=params + )) + + return tools + +@app.get("/api/tools/{tool_name}", response_model=ToolInfo) +async def get_tool(tool_name: str): + """Get detailed information about a specific tool""" + tool = mcp_server._tool_manager.tools.get(tool_name) + + if not tool: + raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found") + + # Extract parameter schema + params = {} + if hasattr(tool, 'schema') and tool.schema: + if hasattr(tool.schema, 'parameters'): + params = tool.schema.parameters + elif hasattr(tool.schema, '__annotations__'): + params = {k: str(v) for k, v in tool.schema.__annotations__.items()} + + return ToolInfo( + name=tool.name, + description=tool.description, + parameters=params + ) + +@app.get("/api/databases") +async def list_databases(): + """List all supported legal databases""" + databases = { + "yargitay": { + "name": "Yargıtay (Court of Cassation)", + "description": "Supreme court for civil and criminal cases", + "tools": ["search_yargitay_detailed", "get_yargitay_document_markdown", + "search_yargitay_bedesten", "get_yargitay_bedesten_document_markdown"], + "chambers": 52 + }, + "danistay": { + "name": "Danıştay (Council of State)", + "description": "Supreme administrative court", + "tools": ["search_danistay_by_keyword", "search_danistay_detailed", + "get_danistay_document_markdown", "search_danistay_bedesten", + "get_danistay_bedesten_document_markdown"], + "chambers": 27 + }, + "emsal": { + "name": "Emsal (Precedent)", + "description": "Precedent decisions from various courts", + "tools": ["search_emsal_detailed_decisions", "get_emsal_document_markdown"] + }, + "uyusmazlik": { + "name": "Uyuşmazlık Mahkemesi", + "description": "Court of Jurisdictional Disputes", + "tools": ["search_uyusmazlik_decisions", "get_uyusmazlik_document_markdown_from_url"] + }, + "anayasa": { + "name": "Anayasa Mahkemesi (Constitutional Court)", + "description": "Constitutional review and individual applications", + "tools": ["search_anayasa_norm_denetimi_decisions", + "get_anayasa_norm_denetimi_document_markdown", + "search_anayasa_bireysel_basvuru_report", + "get_anayasa_bireysel_basvuru_document_markdown"] + }, + "kik": { + "name": "Kamu İhale Kurulu", + "description": "Public Procurement Authority", + "tools": ["search_kik_decisions", "get_kik_document_markdown"] + }, + "rekabet": { + "name": "Rekabet Kurumu", + "description": "Competition Authority", + "tools": ["search_rekabet_kurumu_decisions", "get_rekabet_kurumu_document"] + }, + "bedesten": { + "name": "Bedesten API", + "description": "Unified API for multiple courts", + "tools": ["search_yerel_hukuk_bedesten", "get_yerel_hukuk_bedesten_document_markdown", + "search_istinaf_hukuk_bedesten", "get_istinaf_hukuk_bedesten_document_markdown", + "search_kyb_bedesten", "get_kyb_bedesten_document_markdown"] + } + } + + return JSONResponse(content=databases) + +@app.get("/api/stats") +async def get_statistics(): + """Get server statistics""" + uptime = (datetime.now() - SERVER_START_TIME).total_seconds() + + # Count tools by database + tool_counts = {} + for tool in mcp_server._tool_manager.tools.values(): + for db in ["yargitay", "danistay", "emsal", "uyusmazlik", "anayasa", "kik", "rekabet", "bedesten"]: + if db in tool.name.lower(): + tool_counts[db] = tool_counts.get(db, 0) + 1 + break + + return JSONResponse({ + "server": { + "uptime_seconds": uptime, + "start_time": SERVER_START_TIME.isoformat(), + "version": "0.1.0" + }, + "tools": { + "total": len(mcp_server._tool_manager.tools), + "by_database": tool_counts + }, + "capabilities": { + "total_chambers": 79, # 52 Yargıtay + 27 Danıştay + "date_filtering": True, + "exact_phrase_search": True, + "dual_api_support": True + } + }) + +# Add a simple authentication example (optional) +# Uncomment to enable basic token authentication +""" +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + +security = HTTPBearer() + +async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): + token = credentials.credentials + expected_token = os.getenv("API_TOKEN") + + if expected_token and token != expected_token: + raise HTTPException(status_code=401, detail="Invalid authentication token") + + return token + +# Then add Depends(verify_token) to any endpoint that needs protection +# Example: async def list_tools(..., token: str = Depends(verify_token)): +""" + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..5cb39b2 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,94 @@ +events { + worker_connections 1024; +} + +http { + upstream yargi_mcp { + server yargi-mcp:8000; + } + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=mcp_limit:10m rate=100r/s; + + server { + listen 80; + server_name localhost; + + # Redirect HTTP to HTTPS in production + # return 301 https://$server_name$request_uri; + + # Security headers + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options DENY; + add_header X-XSS-Protection "1; mode=block"; + add_header Referrer-Policy "strict-origin-when-cross-origin"; + + # API endpoints + location /api/ { + limit_req zone=api_limit burst=20 nodelay; + + proxy_pass http://yargi_mcp; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # MCP endpoint (higher rate limit) + location /mcp-server/mcp/ { + limit_req zone=mcp_limit burst=50 nodelay; + + proxy_pass http://yargi_mcp; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Longer timeouts for MCP operations + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + + # Health check (no rate limit) + location /health { + proxy_pass http://yargi_mcp; + proxy_set_header Host $host; + } + + # Root and other paths + location / { + limit_req zone=api_limit burst=10 nodelay; + + proxy_pass http://yargi_mcp; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + + # SSL configuration (uncomment for production) + # server { + # listen 443 ssl http2; + # server_name your-domain.com; + # + # ssl_certificate /etc/nginx/ssl/cert.pem; + # ssl_certificate_key /etc/nginx/ssl/key.pem; + # ssl_protocols TLSv1.2 TLSv1.3; + # ssl_ciphers HIGH:!aNULL:!MD5; + # + # # Include all location blocks from above + # } +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f8031f6..c89502d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,20 @@ dependencies = [ "pypdf>=5.5.0", ] +[project.optional-dependencies] +asgi = [ + "uvicorn[standard]>=0.30.0", + "starlette>=0.37.0", +] +api = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.30.0", +] +production = [ + "gunicorn>=22.0.0", + "uvicorn[standard]>=0.30.0", +] + [project.scripts] yargi-mcp = "mcp_server_main:main" diff --git a/railway.json b/railway.json new file mode 100644 index 0000000..0c54927 --- /dev/null +++ b/railway.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://railway.app/railway.schema.json", + "build": { + "builder": "NIXPACKS", + "buildCommand": "pip install -e .[asgi]" + }, + "deploy": { + "startCommand": "uvicorn asgi_app:app --host 0.0.0.0 --port $PORT", + "healthcheckPath": "/health", + "healthcheckTimeout": 30, + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 3 + }, + "variables": { + "ALLOWED_ORIGINS": "*", + "LOG_LEVEL": "info" + } +} \ No newline at end of file diff --git a/run_asgi.py b/run_asgi.py new file mode 100644 index 0000000..30ef9f3 --- /dev/null +++ b/run_asgi.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +""" +Standalone ASGI server runner for Yargı MCP + +This script provides a simple way to run the Yargı MCP server +as a web service using uvicorn. + +Usage: + python run_asgi.py + python run_asgi.py --host 0.0.0.0 --port 8080 + python run_asgi.py --reload # For development +""" + +import os +import sys +import argparse +import logging +from pathlib import Path + +# Add project root to Python path +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import uvicorn +except ImportError: + print("Error: uvicorn is not installed.") + print("Please install it with: pip install uvicorn") + sys.exit(1) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +def main(): + parser = argparse.ArgumentParser( + description="Run Yargı MCP server as an ASGI web service" + ) + parser.add_argument( + "--host", + type=str, + default=os.getenv("HOST", "127.0.0.1"), + help="Host to bind to (default: 127.0.0.1)" + ) + parser.add_argument( + "--port", + type=int, + default=int(os.getenv("PORT", "8000")), + help="Port to bind to (default: 8000)" + ) + parser.add_argument( + "--reload", + action="store_true", + help="Enable auto-reload for development" + ) + parser.add_argument( + "--transport", + choices=["http", "sse"], + default="http", + help="Transport type (default: http)" + ) + parser.add_argument( + "--log-level", + choices=["debug", "info", "warning", "error"], + default=os.getenv("LOG_LEVEL", "info").lower(), + help="Log level (default: info)" + ) + parser.add_argument( + "--workers", + type=int, + default=1, + help="Number of worker processes (default: 1)" + ) + + args = parser.parse_args() + + # Select app based on transport + app_name = "asgi_app:app" if args.transport == "http" else "asgi_app:sse_app" + + # Configure uvicorn + config = { + "app": app_name, + "host": args.host, + "port": args.port, + "log_level": args.log_level, + "reload": args.reload, + "access_log": True, + } + + # Add workers only if not in reload mode + if not args.reload and args.workers > 1: + config["workers"] = args.workers + + # Print startup information + print(f"Starting Yargı MCP server...") + print(f"Host: {args.host}") + print(f"Port: {args.port}") + print(f"Transport: {args.transport}") + print(f"Log level: {args.log_level}") + if args.reload: + print("Auto-reload: enabled") + else: + print(f"Workers: {args.workers}") + print(f"\nServer will be available at: http://{args.host}:{args.port}") + print(f"MCP endpoint: http://{args.host}:{args.port}/mcp/") + print(f"Health check: http://{args.host}:{args.port}/health") + print(f"API status: http://{args.host}:{args.port}/status") + print("\nPress CTRL+C to stop the server\n") + + # Run uvicorn + try: + uvicorn.run(**config) + except KeyboardInterrupt: + print("\nShutting down server...") + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/starlette_app.py b/starlette_app.py new file mode 100644 index 0000000..8310131 --- /dev/null +++ b/starlette_app.py @@ -0,0 +1,159 @@ +""" +Starlette integration example for Yargı MCP Server + +This module demonstrates how to integrate the Yargı MCP server +with a Starlette application, including authentication middleware +and custom routing. + +Usage: + uvicorn starlette_app:app --host 0.0.0.0 --port 8000 +""" + +import os +from starlette.applications import Starlette +from starlette.routing import Mount, Route +from starlette.requests import Request +from starlette.responses import JSONResponse, PlainTextResponse, RedirectResponse +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware +from starlette.middleware.authentication import AuthenticationMiddleware +from starlette.authentication import ( + AuthenticationBackend, AuthCredentials, SimpleUser, AuthenticationError +) + +# Import the main MCP app +from mcp_server_main import app as mcp_server + +# Simple token authentication backend +class TokenAuthBackend(AuthenticationBackend): + async def authenticate(self, request): + auth_header = request.headers.get("Authorization") + expected_token = os.getenv("API_TOKEN") + + # Skip auth for health check and public endpoints + if request.url.path in ["/health", "/", "/login"]: + return None + + if not expected_token: + # No token configured, allow all + return AuthCredentials(["authenticated"]), SimpleUser("anonymous") + + if not auth_header: + raise AuthenticationError("Authorization header required") + + try: + scheme, token = auth_header.split() + if scheme.lower() != "bearer": + raise AuthenticationError("Invalid authentication scheme") + + if token != expected_token: + raise AuthenticationError("Invalid token") + + return AuthCredentials(["authenticated"]), SimpleUser("user") + except ValueError: + raise AuthenticationError("Invalid authorization header format") + +# Homepage +async def homepage(request: Request): + return JSONResponse({ + "service": "Yargı MCP Server", + "version": "0.1.0", + "endpoints": { + "mcp": "/mcp-server/mcp/", + "api": "/api/", + "health": "/health" + } + }) + +# API info endpoint +async def api_info(request: Request): + if not request.user.is_authenticated: + return JSONResponse({"error": "Authentication required"}, status_code=401) + + return JSONResponse({ + "authenticated_as": request.user.display_name, + "available_tools": len(mcp_server._tool_manager.tools), + "databases": [ + "Yargıtay", "Danıştay", "Emsal", "Uyuşmazlık", + "Anayasa", "KIK", "Rekabet", "Bedesten" + ] + }) + +# Health check +async def health_check(request: Request): + return JSONResponse({ + "status": "healthy", + "service": "Yargı MCP Server" + }) + +# Login example (returns token for demo) +async def login(request: Request): + token = os.getenv("API_TOKEN", "demo-token") + return JSONResponse({ + "message": "Use this token in Authorization header", + "example": f"Authorization: Bearer {token}", + "note": "Set API_TOKEN environment variable to change token" + }) + +# Create MCP ASGI app +mcp_app = mcp_server.http_app(path='/mcp') + +# Configure middleware +middleware = [ + Middleware( + CORSMiddleware, + allow_origins=os.getenv("ALLOWED_ORIGINS", "*").split(","), + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ), + Middleware(AuthenticationMiddleware, backend=TokenAuthBackend()), +] + +# Create routes +routes = [ + Route("/", homepage), + Route("/health", health_check), + Route("/login", login), + Route("/api/info", api_info), + Mount("/mcp-server", app=mcp_app), +] + +# Create Starlette app +app = Starlette( + routes=routes, + middleware=middleware, + lifespan=mcp_app.lifespan +) + +# Nested mount example +def create_nested_app(): + """Example of nested mounting for complex routing structures""" + + # Create inner app with MCP + inner_app = Starlette( + routes=[Mount("/services", app=mcp_app)], + middleware=middleware + ) + + # Create outer app + outer_app = Starlette( + routes=[ + Route("/", homepage), + Mount("/v1", app=inner_app), + ], + lifespan=mcp_app.lifespan + ) + + # MCP would be available at /v1/services/mcp/ + return outer_app + +# Export both apps +nested_app = create_nested_app() + +if __name__ == "__main__": + import uvicorn + print("Starting Starlette app with authentication...") + print("Set API_TOKEN environment variable to enable authentication") + print("Example: API_TOKEN=secret-token python starlette_app.py") + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file