From 28eb050b748b61d784b26d58a13face690351b2d Mon Sep 17 00:00:00 2001 From: root Date: Wed, 15 Jul 2026 20:04:31 +0100 Subject: [PATCH] Update README.md and correct ENV in server.py --- README.md | 53 +++++++++++++++++++++++++++++++++++++++-------------- server.py | 14 +++++++------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b737042..a62daf4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Turnstone MCP Server -A [Model Context Protocol](https://modelcontextprotocol.io/) server providing four sandboxed, production-ready tools for LLM-driven workflows. Built with **FastMCP** and **Playwright**, it exposes code execution, headless browser automation, and file-to-Markdown conversion over HTTP. +A [Model Context Protocol](https://modelcontextprotocol.io/) server providing four sandboxed tools for LLM-driven workflows. Built with **FastMCP** and **Playwright**, it exposes code execution, headless browser automation, and file-to-Markdown conversion over HTTP. + +Designed to run as a Docker container alongside the Turnstone platform, sharing the `turnstone_workspace` volume so tools can read and write files that other services in the ecosystem use. ## Tools @@ -13,20 +15,23 @@ A [Model Context Protocol](https://modelcontextprotocol.io/) server providing fo ## Quick Start -```bash -# Install dependencies -pip install fastmcp playwright markitdown -playwright install chromium +Run the server with Docker Compose: -# Run the server -python server.py +```bash +docker compose up --build ``` -The server starts on `http://0.0.0.0:8000`. The MCP health endpoint is available at `/health`. +The server starts on port **8000**. The MCP health endpoint is available at `http://localhost:8000/health`. + +### Running in the background + +```bash +docker compose up -d --build +``` ## Configuration -All limits are configurable via environment variables with sensible defaults: +All limits are configurable via environment variables with sensible defaults. Set them in your `.env` file or pass them through `docker compose`: | Variable | Default | Description | |---|---|---| @@ -36,12 +41,12 @@ All limits are configurable via environment variables with sensible defaults: | `PLAYWRIGHT_NAV_TIMEOUT` | `30000` | Browser navigation timeout (ms) | | `PLAYWRIGHT_TEXT_TIMEOUT` | `15000` | Page text extraction timeout (ms) | | `EXTRACT_MAX_FILE_SIZE` | `10485760` | Maximum file size for extraction (10 MB) | -| `WORKSPACE_ROOT` | `/workspace` | Base directory for file-relative operations | +| `WORKSPACE_MOUNT` | `workspace` | Docker volume name for the shared workspace | Example with custom limits: ```bash -PYTHON_EXEC_TIMEOUT=60 PYTHON_MAX_MEMORY_MB=512 python server.py +PYTHON_EXEC_TIMEOUT=60 PYTHON_MAX_MEMORY_MB=512 docker compose up --build ``` ## Architecture @@ -49,7 +54,7 @@ PYTHON_EXEC_TIMEOUT=60 PYTHON_MAX_MEMORY_MB=512 python server.py ``` ┌──────────────┐ HTTP/MCP ┌──────────────────┐ │ LLM Client │ ────────────────► │ Turnstone MCP │ -│ │ │ Server :8000 │ +│ │ │ Container :8000 │ └──────────────┘ └────────┬─────────┘ │ ┌───────────────────────┼───────────────────────┐ @@ -58,6 +63,13 @@ PYTHON_EXEC_TIMEOUT=60 PYTHON_MAX_MEMORY_MB=512 python server.py │ subprocess │ │ Playwright │ │ markitdown │ │ (Python) │ │ Chromium │ │ (file → MD) │ └───────────┘ └─────────────┘ └───────────────┘ + │ │ │ + └───────────┬───────────┴───────────────────────┘ + │ + ┌─────────▼──────────┐ + │ turnstone_workspace│ + │ (shared volume) │ + └────────────────────┘ ``` ### Key design decisions @@ -67,6 +79,19 @@ PYTHON_EXEC_TIMEOUT=60 PYTHON_MAX_MEMORY_MB=512 python server.py - **Path traversal protection** — `extract_to_markdown` resolves all paths against `WORKSPACE_ROOT` and rejects anything that escapes it. - **Structured JSON logging** — All tool invocations and responses are logged as structured JSON for observability. - **CORS enabled** — The server accepts requests from any origin with standard MCP headers. +- **Shared workspace volume** — The container mounts the `turnstone_workspace` Docker volume at `/workspace`, giving the MCP tools read/write access to files used by other Turnstone services. + +## Docker Configuration + +The service is defined in `compose.yaml`: + +| Setting | Value | +|---|---| +| Port mapping | `8000:8000` | +| User | `1000:1000` | +| Restart policy | `unless-stopped` | +| Health check | HTTP GET `/health` every 30 s (timeout 10 s, 3 retries) | +| Workspace mount | `${WORKSPACE_MOUNT:-workspace}:/workspace` | ## Endpoints @@ -78,11 +103,11 @@ PYTHON_EXEC_TIMEOUT=60 PYTHON_MAX_MEMORY_MB=512 python server.py ## Project Structure ``` +compose.yaml # Docker Compose service definition +Dockerfile # Container build instructions server.py # Complete MCP server — all logic in a single module ``` -This repository is intentionally minimal: one file, zero dependencies beyond the pip requirements. - ## Development No test framework or build step is required. To add a new tool, follow the existing pattern: diff --git a/server.py b/server.py index 7b98664..635ed7b 100644 --- a/server.py +++ b/server.py @@ -66,9 +66,9 @@ PLAYWRIGHT_TEXT_TIMEOUT = int(os.environ.get("PLAYWRIGHT_TEXT_TIMEOUT", "15000") EXTRACT_MAX_FILE_SIZE = int(os.environ.get("EXTRACT_MAX_FILE_SIZE", "10485760")) # 10 MB # Defaults to /workspace to match Docker volumes -WORKSPACE_ROOT = os.path.abspath(os.environ.get("WORKSPACE_ROOT", "/workspace")) +WORKSPACE_MOUNT = os.path.abspath(os.environ.get("WORKSPACE_MOUNT", "/workspace")) -logger.info(f"Workspace Root configured to: {WORKSPACE_ROOT}") +logger.info(f"Workspace Root configured to: {WORKSPACE_MOUNT}") # --------------------------------------------------------------------------- # FastMCP app @@ -115,10 +115,10 @@ class PlaywrightBrowserManager: browser_manager = PlaywrightBrowserManager() def _resolve_safe_path(relative_path: str) -> str: - """Resolve a relative path against WORKSPACE_ROOT, blocking traversal.""" - target = os.path.abspath(os.path.join(WORKSPACE_ROOT, relative_path)) - if not target.startswith(WORKSPACE_ROOT): - raise PermissionError("Access denied: path escapes WORKSPACE_ROOT") + """Resolve a relative path against WORKSPACE_MOUNT, blocking traversal.""" + target = os.path.abspath(os.path.join(WORKSPACE_MOUNT, relative_path)) + if not target.startswith(WORKSPACE_MOUNT): + raise PermissionError("Access denied: path escapes WORKSPACE_MOUNT") return target # --------------------------------------------------------------------------- @@ -318,7 +318,7 @@ def extract_to_markdown(relative_path: str) -> str: Parameters ---------- relative_path : str - Path relative to WORKSPACE_ROOT. + Path relative to WORKSPACE_MOUNT. Returns -------