Files
mcp-linkgen/README.md
T
2026-09-04 18:30:15 +01:00

6.9 KiB
Raw Blame History

mcp-linkgen

A lightweight Model Context Protocol server that generates ephemeral, single-use download links for files stored in a configurable workspace directory. Intended for use in the Turnstone project.

Features

  • Single MCP tool — generate_download_link mints a URL containing a cryptographically random token.
  • Single-use tokens — each link is consumed on first request; no replay possible.
  • Time-limited — links expire after a configurable TTL (default 10 min, clamped to 1–60 min).
  • Path traversal protection — os.path.realpath() resolves symlinks and .. before validating against the workspace root.
  • Directory blocking — only files can be shared; directories are rejected.
  • In-memory token store — tokens are never written to disk.
  • Configurable CORS — restrict cross-origin access to specific domains.
  • Per-IP rate limiting — in-memory sliding-window limits on the MCP and download endpoints.
  • Structured JSON logging — every tool call and download is logged as a single JSON line.
  • Docker + native — runs in a ~120 MB container or directly via python server.py.

Quick Start

Docker

Copy .env example and change any defaults as required:

cp .env.example .env

Build and run the server:

docker compose up --build

The server listens on port 8000 by default.

Configuration

All settings are read from environment variables. A .env.example file is provided with sensible defaults — edit it before starting the server.

Variable Default Description
HOST 0.0.0.0 Bind address
PORT 8000 Listen port
WORKSPACE_ROOT /workspace Absolute path to the directory containing files to share. Leave undefined to mount turnstone_workspace volume
PUBLIC_BASE_URL http://localhost:8000 User-facing base URL for generated links
DOWNLOAD_DEFAULT_TTL 10 Default link lifetime in minutes
DOWNLOAD_MIN_TTL 1 Minimum allowed TTL (minutes)
DOWNLOAD_MAX_TTL 60 Maximum allowed TTL (minutes)
CORS_ORIGINS * Comma-separated list of allowed origins, or * for all
MCP_RATE_LIMIT 60 Max MCP tool requests per minute per IP (0 disables)
DOWNLOAD_RATE_LIMIT 10 Max download requests per minute per IP (0 disables)
RATE_LIMIT_WINDOW 60 Sliding window size in seconds
MCP_ENDPOINT /mcp HTTP path for the MCP endpoint (used by the rate limiter)

CORS examples

# Open (development)
CORS_ORIGINS=*

# Restricted to your domain
CORS_ORIGINS=https://myapp.com,https://admin.myapp.com

Endpoints

Method Path Description
POST /mcp MCP protocol endpoint (Streamable HTTP)
GET /health Liveness probe — returns {"status":"healthy"}
GET /download?t=<token> Streams the file, then invalidates the token

Security Model

Strengths

Property Mechanism
Single-use tokens Token is deleted from the in-memory store on first successful download, preventing replay attacks.
Time-limited links Tokens carry an expires_at timestamp. Expired tokens are rejected (HTTP 410) and periodically garbage-collected.
Path traversal protection _validate_download_path() calls os.path.realpath() to resolve symlinks and .., then verifies the result starts with WORKSPACE_ROOT + "/".
Directory isolation IsADirectoryError is raised for directories — callers must archive them first.
In-memory store Tokens are never written to disk or logged, limiting exposure surface.
CORS restrictions Configurable CORS_ORIGINS prevents arbitrary cross-origin requests to the /download endpoint.
Rate limiting Per-IP sliding-window limits on the MCP (POST /mcp) and download (GET /download) endpoints slow brute-force token guessing and API abuse. Exceeding a limit returns HTTP 429 with a Retry-After header.

Drawbacks and precautions

Concern Details
No transport encryption The server itself speaks plain HTTP. Deploy behind a TLS-terminating reverse proxy (nginx, Caddy, cloud LB) and set PUBLIC_BASE_URL to the https:// address.
No client binding Anyone who obtains a token URL can use it — tokens are not tied to an IP, session, or API key. Treat generated URLs as secrets.
In-memory persistence All tokens are lost on server restart or crash. This is a feature (no state leakage) but means links do not survive restarts.
Rate limits are in-memory and per-instance Rate-limit counters live in process memory and are not shared across multiple server instances. For horizontally-scaled deployments, front with a shared rate limiter (e.g., a reverse proxy or Redis-backed middleware).
Rate limits key on IP Counters are keyed by the client IP (honoring a single X-Forwarded-For entry). Behind a proxy, ensure the proxy sets X-Forwarded-For correctly; otherwise all clients may share one bucket. A determined attacker can rotate IPs to bypass limits — this is a deterrent, not a hard boundary.
Workspace scope Any file inside WORKSPACE_ROOT is reachable by an attacker who knows the path. Limit WORKSPACE_ROOT to the narrowest directory needed.

Opaque query parameter

The download endpoint reads the token from a deliberately opaque single-character query parameter: ?t=<token>. This naming avoids common credential-leak heuristics that flag parameters named token, key, api_key, etc., so links pass through guardrails without requiring an allow-list rule.

Two things to note:

  • No backward compatibility — ?t= is the only accepted parameter name. The old ?token= is not read and will return HTTP 400. Links issued by prior versions of this server will no longer work; regenerate them.
  • This is not a security control — renaming the parameter is a guardrail workaround (security through obscurity), not a hardening measure. Anyone who obtains a link can still download the file. The actual protections remain token entropy (secrets.token_urlsafe(32)), single-use enforcement, TTL expiry, and per-IP rate limiting.

Usage with an LLM client

  1. The LLM calls generate_download_link(path="/workspace/report.pdf", ttl_minutes=15).
  2. The server returns http://localhost:8000/download?t=a1b2c3....
  3. The LLM presents the link to the user (or the user downloads it directly).
  4. On the first GET /download?t=..., the file is streamed and the token is destroyed.

Project Structure

mcp-linkgen/
├── server.py          # Single-module MCP server (all logic)
├── .env.example       # Environment configuration
├── Dockerfile         # Slim container build
├── compose.yaml       # Docker Compose service definition
├── README.md          # This file
├── LICENSE            # MIT license
└── .gitignore