import sys
import os
# Add project root to sys.path to support running as a script
# current: src/server/main.py -> parent: src/server -> parent: src -> parent: root
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(current_dir))
if project_root not in sys.path:
sys.path.insert(0, project_root)
from mcp.server.fastmcp import FastMCP
from src.core.parser import PDFParser
import asyncio
# Initialize FastMCP Server
mcp = FastMCP("pdf-reader")
# Initialize PDF Parser
# Note: PDFParser is stateless or manages its own state per call,
# but here we instantiate it once. If it holds state, be careful.
# Our PDFParser implementation is mostly stateless (creating sub-components).
parser = PDFParser()
@mcp.tool()
async def read_pdf(
source: str,
page_range: str = None,
extract_images: bool = False,
force_ocr: bool = False,
) -> str:
"""
Read content from a PDF file (local path or URL).
Returns a unified Markdwon string containing text, tables, and image references.
Args:
source: Local file path or URL to the PDF.
page_range: Format "1-5" or "10". If not provided, reads all pages.
extract_images: If True, extracts images to temp dir and links them.
"""
result = await parser.parse(source, page_range, extract_images, force_ocr)
# Format the result for the AI
# We return a string content. If the client expects JSON, we can return json.dumps(result).
# But text-based LLMs usually prefer direct text content.
# Let's construct a rich report.
metadata = result["metadata"]
content = result["content"]
report = f"""# PDF Extraction Result
## Metadata
- **Title**: {metadata['title']}
- **Page Count**: {metadata['page_count']}
- **Source**: {metadata['source']}
## Content
{content}
"""
return report
@mcp.tool()
async def get_pdf_metadata(source: str) -> str:
"""
Quickly retrieve metadata from a PDF without reading the full content.
"""
# We can reuse parser but limit the logical scope.
# Actually parser loads the doc anyway.
# For optimization, we might make a separate method in parser, but for now reuse.
# We load it but generate result without full extraction
doc = await parser.loader.load(source)
try:
meta = {
"page_count": len(doc),
"title": doc.metadata.get("title", ""),
"author": doc.metadata.get("author", ""),
}
return str(meta)
finally:
doc.close()
@mcp.resource("pdf://{file_path}")
async def read_pdf_resource(file_path: str) -> str:
"""
Directly read a PDF file as a resource using URI scheme pdf://...
Warning: file_path must be absolute.
"""
# Simply delegate to the parsing logic
# Note: Resources usually return raw content, but for PDF we want the processed markdown
# because raw PDF bytes are not useful to the LLM directly as text.
# Resource templates extract the variables.
# FastMCP resources route passes the variable.
# We need to reconstruct full path if needed, but here it comes as string.
# Re-adding the leading slash if it was stripped is a common gotcha with URI templates,
# but let's assume valid absolute path for now.
# Using the same parser logic
result = await parser.parse(file_path)
return result["content"]
@mcp.prompt()
def summarize_pdf(source: str) -> str:
"""
Create a prompt to summarize a PDF document.
"""
return f"""Please read the PDF content from the following source: {source}
You can use the 'read_pdf' tool to get the content.
Once you have the content, please provide a comprehensive summary including:
1. Main purpose of the document
2. Key findings or arguments
3. Important data points (from tables if any)
4. Conclusions
Source: {source}
"""
if __name__ == "__main__":
mcp.run()