diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d2e5c8f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,90 @@ +# Git files +.git/ +.gitignore +.github/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +venv/ +env/ +ENV/ +.venv/ + +# IDEs and editors +.vscode/ +.cursor/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Testing +.pytest_cache/ +.coverage +.coverage.* +htmlcov/ +.tox/ +.nox/ +coverage.xml +*.cover +.hypothesis/ + +# Documentation (not needed in container) +*.md +!README.md +docs/ + +# Environment files +.env +.env.* +*.env + +# CI/CD +.github/ +.gitlab-ci.yml +.travis.yml + +# Test files (not needed in production) +tests/ +test_*.py +*_test.py + +# Development files +requirements-dev.txt +pyproject.toml +setup.py +setup.cfg + +# Logs and temp files +*.log +*.tmp +tmp/ +temp/ + +# Docker files (avoid recursive copies) +Dockerfile* +docker-compose*.yml +.dockerignore diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 093a764..d7ed358 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -94,6 +94,155 @@ deals = client.find_recent_deals_for_address("תל אביב רוטשילד 1", y print(f"Found {len(deals)} deals") ``` +### Option 4: Cloud Deployment (HTTP) + +Deploy Nadlan-MCP as an HTTP service to cloud platforms like Render, Railway, or using Docker. + +#### Prerequisites + +- Docker installed (for Docker deployment) +- Render/Railway account (for cloud deployment) +- Git repository (for cloud deployment) + +#### 4.1: Render Deployment + +**Step 1:** Push your code to a Git repository (GitHub, GitLab, etc.) + +**Step 2:** Create a new Web Service on Render: +- Go to https://dashboard.render.com +- Click "New +" → "Web Service" +- Connect your Git repository +- Configure: + - **Name:** `nadlan-mcp` (or your preferred name) + - **Environment:** `Docker` + - **Region:** Choose closest to your users + - **Branch:** `main` (or your default branch) + - **Build Command:** (leave empty - Docker handles this) + - **Start Command:** (leave empty - Docker CMD is used) + +**Step 3:** Configure Environment Variables (optional): + +In Render dashboard, add environment variables: +``` +GOVMAP_MAX_RETRIES=3 +GOVMAP_REQUESTS_PER_SECOND=5.0 +GOVMAP_DEFAULT_YEARS_BACK=2 +``` + +**Step 4:** Deploy +- Click "Create Web Service" +- Render will automatically build and deploy your Docker container +- Wait for deployment to complete (~2-5 minutes) + +**Step 5:** Access Your Service +- Your service will be available at: `https://your-service-name.onrender.com` +- MCP endpoint: `https://your-service-name.onrender.com/mcp` +- Health check: `https://your-service-name.onrender.com/health` + +**Important Notes:** +- Render's free tier may have cold starts (delays when service is idle) +- For production, use a paid plan for better performance +- The HTTP server runs on the port specified by Render's `PORT` environment variable + +#### 4.2: Docker Deployment + +**Build the Docker Image:** + +```bash +docker build -t nadlan-mcp . +``` + +**Run Locally:** + +```bash +# Run on default port 8000 +docker run -p 8000:8000 nadlan-mcp + +# Run on custom port +docker run -p 8080:8080 -e PORT=8080 nadlan-mcp + +# Run with environment variables +docker run -p 8000:8000 \ + -e GOVMAP_MAX_RETRIES=5 \ + -e GOVMAP_REQUESTS_PER_SECOND=3.0 \ + nadlan-mcp +``` + +**Test the Deployment:** + +```bash +# Check health endpoint +curl http://localhost:8000/health + +# Expected response: +# {"status":"ok","service":"nadlan-mcp"} +``` + +**Push to Docker Registry (Optional):** + +```bash +# Tag for Docker Hub +docker tag nadlan-mcp your-username/nadlan-mcp:latest + +# Push to Docker Hub +docker push your-username/nadlan-mcp:latest + +# Or use GitHub Container Registry +docker tag nadlan-mcp ghcr.io/your-username/nadlan-mcp:latest +docker push ghcr.io/your-username/nadlan-mcp:latest +``` + +#### 4.3: Railway Deployment + +**Step 1:** Install Railway CLI (optional) or use web dashboard + +```bash +npm install -g @railway/cli +railway login +``` + +**Step 2:** Deploy from CLI: + +```bash +railway init +railway up +``` + +**Or via Web Dashboard:** +- Go to https://railway.app +- Click "New Project" → "Deploy from GitHub repo" +- Select your repository +- Railway auto-detects Dockerfile and deploys + +**Step 3:** Configure Environment Variables + +In Railway dashboard, add variables as needed (see Configuration section below) + +**Step 4:** Access Your Service +- Railway provides a public URL +- MCP endpoint: `https://your-service.railway.app/mcp` +- Health check: `https://your-service.railway.app/health` + +#### 4.4: Other Cloud Platforms + +The HTTP server can be deployed to any platform that supports: +- Docker containers +- Python applications +- Port binding via `PORT` environment variable + +**Supported Platforms:** +- **Google Cloud Run** - Serverless container deployment +- **AWS ECS/Fargate** - Container orchestration +- **Azure Container Instances** - Container deployment +- **DigitalOcean App Platform** - PaaS deployment +- **Heroku** - Dyno-based deployment + +**Deployment Pattern:** +1. Use the provided `Dockerfile` +2. Set `PORT` environment variable (if not auto-set by platform) +3. Configure health check to `GET /health` +4. Deploy and access at `https://your-domain.com/mcp` + ## Configuration ### Environment Variables diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..18714a7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +# Dockerfile for Nadlan-MCP HTTP Server +# Supports deployment to Render, Railway, Docker, and other container platforms + +FROM python:3.13-slim + +# Set environment variables +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Set working directory +WORKDIR /app + +# Install system dependencies if needed +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better layer caching +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY nadlan_mcp/ ./nadlan_mcp/ +COPY run_http_server.py . +COPY README.md . + +# Create non-root user for security +RUN useradd -m -u 1000 appuser && \ + chown -R appuser:appuser /app +USER appuser + +# Expose port (will be overridden by PORT env var in production) +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:${PORT:-8000}/health')" || exit 1 + +# Run the HTTP server +CMD ["python", "run_http_server.py"] diff --git a/README.md b/README.md index 5054541..bd602a2 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,46 @@ You can also run the server directly: python -m nadlan_mcp.simple_fastmcp_server ``` +#### 5. HTTP Server (For Cloud Deployment) + +**NEW: HTTP transport for deploying to Render, Railway, Docker, and other cloud platforms** + +For production deployment to cloud platforms, use the HTTP server: + +```bash +python run_http_server.py +``` + +This starts the FastMCP server with HTTP transport, listening on port 8000 by default (configurable via PORT environment variable). + +**Using Docker:** + +Build and run the container: + +```bash +# Build the Docker image +docker build -t nadlan-mcp . + +# Run the container +docker run -p 8000:8000 nadlan-mcp + +# Or with custom port +docker run -p 8080:8080 -e PORT=8080 nadlan-mcp +``` + +**Environment Variables:** + +- `PORT` - HTTP server port (default: 8000) +- `HOST` - Bind address (default: 0.0.0.0) +- See DEPLOYMENT.md for all Govmap API configuration options + +**Endpoints:** + +- `http://localhost:8000/mcp` - MCP protocol endpoint +- `http://localhost:8000/health` - Health check endpoint + +For detailed deployment instructions to Render, Railway, or other platforms, see [DEPLOYMENT.md](DEPLOYMENT.md). + #### MCP Client Configuration To connect to the server from MCP clients, use the following configuration: diff --git a/nadlan_mcp/fastmcp_server.py b/nadlan_mcp/fastmcp_server.py index 508bd9a..c3de75f 100644 --- a/nadlan_mcp/fastmcp_server.py +++ b/nadlan_mcp/fastmcp_server.py @@ -11,6 +11,7 @@ import logging from typing import Any, Dict, List, Optional from mcp.server.fastmcp import FastMCP +from starlette.responses import JSONResponse from nadlan_mcp.govmap import GovmapClient from nadlan_mcp.govmap.models import Deal @@ -966,6 +967,18 @@ def get_market_activity_metrics(address: str, years_back: int = 2, radius_meters return f"Error analyzing market activity: {str(e)}" +# Health check endpoint for HTTP deployments +@mcp.custom_route("/health", methods=["GET"]) +async def health_check(request): + """ + Health check endpoint for container orchestration platforms (Render, Railway, etc.). + + Returns a simple OK status to indicate the server is running. + This endpoint is accessible at GET /health when using HTTP transport. + """ + return JSONResponse({"status": "ok", "service": "nadlan-mcp"}) + + # Run the server if __name__ == "__main__": mcp.run() diff --git a/requirements.txt b/requirements.txt index bc804c2..ebd25bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ python-dotenv>=1.0.0,<2.0.0 mcp>=1.0.0,<2.0.0 fastmcp>=0.1.0,<1.0.0 pydantic>=2.0.0,<3.0.0 +uvicorn>=0.27.0,<1.0.0 diff --git a/run_http_server.py b/run_http_server.py new file mode 100644 index 0000000..43ff8f7 --- /dev/null +++ b/run_http_server.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +HTTP Server Entry Point for Nadlan-MCP + +This module provides an HTTP/SSE transport server for the Nadlan-MCP service, +enabling deployment to cloud platforms like Render, Railway, or Docker containers. + +For local Claude Desktop integration, use run_fastmcp_server.py (stdio transport) instead. +""" + +import os +import sys +import logging +import uvicorn +from nadlan_mcp.fastmcp_server import mcp + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +def main(): + """Run the FastMCP server with HTTP transport using uvicorn.""" + # Get port from environment variable (Render sets PORT automatically) + port = int(os.environ.get("PORT", 8000)) + host = "0.0.0.0" + + logger.info("=" * 60) + logger.info("Starting Nadlan-MCP HTTP Server") + logger.info("=" * 60) + logger.info(f"Transport: HTTP (via uvicorn)") + logger.info(f"Host: {host}") + logger.info(f"Port: {port}") + logger.info(f"MCP Endpoint: http://{host}:{port}/mcp") + logger.info(f"Health Check: http://{host}:{port}/health") + logger.info("=" * 60) + + try: + # Get the HTTP ASGI app from FastMCP + # Try different possible method names based on FastMCP version + if hasattr(mcp, 'streamable_http_app'): + app = mcp.streamable_http_app() + elif hasattr(mcp, 'http_app'): + app = mcp.http_app() + elif hasattr(mcp, 'get_app'): + app = mcp.get_app() + else: + # Fallback: try to access the app directly + app = getattr(mcp, 'app', None) + if app is None: + raise AttributeError( + "FastMCP instance has no HTTP app method. " + "Available methods: " + ", ".join(dir(mcp)) + ) + + logger.info(f"Using app: {type(app).__name__}") + + # Run with uvicorn + uvicorn.run( + app, + host=host, + port=port, + log_level="info" + ) + except Exception as e: + logger.error(f"Failed to start HTTP server: {e}", exc_info=True) + sys.exit(1) + + +if __name__ == "__main__": + main()