md-pdf-mcp
by seanivore
Verified
- md_pdf_mcp
"""Core MCP server implementation for markdown to PDF conversion."""
from pathlib import Path
import base64
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
from pydantic import AnyUrl
from .converter import convert_markdown_to_pdf
async def serve() -> None:
"""Run the markdown to PDF MCP server."""
# Define our tool
convert_markdown_tool = types.Tool(
name="convert_markdown",
description="Convert markdown to PDF using VS Code styling",
inputSchema={
"type": "object",
"properties": {
"markdown": {"type": "string", "description": "Markdown content to convert"},
"output_path": {"type": "string", "description": "Full path where to save the PDF (must end in .pdf)"},
"theme": {"type": "string", "enum": ["light", "high-contrast"], "default": "light"}
},
"required": ["markdown", "output_path"]
}
)
# Create server
app = Server(
name="md-pdf-mcp",
version="0.1.0"
)
# Set up capabilities
app.tools = [convert_markdown_tool] # Register tool directly
app.resources = { # Register resource schemes
"pdf": types.ResourceTemplate(
uriTemplate="pdf://local/{path}",
name="PDF Files",
description="Generated PDF files",
mimeType="application/pdf"
),
"markdown": types.ResourceTemplate(
uriTemplate="markdown://local/{path}",
name="Markdown Files",
description="Source markdown files",
mimeType="text/markdown"
)
}
app.prompts = { # Register prompt templates
"convert-with-style": types.Prompt(
name="convert-with-style",
description="Convert markdown with custom styling options",
arguments=[
types.PromptArgument(
name="theme",
description="PDF theme (light or high-contrast)",
required=False
)
]
),
"batch-convert": types.Prompt(
name="batch-convert",
description="Convert multiple markdown files to PDF",
arguments=[
types.PromptArgument(
name="directory",
description="Directory containing markdown files",
required=True
)
]
)
}
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return app.tools
@app.call_tool()
async def call_tool(
name: str,
arguments: dict
) -> list[types.TextContent]:
if name == "convert_markdown":
try:
# Validate output path
output_path = arguments["output_path"]
if not output_path.endswith('.pdf'):
output_path += '.pdf'
# Validate theme
theme = arguments.get("theme", "light")
if theme not in ["light", "high-contrast"]:
return [types.TextContent(
type="text",
text=f"Error: Invalid theme '{theme}'. Must be either 'light' or 'high-contrast'",
isError=True
)]
# Convert markdown to PDF
success = convert_markdown_to_pdf(
arguments["markdown"],
output_path,
theme
)
if success:
return [types.TextContent(
type="text",
text=f"Successfully converted markdown to PDF: {output_path}"
)]
else:
return [types.TextContent(
type="text",
text="Failed to generate PDF",
isError=True
)]
except Exception as e:
return [types.TextContent(
type="text",
text=f"Error: {str(e)}",
isError=True
)]
raise ValueError(f"Tool not found: {name}")
# Resource handlers
@app.list_resources()
async def list_resources() -> types.ListResourcesResult:
# List PDF files in current directory
resources = []
for file in Path().glob("*.pdf"):
resources.append(types.Resource(
uri=f"file:///{file.absolute()}",
name=file.name,
mimeType="application/pdf"
))
# List markdown files in current directory
for file in Path().glob("*.md"):
resources.append(types.Resource(
uri=f"file:///{file.absolute()}",
name=file.name,
mimeType="text/markdown"
))
# Return a ListResourcesResult with just the resources field
return types.ListResourcesResult(resources=resources)
@app.list_resource_templates()
async def list_resource_templates() -> types.ListResourceTemplatesResult:
templates = list(app.resources.values())
return types.ListResourceTemplatesResult(resourceTemplates=templates)
@app.read_resource()
async def read_resource(uri: str) -> types.ReadResourceResult:
# Parse the URI to get the scheme and path
uri_str = str(uri) # Convert AnyUrl to string
try:
if uri_str.startswith("pdf://local/"):
path = Path(uri_str.replace("pdf://local/", ""))
if not path.exists():
raise ValueError(f"PDF file not found: {path}")
# Read PDF as binary and encode as base64
contents = [types.BlobResourceContents(
uri=AnyUrl(uri_str),
blob=base64.b64encode(path.read_bytes()).decode(),
mimeType="application/pdf"
)]
return types.ReadResourceResult(contents=contents)
elif uri_str.startswith("markdown://local/"):
path = Path(uri_str.replace("markdown://local/", ""))
if not path.exists():
raise ValueError(f"Markdown file not found: {path}")
# Read markdown as text
contents = [types.TextResourceContents(
uri=AnyUrl(uri_str),
text=path.read_text(encoding='utf-8'),
mimeType="text/markdown"
)]
return types.ReadResourceResult(contents=contents)
raise ValueError(f"Unsupported resource URI scheme: {uri_str}")
except Exception as e:
# Return an error resource content
contents = [types.TextResourceContents(
uri=AnyUrl(uri_str),
text=f"Error reading resource: {str(e)}",
mimeType="text/plain",
isError=True
)]
return types.ReadResourceResult(contents=contents)
# Prompt handlers
@app.list_prompts()
async def list_prompts() -> list[types.Prompt]:
return list(app.prompts.values())
@app.get_prompt()
async def get_prompt(name: str, arguments: dict) -> types.GetPromptResult:
if name == "convert-with-style":
theme = arguments.get("theme", "light")
messages = [
types.PromptMessage(
role="user",
content=types.TextContent(
type="text",
text=f"Please convert this markdown file to PDF using the {theme} theme:\n\n"
)
),
types.PromptMessage(
role="assistant",
content=types.TextContent(
type="text",
text="I'll help you convert your markdown to a beautifully styled PDF. "
"Would you like to proceed with the conversion?"
)
)
]
return types.GetPromptResult(
messages=messages,
description=f"Convert markdown to PDF using {theme} theme"
)
elif name == "batch-convert":
directory = arguments.get("directory", ".")
messages = [
types.PromptMessage(
role="user",
content=types.TextContent(
type="text",
text=f"Please convert all markdown files in the directory '{directory}' to PDFs:\n\n"
)
),
types.PromptMessage(
role="assistant",
content=types.TextContent(
type="text",
text="I'll help you convert all markdown files in the specified directory to PDFs. "
"Would you like to proceed with the batch conversion?"
)
)
]
return types.GetPromptResult(
messages=messages,
description=f"Batch convert markdown files in {directory} to PDFs"
)
raise ValueError(f"Prompt not found: {name}")
# Use stdio transport
async with stdio_server() as streams:
await app.run(
streams[0], # stdin
streams[1], # stdout
app.create_initialization_options() # Use proper initialization options
)