# Turnstone MCP Server A [Model Context Protocol](https://modelcontextprotocol.io/) server providing five sandboxed tools for LLM-driven workflows. Built with **FastMCP** and **Playwright**, it exposes code execution, headless browser automation, file-to-Markdown conversion, and ephemeral file downloads 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 | Tool | Description | |---|---| | `execute_python_code` | Runs Python source in a subprocess with timeout, memory, and output-size limits | | `playwright_navigate` | Navigates a persistent headless Chromium session to a URL | | `playwright_get_page_text` | Extracts rendered text from the current browser page | | `extract_to_markdown` | Converts supported files (PDF, DOCX, images, CSV, etc.) to Markdown via `markitdown` | | `generate_download_link` | Creates a single-use, time-limited HTTPS download URL for a file in `/workspace` | ## Quick Start Run the server with Docker Compose: ```bash docker compose up --build ``` 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. Set them in your `.env` file or pass them through `docker compose`: | Variable | Default | Description | |---|---|---| | `PYTHON_EXEC_TIMEOUT` | `30` | Python execution timeout (seconds) | | `PYTHON_MAX_OUTPUT` | `65536` | Maximum stdout/stderr capture (bytes) | | `PYTHON_MAX_MEMORY_MB` | `256` | Address-space memory limit per subprocess (MB) | | `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) | | `PUBLIC_BASE_URL` | `http://localhost:8000` | Public HTTPS base URL for ephemeral download links | | `WORKSPACE_MOUNT` | `/workspace` | Absolute path to the shared workspace root (used as the Docker volume name in `compose.yaml`) | Example with custom limits: ```bash PYTHON_EXEC_TIMEOUT=60 PYTHON_MAX_MEMORY_MB=512 docker compose up --build ``` ## Architecture ``` ┌──────────────┐ HTTP/MCP ┌──────────────────┐ │ LLM Client │ ────────────────► │ Turnstone MCP │ │ │ │ Container :8000 │ └──────────────┘ └────────┬─────────┘ │ ┌───────────────────────┼───────────────────────┐ │ │ │ ┌─────▼─────┐ ┌──────▼──────┐ ┌───────▼───────┐ │ subprocess │ │ Playwright │ │ markitdown │ │ (Python) │ │ Chromium │ │ (file → MD) │ └───────────┘ └─────────────┘ └───────────────┘ │ │ │ └───────────┬───────────┴───────────────────────┘ │ ┌─────────▼──────────┐ │ turnstone_workspace│ │ (shared volume) │ └────────────────────┘ ``` ### Key design decisions - **Persistent browser session** — A singleton `PlaywrightBrowserManager` holds the Chromium process alive across tool calls, preventing garbage-collection teardowns. - **Sandboxed execution** — Python code runs in a separate process with a per-subprocess memory limit (`resource.setrlimit`) and a wall-clock timeout enforced by `subprocess.run`. - **Path traversal protection** — `extract_to_markdown` and `generate_download_link` resolve all paths against `WORKSPACE_MOUNT` and reject anything that escapes it. - **Ephemeral download links** — `generate_download_link` mints a 256-bit single-use token linked to a file path; clicking the URL streams the file and immediately invalidates the token. Expired tokens are garbage-collected automatically. - **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. ## Credential Leak Guardrail — Action Required The Turnstone credential leak guardrail intercepts outbound messages and flags URLs it considers suspicious. Because `generate_download_link` returns a URL containing a token, the guardrail will block it unless a policy explicitly allows the pattern. > **Action:** Add the following pattern to the Turnstone credential leak policy: > > ``` > PUBLIC_BASE_URL/download? > ``` > > Replace `PUBLIC_BASE_URL` with the actual value of the environment variable (e.g., `https://turnstone.example.com/download?`). Without this policy, any attempt by the agent to present a download link to the user will be silently blocked by the credential leak filter. ## 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 | Method | Path | Description | |---|---|---| | `POST` | `/` | MCP protocol endpoint (default) | | `GET` | `/health` | Returns `{"status": "healthy", "service": "turnstone-mcp"}` | | `GET` | `/download?token=` | Streams a file identified by a single-use token (see `generate_download_link` tool) | ## Project Structure ``` compose.yaml # Docker Compose service definition Dockerfile # Container build instructions server.py # Complete MCP server — all logic in a single module ``` ## Development No test framework or build step is required. To add a new tool, follow the existing pattern: 1. Define the function with `@mcp.tool()` 2. Add docstring with `Parameters` and `Returns` sections 3. Implement input validation and error handling 4. Return a string result (success or error message) ## License See the repository root for licensing information.