diff --git a/Docker/deployment.md b/Docker/deployment.md index 920ff4a1..651656fd 100644 --- a/Docker/deployment.md +++ b/Docker/deployment.md @@ -1,10 +1,11 @@ # Optics Framework API Deployment Guide -This guide outlines how to deploy the Optics API using Docker in two modes: +This guide outlines how to deploy the Optics API and MCP server using Docker: -1. **Production**: Uses the published `optics-framework` from PyPI (see `prod/Dockerfile`) -2. **Development**: Uses a locally built `.whl` package from the Poetry-managed `optics-framework` source (see `dev/Dockerfile`) +1. **Production (REST API)**: Uses the published `optics-framework` from PyPI (see `prod/Dockerfile`) +2. **Development (REST API)**: Uses a locally built `.whl` package from the Poetry-managed `optics-framework` source (see `dev/Dockerfile`) +3. **MCP (HTTP transport)**: Uses separate images under `mcp/` that install `optics-framework[mcp]` and run `optics mcp --transport http` on port **8090** --- @@ -100,3 +101,101 @@ If running Appium on your host machine, use this URL in your config: ``` appium_url: "http://host.docker.internal:4723" ``` + +--- + +## ✅ Deployment Mode 3: MCP Server (HTTP transport) + +The MCP images expose the optics keyword engine over the [Model Context Protocol](https://modelcontextprotocol.io) at `http://:8090/mcp`. Containerized MCP always uses **HTTP transport** (not stdio). Sessions are **not shared** with `optics serve` even if both containers run. + +### 📁 Folder Structure +``` +mcp/ +├── prod/ +│ └── Dockerfile +└── dev/ + └── Dockerfile +``` + +### Production MCP (PyPI) + +#### Build +```sh +cd /path/to/optics-framework +docker build -f Docker/mcp/prod/Dockerfile -t optics-mcp-prod . +``` + +#### Run +```sh +docker run -d -p 8090:8090 --name optics-mcp-prod optics-mcp-prod +``` + +#### Vision Backend Selection +Same as the REST API images: use `--build-arg VISION_BACKEND=...` (`easyocr`, `google-vision`, or `pytesseract`). + +Example (Google Vision): +```sh +docker build -f Docker/mcp/prod/Dockerfile \ + --build-arg VISION_BACKEND=google-vision \ + -t optics-mcp-prod . +``` + +If using Google Vision, mount your service account JSON and set the env variable: +```sh +docker run -d -p 8090:8090 \ + -e GOOGLE_APPLICATION_CREDENTIALS=/app/service-account.json \ + -v /path/to/service-account.json:/app/service-account.json \ + --name optics-mcp-prod optics-mcp-prod +``` + +### Development MCP (Local .whl) + +Build the wheel first (`poetry build` from the repo root), then: + +```sh +docker build -f Docker/mcp/dev/Dockerfile \ + --build-arg WHL_FILE=optics_framework-0.x.x-py3-none-any.whl \ + -t optics-mcp-dev . +``` + +```sh +docker run -d -p 8091:8090 --name optics-mcp-dev optics-mcp-dev +``` + +### Docker Compose + +From the repo root: + +```sh +# Production MCP on host port 8090 +docker compose -f Docker/docker-compose.yml up --build mcp + +# Development MCP on host port 8091 +docker compose -f Docker/docker-compose.yml up --build mcp-dev +``` + +### Connect an MCP client + +Point your MCP client at the container's HTTP endpoint: + +```json +{ + "mcpServers": { + "optics": { "url": "http://127.0.0.1:8090/mcp" } + } +} +``` + +Use port **8091** when running the `mcp-dev` compose service. + +#### Appium from inside the container + +When `start_session` targets Appium on the host machine: + +```json +{ + "driver": "appium", + "url": "http://host.docker.internal:4723", + "capabilities": { "...": "..." } +} +``` diff --git a/Docker/dev/Dockerfile b/Docker/dev/Dockerfile index 6c0effae..e805404d 100644 --- a/Docker/dev/Dockerfile +++ b/Docker/dev/Dockerfile @@ -25,15 +25,23 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ curl \ libgl1 \ libglib2.0-0 \ + xvfb \ + dbus-x11 \ + tesseract-ocr \ && rm -rf /var/lib/apt/lists/* # Install common Python packages and optics-framework wheel and vision backend RUN pip install --no-cache-dir \ - appium-python-client \ - && pip install --no-cache-dir "/app/${WHL_FILE}" \ + appium-python-client playwright \ + && playwright install-deps chromium firefox \ + && WHEEL=$(if [ -n "$WHL_FILE" ]; then echo "/app/${WHL_FILE}"; else ls /app/*.whl 2>/dev/null | head -1; fi) \ + && if [ -z "$WHEEL" ] || [ ! -f "$WHEEL" ]; then \ + echo "ERROR: No wheel in dist/. Run 'poetry build' first or set WHL_FILE to the .whl filename."; \ + exit 1; \ + fi \ + && pip install --no-cache-dir "$WHEEL" \ && if [ "$VISION_BACKEND" = "easyocr" ]; then \ - pip install --no-cache-dir easyocr && \ - python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \ + pip install --no-cache-dir easyocr; \ elif [ "$VISION_BACKEND" = "google-vision" ]; then \ pip install --no-cache-dir google-cloud-vision; \ elif [ "$VISION_BACKEND" = "pytesseract" ]; then \ @@ -49,10 +57,22 @@ RUN chown -R appuser:appuser /app # Switch to non-root user USER appuser +# Pre-download EasyOCR models as appuser (must match runtime user's ~/.EasyOCR/) +RUN if [ "$VISION_BACKEND" = "easyocr" ]; then \ + python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \ + fi + +# Install Playwright browsers (Chromium and Firefox) as appuser (must be done as the user who will run the app) +RUN playwright install chromium firefox + EXPOSE 8000 # Allow dynamic worker count via UVICORN_WORKERS env variable (default 1) ENV UVICORN_WORKERS=1 +# Set DISPLAY for X11 (xvfb will provide virtual display) +ENV DISPLAY=:99 + # Use exec form for CMD with shell for env var substitution -CMD ["/bin/sh", "-c", "exec optics serve --host 0.0.0.0 --port 8000 --workers \"${UVICORN_WORKERS:-1}\""] +# Start xvfb in the background, then run optics serve +CMD ["/bin/sh", "-c", "Xvfb :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & exec optics serve --host 0.0.0.0 --port 8000 --workers \"${UVICORN_WORKERS:-1}\""] diff --git a/Docker/docker-compose.yml b/Docker/docker-compose.yml index 78362b8d..fc3a9589 100644 --- a/Docker/docker-compose.yml +++ b/Docker/docker-compose.yml @@ -45,7 +45,52 @@ services: timeout: 10s retries: 5 + mcp: + build: + context: .. + dockerfile: Docker/mcp/prod/Dockerfile + image: optics/mcp:prod + container_name: optics_mcp_prod + ports: + - "8090:8090" + restart: unless-stopped + environment: + - PYTHONUNBUFFERED=1 + healthcheck: + test: ["CMD-SHELL", "python -c \"import socket; s=socket.create_connection(('127.0.0.1',8090),2); s.close()\""] + interval: 45s + timeout: 10s + retries: 5 + deploy: + resources: + limits: + cpus: '1.0' + memory: 1024M + + mcp-dev: + build: + context: .. + dockerfile: Docker/mcp/dev/Dockerfile + args: + # pass WHL_FILE if you want to install from a specific wheel in dist/ + WHL_FILE: "" + image: optics/mcp:dev + container_name: optics_mcp_dev + ports: + - "8091:8090" # maps host 8091 -> container 8090 so prod MCP / mock OCR don't conflict + volumes: + - ..:/app:cached + environment: + - PYTHONUNBUFFERED=1 + healthcheck: + test: ["CMD-SHELL", "python -c \"import socket; s=socket.create_connection(('127.0.0.1',8090),2); s.close()\""] + interval: 45s + timeout: 10s + retries: 5 + # Usage examples: # docker compose -f Docker/docker-compose.yml up --build app # docker compose -f Docker/docker-compose.yml up --build dev +# docker compose -f Docker/docker-compose.yml up --build mcp +# docker compose -f Docker/docker-compose.yml up --build mcp-dev # docker compose -f Docker/docker-compose.yml up --build diff --git a/Docker/mcp/dev/Dockerfile b/Docker/mcp/dev/Dockerfile new file mode 100644 index 00000000..244dc54a --- /dev/null +++ b/Docker/mcp/dev/Dockerfile @@ -0,0 +1,77 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.12-slim + +# Create a non-root user +RUN useradd --create-home --shell /bin/bash appuser + +WORKDIR /app + +# Accept the .whl filename as a build argument +ARG WHL_FILE +ARG VISION_BACKEND=easyocr + +ENV VISION_BACKEND=${VISION_BACKEND} +ENV WHL_FILE=${WHL_FILE} + +# Copy wheel(s) from dist/ (empty WHL_FILE copies all of dist/, matching Docker/dev/Dockerfile) +COPY dist/${WHL_FILE} /app/ + + +# Install system dependencies (first layer for cache) +RUN apt-get update && apt-get install --no-install-recommends -y \ + build-essential \ + curl \ + libgl1 \ + libglib2.0-0 \ + xvfb \ + dbus-x11 \ + tesseract-ocr \ + && rm -rf /var/lib/apt/lists/* + +# Install common Python packages and optics-framework wheel with MCP extra and vision backend +RUN pip install --no-cache-dir \ + appium-python-client playwright \ + && playwright install-deps chromium firefox \ + && WHEEL=$(if [ -n "$WHL_FILE" ]; then echo "/app/${WHL_FILE}"; else ls /app/*.whl 2>/dev/null | head -1; fi) \ + && if [ -z "$WHEEL" ] || [ ! -f "$WHEEL" ]; then \ + echo "ERROR: No wheel in dist/. Run 'poetry build' first or set WHL_FILE to the .whl filename."; \ + exit 1; \ + fi \ + && pip install --no-cache-dir "${WHEEL}[mcp]" \ + && if [ "$VISION_BACKEND" = "easyocr" ]; then \ + pip install --no-cache-dir easyocr; \ + elif [ "$VISION_BACKEND" = "google-vision" ]; then \ + pip install --no-cache-dir google-cloud-vision; \ + elif [ "$VISION_BACKEND" = "pytesseract" ]; then \ + pip install --no-cache-dir pytesseract; \ + fi + +# If using google-vision, user must mount service account json and set GOOGLE_APPLICATION_CREDENTIALS +# Example: docker run -e GOOGLE_APPLICATION_CREDENTIALS=/app/service-account.json -v /path/to/service-account.json:/app/service-account.json ... + +# Set permissions +RUN chown -R appuser:appuser /app + +# Switch to non-root user +USER appuser + +# Pre-download EasyOCR models as appuser (must match runtime user's ~/.EasyOCR/) +RUN if [ "$VISION_BACKEND" = "easyocr" ]; then \ + python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \ + fi + +# Install Playwright browsers (Chromium and Firefox) as appuser (must be done as the user who will run the app) +RUN playwright install chromium firefox + +EXPOSE 8090 + +# Allow dynamic port via MCP_PORT env variable (default 8090) +ENV MCP_PORT=8090 + +# Set DISPLAY for X11 (xvfb will provide virtual display) +ENV DISPLAY=:99 + +# Use exec form for CMD with shell for env var substitution +# Start xvfb in the background, then run optics mcp over HTTP +CMD ["/bin/sh", "-c", "Xvfb :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & exec optics mcp --transport http --host 0.0.0.0 --port \"${MCP_PORT:-8090}\""] diff --git a/Docker/mcp/prod/Dockerfile b/Docker/mcp/prod/Dockerfile new file mode 100644 index 00000000..22e60e60 --- /dev/null +++ b/Docker/mcp/prod/Dockerfile @@ -0,0 +1,69 @@ +# syntax=docker/dockerfile:1 +FROM python:3.12-slim + +# Create a non-root user +RUN useradd --create-home --shell /bin/bash appuser + +# Set working directory +WORKDIR /app + +# Build arguments for vision backend +ARG VISION_BACKEND=easyocr +ENV VISION_BACKEND=${VISION_BACKEND} + + +# Install system dependencies (first layer for cache) +RUN apt-get update && apt-get install --no-install-recommends -y \ + build-essential \ + curl \ + libgl1 \ + libglib2.0-0 \ + xvfb \ + dbus-x11 \ + tesseract-ocr \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --no-cache-dir uv \ + && uv pip install --system appium-python-client playwright \ + && playwright install-deps chromium firefox + +# Install optics-framework with MCP extra and vision backend (late layer for cache efficiency) +RUN uv pip install --system "optics-framework[mcp]" \ + && if [ "$VISION_BACKEND" = "easyocr" ]; then \ + uv pip install --system easyocr; \ + elif [ "$VISION_BACKEND" = "google-vision" ]; then \ + uv pip install --system google-cloud-vision; \ + elif [ "$VISION_BACKEND" = "pytesseract" ]; then \ + uv pip install --system pytesseract; \ + fi + +# If using google-vision, user must mount service account json and set GOOGLE_APPLICATION_CREDENTIALS +# Example: docker run -e GOOGLE_APPLICATION_CREDENTIALS=/app/service-account.json -v /path/to/service-account.json:/app/service-account.json ... + +# Set permissions +RUN chown -R appuser:appuser /app + +# Switch to non-root user to install Playwright browsers in user's cache +USER appuser + +# Pre-download EasyOCR models as appuser (must match runtime user's ~/.EasyOCR/) +RUN if [ "$VISION_BACKEND" = "easyocr" ]; then \ + python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \ + fi + +# Install Playwright browsers (Chromium and Firefox) as appuser (must be done as the user who will run the app) +RUN playwright install chromium firefox + +# Note: We stay as appuser for the rest of the Dockerfile + +EXPOSE 8090 + +# Allow dynamic port via MCP_PORT env variable (default 8090) +ENV MCP_PORT=8090 + +# Set DISPLAY for X11 (xvfb will provide virtual display) +ENV DISPLAY=:99 + +# Use exec form for CMD with shell for env var substitution +# Start xvfb in the background, then run optics mcp over HTTP +CMD ["/bin/sh", "-c", "Xvfb :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & exec optics mcp --transport http --host 0.0.0.0 --port \"${MCP_PORT:-8090}\""] diff --git a/Docker/prod/Dockerfile b/Docker/prod/Dockerfile index fdde4fd3..0f663eb1 100644 --- a/Docker/prod/Dockerfile +++ b/Docker/prod/Dockerfile @@ -20,6 +20,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ libglib2.0-0 \ xvfb \ dbus-x11 \ + tesseract-ocr \ && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir uv \ @@ -29,8 +30,7 @@ RUN pip install --no-cache-dir uv \ # Install optics-framework and vision backend (late layer for cache efficiency) RUN uv pip install --system optics-framework \ && if [ "$VISION_BACKEND" = "easyocr" ]; then \ - uv pip install --system easyocr && \ - python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \ + uv pip install --system easyocr; \ elif [ "$VISION_BACKEND" = "google-vision" ]; then \ uv pip install --system google-cloud-vision; \ elif [ "$VISION_BACKEND" = "pytesseract" ]; then \ @@ -46,6 +46,11 @@ RUN chown -R appuser:appuser /app # Switch to non-root user to install Playwright browsers in user's cache USER appuser +# Pre-download EasyOCR models as appuser (must match runtime user's ~/.EasyOCR/) +RUN if [ "$VISION_BACKEND" = "easyocr" ]; then \ + python3 -c "import easyocr; easyocr.Reader(['en'], download_enabled=True)"; \ + fi + # Install Playwright browsers (Chromium and Firefox) as appuser (must be done as the user who will run the app) RUN playwright install chromium firefox diff --git a/docs/usage/mcp_usage.md b/docs/usage/mcp_usage.md index 3393289c..88a438df 100644 --- a/docs/usage/mcp_usage.md +++ b/docs/usage/mcp_usage.md @@ -59,6 +59,44 @@ optics mcp --transport http --host 127.0.0.1 --port 8090 | `--host` | `127.0.0.1` | bind host (http only) | | `--port` | `8090` | bind port (http only) | +### Docker + +Containerized MCP runs **HTTP transport** bound to `0.0.0.0:8090` (stdio is for +local clients that spawn the process). Images live under `Docker/mcp/` and +install the `[mcp]` extra (`fastmcp`). + +**Docker Compose** (from the repo root): + +```bash +# Production image (PyPI) — host port 8090 +docker compose -f Docker/docker-compose.yml up --build mcp + +# Development image (local .whl) — host port 8091 +docker compose -f Docker/docker-compose.yml up --build mcp-dev +``` + +**Standalone build/run:** + +```bash +docker build -f Docker/mcp/prod/Dockerfile -t optics-mcp-prod . +docker run -d -p 8090:8090 --name optics-mcp-prod optics-mcp-prod +``` + +Connect your MCP client to the container: + +```json +{ + "mcpServers": { + "optics": { "url": "http://127.0.0.1:8090/mcp" } + } +} +``` + +Use port **8091** for the `mcp-dev` compose service. When `start_session` +targets Appium on the host, set `"url": "http://host.docker.internal:4723"`. +See [`Docker/deployment.md`](../../Docker/deployment.md) for vision-backend +build args, Google Vision credential mounts, and dev-wheel builds. + ## 4. Connect an MCP client **stdio** — the client launches the server itself. Add to your client's MCP