This document covers setup and usage of the Model Context Protocol (MCP) server for Markdown in the Middle.
The Model Context Protocol allows LLMs (like Claude) to call tools defined by your application. The MCP server in Markdown in the Middle exposes two tools:
fetch_markdown- Fetch a URL and convert to Markdownfetch_raw- Fetch a URL and return raw HTML/JSON body
This allows Claude and other MCP-compatible clients to fetch and convert web content on demand.
# In stdio mode (recommended for Claude Desktop)
./markdowninthemiddle mcp --transport chromedpThis starts the MCP server in stdio mode, which reads JSON-RPC requests from stdin and writes responses to stdout.
Edit ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"markdowninthemiddle": {
"command": "/full/path/to/markdowninthemiddle",
"args": ["mcp", "--transport", "chromedp"]
}
}
}Restart Claude Desktop.
Simply ask Claude to fetch and convert a URL:
Can you fetch and summarize https://example.com?
Claude will:
- Call
fetch_markdowntool with the URL - Get back Markdown-converted content
- Summarize it for you
For clients other than Claude Desktop, start the MCP server in HTTP mode:
./markdowninthemiddle mcp \
--transport chromedp \
--mcp-transport http \
--mcp-addr :8081Then make HTTP requests to http://localhost:8081 with JSON-RPC tool calls.
The docker-compose.yml includes an MCP service that runs alongside the proxy:
# Start proxy (8080) + MCP server (8081) + Chrome (9222)
docker compose up -d
# View logs
docker compose logs -f mcp
# Test MCP health
curl http://localhost:8081All services share:
- Same configuration file (
config.yml) - Same Chrome instance (
http://chrome:9222) - Same output directories
HTTP Transport (default):
./markdowninthemiddle mcp --transport httpUses standard HTTP client. Fast, but no JavaScript rendering.
chromedp Transport:
./markdowninthemiddle mcp --transport chromedpUses headless Chrome for JavaScript rendering. Slower, but handles dynamic content.
Stdio Mode (for Claude Desktop):
./markdowninthemiddle mcp --mcp-transport stdioReads from stdin, writes to stdout. Use with Claude Desktop integration.
HTTP Mode (for other clients):
./markdowninthemiddle mcp --mcp-transport http --mcp-addr :8081Listens on HTTP port. Use with REST clients or custom integrations.
When using --transport chromedp:
# Custom Chrome URL (default: http://localhost:9222)
./markdowninthemiddle mcp --transport chromedp --chrome-url http://chrome-host:9222
# Custom pool size (default: 5)
./markdowninthemiddle mcp --transport chromedp --chrome-pool-size 10Configure defaults in config.yml:
transport:
type: chromedp # or http
chromedp:
url: http://localhost:9222
pool_size: 5
output:
enabled: true # Save converted files
dir: ./markdown
conversion:
tiktokenEncoding: cl100k_baseFetch a URL and convert response to Markdown.
Input:
{
"url": "https://example.com"
}Output:
{
"url": "https://example.com",
"markdown": "# Example\n\nContent here...",
"tokens": 245,
"status_code": 200
}Supported content types:
text/html- Converted to Markdownapplication/json- Formatted as Markdown (with optional Mustache template)- Other types - Returned as-is
Token Counting:
The tokens field in the response is automatically calculated for all Markdown output (both HTML and JSON conversions). This uses TikToken encoding (cl100k_base), the same encoding used by Claude and GPT-4.
Use this to:
- Estimate LLM usage costs before processing
- Monitor token consumption in batch operations
- Stay within token budget limits
Example: Token-aware processing
User: Fetch https://api.example.com/items and tell me how many tokens it would take to process all items
Claude:
1. Calls fetch_markdown with the URL
2. Receives response with "tokens": 1234
3. Calculates cost/feasibility based on token count
4. Responds with token estimate
Fetch a URL and return the raw response body.
Input:
{
"url": "https://api.example.com/data"
}Output:
{
"url": "https://api.example.com/data",
"status_code": 200,
"content_type": "application/json",
"body": "{\"key\": \"value\"}"
}-
Verify the MCP server starts without errors:
./markdowninthemiddle mcp --transport chromedp # Should show: "✅ chromedp browser pool ready for MCP requests" -
Check the command path in
claude_desktop_config.jsonis absolute and correct -
Restart Claude Desktop completely (quit and reopen)
-
Check Claude's system logs:
# macOS tail -f ~/Library/Logs/Claude/debug.log
-
Verify Chrome DevTools is running:
curl http://localhost:9222/json/version
-
If using Docker, ensure Chrome service is running:
docker compose ps chrome
-
Check Chrome URL matches your setup:
./markdowninthemiddle mcp --chrome-url http://localhost:9222
This is normal for chromedp transport (JavaScript rendering). To speed up:
-
Increase pool size for concurrent requests:
./markdowninthemiddle mcp --transport chromedp --chrome-pool-size 20
-
Or switch to HTTP transport (no JavaScript):
./markdowninthemiddle mcp --transport http
Both can run simultaneously on different ports:
# Terminal 1: Proxy on 8080
./markdowninthemiddle --transport chromedp
# Terminal 2: MCP server on 8081
./markdowninthemiddle mcp --transport chromedp --mcp-transport http --mcp-addr :8081Or use Docker Compose (recommended):
docker compose up -dFor APIs that return JSON, use Mustache templates to customize conversion:
In config.yml:
templates:
store:
patterns:
- pattern: "^https://api\.example\.com/"
template_file: ./templates/example-api.mustacheFor high-concurrency workloads:
# Instance 1 - High concurrency for chromedp
./markdowninthemiddle mcp \
--transport chromedp \
--chrome-pool-size 20 \
--mcp-transport http \
--mcp-addr :8081 &
# Instance 2 - Light load, no JavaScript
./markdowninthemiddle mcp \
--transport http \
--mcp-transport http \
--mcp-addr :8082 &Then load-balance between both instances.
Typical response times (single concurrent request):
| Transport | Content | Time |
|---|---|---|
| HTTP | Plain HTML (50KB) | 20ms |
| HTTP | JSON (10KB) | 5ms |
| chromedp | SPA with JS (200KB) | 1500ms |
| chromedp | Plain HTML (50KB) | 800ms |
chromedp adds significant latency due to JavaScript rendering. Use HTTP transport for APIs and static content, chromedp only for dynamic SPAs.
- Use HTTP transport for APIs and static content
- Increase pool size if you have multiple concurrent requests
- Cache results - Enable output writing to avoid re-fetching
- Filter domains - Use
--allowpatterns to restrict scope - Run multiple instances - One for chromedp, one for HTTP
import subprocess
import json
# Start MCP server
proc = subprocess.Popen(
["./markdowninthemiddle", "mcp", "--transport", "chromedp"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
# Call fetch_markdown
request = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "fetch_markdown",
"arguments": {"url": "https://example.com"}
}
}
proc.stdin.write(json.dumps(request).encode() + b"\n")
response = json.loads(proc.stdout.readline())
print(response["result"]["content"][0]["text"])const { spawn } = require('child_process');
const mcp = spawn('./markdowninthemiddle', ['mcp', '--transport', 'chromedp']);
const request = {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: "fetch_markdown",
arguments: { url: "https://example.com" }
}
};
mcp.stdin.write(JSON.stringify(request) + '\n');
mcp.stdout.on('data', (data) => {
console.log(JSON.parse(data).result);
});- README.md - Main documentation
- CHROMEDP.md - JavaScript rendering setup
- DOCKER.md - Docker deployment