Update README.md and correct ENV in server.py
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
# Turnstone MCP Server
|
# 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
|
## Tools
|
||||||
|
|
||||||
@@ -13,20 +15,23 @@ A [Model Context Protocol](https://modelcontextprotocol.io/) server providing fo
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
Run the server with Docker Compose:
|
||||||
# Install dependencies
|
|
||||||
pip install fastmcp playwright markitdown
|
|
||||||
playwright install chromium
|
|
||||||
|
|
||||||
# Run the server
|
```bash
|
||||||
python server.py
|
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
|
## 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 |
|
| 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_NAV_TIMEOUT` | `30000` | Browser navigation timeout (ms) |
|
||||||
| `PLAYWRIGHT_TEXT_TIMEOUT` | `15000` | Page text extraction timeout (ms) |
|
| `PLAYWRIGHT_TEXT_TIMEOUT` | `15000` | Page text extraction timeout (ms) |
|
||||||
| `EXTRACT_MAX_FILE_SIZE` | `10485760` | Maximum file size for extraction (10 MB) |
|
| `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:
|
Example with custom limits:
|
||||||
|
|
||||||
```bash
|
```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
|
## Architecture
|
||||||
@@ -49,7 +54,7 @@ PYTHON_EXEC_TIMEOUT=60 PYTHON_MAX_MEMORY_MB=512 python server.py
|
|||||||
```
|
```
|
||||||
┌──────────────┐ HTTP/MCP ┌──────────────────┐
|
┌──────────────┐ HTTP/MCP ┌──────────────────┐
|
||||||
│ LLM Client │ ────────────────► │ Turnstone 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 │
|
│ subprocess │ │ Playwright │ │ markitdown │
|
||||||
│ (Python) │ │ Chromium │ │ (file → MD) │
|
│ (Python) │ │ Chromium │ │ (file → MD) │
|
||||||
└───────────┘ └─────────────┘ └───────────────┘
|
└───────────┘ └─────────────┘ └───────────────┘
|
||||||
|
│ │ │
|
||||||
|
└───────────┬───────────┴───────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────▼──────────┐
|
||||||
|
│ turnstone_workspace│
|
||||||
|
│ (shared volume) │
|
||||||
|
└────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key design decisions
|
### 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.
|
- **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.
|
- **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.
|
- **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
|
## Endpoints
|
||||||
|
|
||||||
@@ -78,11 +103,11 @@ PYTHON_EXEC_TIMEOUT=60 PYTHON_MAX_MEMORY_MB=512 python server.py
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
|
compose.yaml # Docker Compose service definition
|
||||||
|
Dockerfile # Container build instructions
|
||||||
server.py # Complete MCP server — all logic in a single module
|
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
|
## Development
|
||||||
|
|
||||||
No test framework or build step is required. To add a new tool, follow the existing pattern:
|
No test framework or build step is required. To add a new tool, follow the existing pattern:
|
||||||
|
|||||||
@@ -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
|
EXTRACT_MAX_FILE_SIZE = int(os.environ.get("EXTRACT_MAX_FILE_SIZE", "10485760")) # 10 MB
|
||||||
|
|
||||||
# Defaults to /workspace to match Docker volumes
|
# 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
|
# FastMCP app
|
||||||
@@ -115,10 +115,10 @@ class PlaywrightBrowserManager:
|
|||||||
browser_manager = PlaywrightBrowserManager()
|
browser_manager = PlaywrightBrowserManager()
|
||||||
|
|
||||||
def _resolve_safe_path(relative_path: str) -> str:
|
def _resolve_safe_path(relative_path: str) -> str:
|
||||||
"""Resolve a relative path against WORKSPACE_ROOT, blocking traversal."""
|
"""Resolve a relative path against WORKSPACE_MOUNT, blocking traversal."""
|
||||||
target = os.path.abspath(os.path.join(WORKSPACE_ROOT, relative_path))
|
target = os.path.abspath(os.path.join(WORKSPACE_MOUNT, relative_path))
|
||||||
if not target.startswith(WORKSPACE_ROOT):
|
if not target.startswith(WORKSPACE_MOUNT):
|
||||||
raise PermissionError("Access denied: path escapes WORKSPACE_ROOT")
|
raise PermissionError("Access denied: path escapes WORKSPACE_MOUNT")
|
||||||
return target
|
return target
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -318,7 +318,7 @@ def extract_to_markdown(relative_path: str) -> str:
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
relative_path : str
|
relative_path : str
|
||||||
Path relative to WORKSPACE_ROOT.
|
Path relative to WORKSPACE_MOUNT.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
|||||||
Reference in New Issue
Block a user