--- title: Markdown Layout Extractor emoji: 📄 colorFrom: red colorTo: yellow sdk: docker app_port: 7860 pinned: false ---
An MCP (Model Context Protocol) server that converts PDFs and documents into Markdown using **Mistral OCR**. ## Features - **`pdf_to_markdown`** — Convert any publicly accessible PDF/document URL to merged Markdown. - **`pdf_to_structured_markdown`** — Convert and get per-page structured output (page index, individual markdown, merged result). - CORS-enabled SSE transport — connect from any MCP client or inspector. - `/health` endpoint for liveness probing. - Structured, colorized logging via Loguru. ## Project Structure ``` pdf_to_md_mcp/ ├── main.py # Entry point — uvicorn runner ├── pyproject.toml ├── sample.env # Secrets reference (copy to .env) ├── development.yml # Non-secret config (server, CORS, OCR model) └── app/ ├── server.py # ASGI app factory (MCP + CORS + health) ├── core/ │ ├── config.py # Pydantic settings (loads .env + development.yml) │ ├── logger.py # Loguru logger │ ├── lifespan.py # AppContext + Mistral client lifecycle │ └── exceptions.py # Domain exceptions ├── services/ │ └── ocr_service.py # Mistral OCR business logic ├── tools/ │ └── markdown_tools.py # @mcp.tool() definitions └── utils/ ├── response.py # create_response() helper └── validators.py # URL validation ``` ## Setup ```bash # Install uv if not already installed curl -LsSf https://astral.sh/uv/install.sh | sh # Install dependencies uv sync # Configure secrets cp sample.env .env # Edit .env — set MISTRAL_API_KEY # Non-secret config (server, CORS, OCR model) lives in development.yml ``` ## Run ```bash uv run main.py ``` Server starts at `http://127.0.0.1:8000` by default. | Endpoint | Description | | --- | --- | | `GET /health` | Liveness probe | | `GET /sse` | MCP SSE transport | | `POST /messages/` | MCP message handler | ## MCP Tools ### `pdf_to_markdown` Convert a document URL to merged Markdown (all pages concatenated). **Input** | Parameter | Type | Description | | --- | --- | --- | | `document_url` | `string` | Publicly accessible URL of a PDF or image document | **Returns** — `string` ``` # Introduction This paper presents... ## Section 2 ... ``` --- ### `pdf_to_structured_markdown` Convert a document URL and get per-page structured output alongside the merged result. **Input** | Parameter | Type | Description | | --- | --- | --- | | `document_url` | `string` | Publicly accessible URL of a PDF or image document | **Returns** — `object` ```json { "page_count": 3, "pages": [ { "index": 0, "markdown": "# Page 1\n..." }, { "index": 1, "markdown": "## Page 2\n..." }, { "index": 2, "markdown": "### Page 3\n..." } ], "markdown": "# Page 1\n...\n\n## Page 2\n...\n\n### Page 3\n..." } ``` ## Debugging with MCP Inspector ```bash npx -y @modelcontextprotocol/inspector ``` Connect to `http://127.0.0.1:8000/sse` locally or your Railway URL in production. ## Deploy to Railway ### 1. Push to GitHub ```bash git init git add . git commit -m "initial commit" gh repo create pdf-to-md-mcp --public --source=. --push ``` ### 2. Create a Railway project Go to [railway.app](https://railway.app) → **New Project** → **Deploy from GitHub repo** → select your repo. Railway detects the `railway.json` and uses `uv run main.py` as the start command automatically. ### 3. Set environment variables In Railway → your service → **Variables**, add: | Variable | Value | |---|---| | `MISTRAL_API_KEY` | your Mistral API key | | `HOST` | `0.0.0.0` | > `PORT` is injected automatically by Railway — do **not** set it manually. > All other config (`MISTRAL_OCR_MODEL`, `LOG_LEVEL`, etc.) is read from `development.yml`. ### 4. Deploy Railway triggers a deploy on every push to your default branch. Once live, your public SSE URL will be: ``` https://