#!/usr/bin/env python3
"""
MCP Server per conversione Markdown a PDF con supporto Mermaid
"""
import os
import tempfile
import subprocess
import base64
import json
import sys
from pathlib import Path
from typing import Dict, Any, List, Optional
import asyncio
# MCP SDK imports
try:
from mcp.server.models import InitializationOptions
from mcp.server import NotificationOptions, Server
from mcp.types import Tool, TextContent, ImageContent, EmbeddedResource
import mcp.types as types
except ImportError:
print("❌ MCP SDK non trovato. Installare con: pip install mcp", file=sys.stderr)
sys.exit(1)
# Crea il server MCP
server = Server("md2pdf-converter")
@server.list_tools()
async def handle_list_tools() -> list[Tool]:
"""Lista tutti i tools disponibili"""
return [
Tool(
name="convert_md_to_pdf",
description="Converte contenuto Markdown in PDF con supporto per diagrammi Mermaid",
inputSchema={
"type": "object",
"properties": {
"markdown_content": {
"type": "string",
"description": "Il contenuto Markdown da convertire"
},
"output_filename": {
"type": "string",
"description": "Nome del file PDF di output",
"default": "output.pdf"
},
"title": {
"type": "string",
"description": "Titolo del documento (opzionale)",
"default": ""
},
"author": {
"type": "string",
"description": "Autore del documento (opzionale)",
"default": ""
},
"papersize": {
"type": "string",
"description": "Formato carta (a4, letter, a3, etc.)",
"default": "a4"
},
"toc": {
"type": "boolean",
"description": "Se includere l'indice (Table of Contents)",
"default": True
},
"highlight_style": {
"type": "string",
"description": "Stile di highlight per il codice",
"default": "pygments"
}
},
"required": ["markdown_content"]
}
),
Tool(
name="convert_md_file_to_pdf",
description="Converte un file Markdown in PDF",
inputSchema={
"type": "object",
"properties": {
"input_path": {
"type": "string",
"description": "Percorso del file Markdown di input"
},
"output_path": {
"type": "string",
"description": "Percorso del file PDF di output (se vuoto, usa input_path con estensione .pdf)",
"default": ""
},
"title": {
"type": "string",
"description": "Titolo del documento (opzionale)",
"default": ""
},
"author": {
"type": "string",
"description": "Autore del documento (opzionale)",
"default": ""
},
"papersize": {
"type": "string",
"description": "Formato carta (a4, letter, a3, etc.)",
"default": "a4"
},
"toc": {
"type": "boolean",
"description": "Se includere l'indice",
"default": True
},
"highlight_style": {
"type": "string",
"description": "Stile di highlight per il codice",
"default": "pygments"
}
},
"required": ["input_path"]
}
),
Tool(
name="list_highlight_styles",
description="Elenca gli stili di highlighting disponibili per il codice",
inputSchema={
"type": "object",
"properties": {}
}
),
Tool(
name="get_pandoc_info",
description="Ottiene informazioni sulla versione di Pandoc e le funzionalità disponibili",
inputSchema={
"type": "object",
"properties": {}
}
)
]
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""Gestisce le chiamate ai tools"""
if name == "convert_md_to_pdf":
result = await convert_md_to_pdf(**arguments)
return [TextContent(type="text", text=json.dumps(result, indent=2))]
elif name == "convert_md_file_to_pdf":
result = await convert_md_file_to_pdf(**arguments)
return [TextContent(type="text", text=json.dumps(result, indent=2))]
elif name == "list_highlight_styles":
result = await list_highlight_styles()
return [TextContent(type="text", text=json.dumps(result, indent=2))]
elif name == "get_pandoc_info":
result = await get_pandoc_info()
return [TextContent(type="text", text=json.dumps(result, indent=2))]
else:
raise ValueError(f"Tool sconosciuto: {name}")
async def convert_md_to_pdf(
markdown_content: str,
output_filename: str = "output.pdf",
title: str = "",
author: str = "",
papersize: str = "a4",
toc: bool = True,
highlight_style: str = "pygments"
) -> Dict[str, Any]:
"""
Converte contenuto Markdown in PDF con supporto per diagrammi Mermaid usando Docker.
"""
try:
# Crea directory temporanea per il lavoro
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Salva il contenuto Markdown in un file temporaneo
input_file = temp_path / "input.md"
with open(input_file, 'w', encoding='utf-8') as f:
f.write(markdown_content)
# Prepara il nome del file di output
output_file = temp_path / output_filename
# Ottieni il percorso della directory del server MCP
server_dir = Path(__file__).parent
# Costruisci il comando Docker
docker_cmd = [
"docker", "run", "--rm",
"-v", f"{temp_path}:/workspace/tmp",
"-v", f"{server_dir}:/workspace",
"-w", "/workspace",
"pandoc-mermaid",
"/workspace/convert.sh",
f"/workspace/tmp/{input_file.name}",
f"/workspace/tmp/{output_file.name}"
]
# Esegui il comando Docker
process = await asyncio.create_subprocess_exec(
*docker_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
return {
"success": False,
"error": f"Errore Docker: {stderr.decode()}",
"stdout": stdout.decode(),
"command": " ".join(docker_cmd)
}
# Verifica che il file PDF sia stato creato
if not output_file.exists():
return {
"success": False,
"error": "File PDF non generato",
"stdout": stdout.decode(),
"stderr": stderr.decode()
}
# Leggi il contenuto del PDF e codificalo in base64
with open(output_file, 'rb') as f:
pdf_content = base64.b64encode(f.read()).decode('utf-8')
# Ottieni informazioni sul file
file_size = output_file.stat().st_size
return {
"success": True,
"filename": output_filename,
"size_bytes": file_size,
"size_human": f"{file_size / 1024:.1f} KB" if file_size < 1024*1024 else f"{file_size / (1024*1024):.1f} MB",
"pdf_base64": pdf_content,
"message": f"✅ PDF generato con successo: {output_filename} ({file_size} bytes)",
"docker_output": stdout.decode()
}
except Exception as e:
return {
"success": False,
"error": f"Errore durante la conversione: {str(e)}"
}
async def convert_md_file_to_pdf(
input_path: str,
output_path: str = "",
title: str = "",
author: str = "",
papersize: str = "a4",
toc: bool = True,
highlight_style: str = "pygments"
) -> Dict[str, Any]:
"""
Converte un file Markdown in PDF usando Docker.
"""
try:
input_file = Path(input_path)
# Verifica che il file di input esista
if not input_file.exists():
return {
"success": False,
"error": f"File di input non trovato: {input_path}"
}
# Determina il file di output
if not output_path:
output_file = input_file.with_suffix('.pdf')
else:
output_file = Path(output_path)
# Ottieni il percorso della directory del server MCP
server_dir = Path(__file__).parent
# Ottieni le directory assolute
input_dir = input_file.parent.resolve()
output_dir = output_file.parent.resolve()
# Crea la directory di output se non esiste
output_dir.mkdir(parents=True, exist_ok=True)
# Costruisci il comando Docker
docker_cmd = [
"docker", "run", "--rm",
"-v", f"{input_dir}:/workspace/input",
"-v", f"{output_dir}:/workspace/output",
"-v", f"{server_dir}:/workspace",
"-w", "/workspace",
"pandoc-mermaid",
"/workspace/convert.sh",
f"/workspace/input/{input_file.name}",
f"/workspace/output/{output_file.name}"
]
# Esegui il comando Docker
process = await asyncio.create_subprocess_exec(
*docker_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
return {
"success": False,
"error": f"Errore Docker: {stderr.decode()}",
"stdout": stdout.decode(),
"command": " ".join(docker_cmd)
}
# Verifica che il file PDF sia stato creato
if not output_file.exists():
return {
"success": False,
"error": "File PDF non generato",
"stdout": stdout.decode(),
"stderr": stderr.decode()
}
# Ottieni informazioni sul file
file_size = output_file.stat().st_size
return {
"success": True,
"output_path": str(output_file),
"filename": output_file.name,
"size_bytes": file_size,
"size_human": f"{file_size / 1024:.1f} KB" if file_size < 1024*1024 else f"{file_size / (1024*1024):.1f} MB",
"message": f"✅ PDF salvato in: {output_file} ({file_size} bytes)",
"docker_output": stdout.decode()
}
except Exception as e:
return {
"success": False,
"error": f"Errore durante la conversione del file: {str(e)}"
}
async def list_highlight_styles() -> Dict[str, Any]:
"""
Elenca gli stili di highlighting disponibili per il codice usando Docker.
"""
try:
# Ottieni il percorso della directory del server MCP
server_dir = Path(__file__).parent
# Costruisci il comando Docker
docker_cmd = [
"docker", "run", "--rm",
"pandoc-mermaid",
"pandoc", "--list-highlight-styles"
]
process = await asyncio.create_subprocess_exec(
*docker_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
styles = stdout.decode().strip().split('\n')
return {
"success": True,
"styles": styles,
"count": len(styles)
}
else:
return {
"success": False,
"error": f"Errore Docker nell'ottenere gli stili: {stderr.decode()}",
"command": " ".join(docker_cmd)
}
except Exception as e:
return {
"success": False,
"error": f"Errore: {str(e)}"
}
async def get_pandoc_info() -> Dict[str, Any]:
"""
Ottiene informazioni sulla versione di Pandoc e le funzionalità disponibili usando Docker.
"""
try:
# Ottieni il percorso della directory del server MCP
server_dir = Path(__file__).parent
# Versione pandoc
version_cmd = [
"docker", "run", "--rm",
"pandoc-mermaid",
"pandoc", "--version"
]
version_process = await asyncio.create_subprocess_exec(
*version_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
version_stdout, version_stderr = await version_process.communicate()
# PDF engines disponibili
engines_cmd = [
"docker", "run", "--rm",
"pandoc-mermaid",
"pandoc", "--list-engines"
]
engines_process = await asyncio.create_subprocess_exec(
*engines_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
engines_stdout, engines_stderr = await engines_process.communicate()
# Controlla se mermaid-filter è disponibile
mermaid_cmd = [
"docker", "run", "--rm",
"pandoc-mermaid",
"which", "mermaid-filter"
]
mermaid_process = await asyncio.create_subprocess_exec(
*mermaid_cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
await mermaid_process.communicate()
return {
"success": True,
"pandoc_version": version_stdout.decode() if version_process.returncode == 0 else "N/A",
"pdf_engines": engines_stdout.decode().strip().split('\n') if engines_process.returncode == 0 else [],
"mermaid_filter_available": mermaid_process.returncode == 0,
"docker_image": "pandoc-mermaid"
}
except Exception as e:
return {
"success": False,
"error": f"Errore: {str(e)}"
}
async def main():
"""Avvia il server MCP in modalità stdio"""
# Importa stdio per il trasporto
from mcp.server.stdio import stdio_server
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="md2pdf-converter",
server_version="1.0.0",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={}
)
)
)
if __name__ == "__main__":
asyncio.run(main())