Skip to main content
Glama
andr3medeiros

PDF Manipulation MCP Server

pdf_auto_crop_page

Automatically crop PDF pages to remove blank margins by detecting content boundaries, optimizing document layout and reducing file size.

Instructions

Automatically crop a PDF page to remove blank margins by detecting content boundaries.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pdf_pathYes
page_numberNo
paddingNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • Core handler function decorated with @mcp.tool(). Opens PDF, detects content bounds from words, images, drawings; computes union rect; applies conservative padding (min 20pt, asymmetric); crops pages if significant margins; saves to timestamped file.
    @mcp.tool()
    async def pdf_auto_crop_page(
        pdf_path: str,
        page_number: Optional[int] = None,
        padding: float = 10.0
    ) -> str:
        """Automatically crop a PDF page to remove blank margins by detecting content boundaries."""
        if not os.path.exists(pdf_path):
            return f"Error: PDF file not found: {pdf_path}"
        
        if not validate_pdf_file(pdf_path):
            return f"Error: Invalid PDF file: {pdf_path}"
        
        try:
            # Open PDF document
            doc = fitz.open(pdf_path)
            
            # Determine pages to process
            if page_number is not None:
                if not validate_page_number(doc, page_number):
                    doc.close()
                    return f"Error: Invalid page number {page_number}. Document has {len(doc)} pages."
                pages_to_process = [page_number]
            else:
                pages_to_process = list(range(len(doc)))
            
            cropped_pages = 0
            
            for page_num in pages_to_process:
                page = doc[page_num]
                
                # Get text at word level for tighter bounds
                words = page.get_text("words")
                text_rects = [word[:4] for word in words if len(word) >= 4]
                
                # Get image rectangles  
                images = page.get_images()
                image_rects = [img[:4] for img in images if len(img) >= 4]
                
                # Get drawing objects (lines, shapes, paths) - NO external dependencies
                drawing_rects = []
                try:
                    drawings = page.get_drawings()
                    for drawing in drawings:
                        if 'rect' in drawing:
                            drawing_rects.append(drawing['rect'])
                except Exception:
                    pass
                
                # Combine all rectangles
                all_rects = text_rects + image_rects + drawing_rects
                
                # Filter out invalid rectangles (outside page bounds or with invalid coordinates)
                page_rect = page.rect
                valid_rects = []
                for rect in all_rects:
                    if len(rect) >= 4:
                        try:
                            r = fitz.Rect(rect[:4])
                            # Check if rectangle is valid and within reasonable bounds
                            if (r.is_valid and 
                                r.x0 >= 0 and r.y0 >= 0 and 
                                r.x1 <= page_rect.width and r.y1 <= page_rect.height and
                                r.width > 0 and r.height > 0):
                                valid_rects.append(rect[:4])
                        except Exception:
                            continue
                
                all_rects = valid_rects
                
                
                if all_rects:
                    # Calculate union of all content rectangles
                    content_rect = fitz.Rect(all_rects[0])
                    for rect in all_rects[1:]:
                        content_rect |= fitz.Rect(rect)
                    
                    # More conservative padding strategy to preserve section flow
                    # Use asymmetric padding: less aggressive on sides, more generous on top/bottom
                    page_rect = page.rect
                    
                    # Calculate how much we can crop while preserving document flow
                    # Only crop if there's significant margin (at least 2 points on each side)
                    margin_threshold = 2.0
                    
                    # Check if margins are significant enough to warrant cropping
                    left_margin = content_rect.x0
                    right_margin = page_rect.width - content_rect.x1
                    top_margin = content_rect.y0
                    bottom_margin = page_rect.height - content_rect.y1
                    
                    # Only crop if margins are substantial
                    if (left_margin > margin_threshold or right_margin > margin_threshold or 
                        top_margin > margin_threshold or bottom_margin > margin_threshold):
                        
                        # Conservative padding: preserve more space for better flow
                        conservative_padding = max(padding, 20.0)  # At least 20 points padding
                        
                        # Asymmetric padding: less on sides, more on top/bottom for better section flow
                        content_rect = content_rect + [
                            -min(conservative_padding * 0.5, left_margin * 0.8),   # left: 50% of padding or 80% of margin
                            -min(conservative_padding, bottom_margin * 0.8),       # bottom: full padding or 80% of margin
                            min(conservative_padding * 0.5, right_margin * 0.8),   # right: 50% of padding or 80% of margin
                            min(conservative_padding, top_margin * 0.8)            # top: full padding or 80% of margin
                        ]
                        
                        # Ensure the crop box is within page bounds
                        content_rect.intersect(page_rect)
                        
                        # Apply crop if there's any reduction in size
                        if (content_rect.width < page_rect.width or 
                            content_rect.height < page_rect.height):
                            page.set_cropbox(content_rect)
                            cropped_pages += 1
                else:
                    # No content found, skip this page
                    continue
            
            if cropped_pages == 0:
                doc.close()
                return "No content found to crop on any pages."
            
            # Generate output filename
            output_path = generate_output_filename(pdf_path, "auto_cropped")
            
            # Save the modified PDF
            doc.save(output_path)
            doc.close()
            
            page_info = f"page {page_number + 1}" if page_number is not None else f"{cropped_pages} pages"
            return f"Successfully auto-cropped {page_info}. Output saved to: {output_path}"
            
        except Exception as e:
            return f"Error auto-cropping PDF: {str(e)}"
  • Initialization of the FastMCP server instance. All tools including pdf_auto_crop_page are registered via @mcp.tool() decorators on their handler functions.
    mcp = FastMCP("pdf-manipulation")
  • Utility to generate timestamped output filename used by pdf_auto_crop_page to save results without overwriting input.
    def generate_output_filename(input_path: str, suffix: str = "modified") -> str:
        """Generate a new filename with timestamp to avoid overwriting originals."""
        path = Path(input_path)
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        return str(path.parent / f"{path.stem}_{suffix}_{timestamp}{path.suffix}")
  • Validates if input file is a valid PDF by attempting to open it with PyMuPDF; used early in handler.
    def validate_pdf_file(pdf_path: str) -> bool:
        """Validate that the file is a valid PDF."""
        try:
            doc = fitz.open(pdf_path)
            doc.close()
            return True
        except Exception:
            return False
  • Checks if specified page number is valid for the document; used when page_number param provided.
    def validate_page_number(doc: fitz.Document, page_num: int) -> bool:
        """Validate that the page number exists in the document."""
        return 0 <= page_num < len(doc)

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed1 schema field changedv1.0.0
    • addedInput schema / title
      Added value: +"pdf_auto_crop_pageArguments"
  2. First observed

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are supplied, so the description must carry the full behavioral burden. It explains the cropping approach but does not disclose whether the file is modified in place, what happens to the original, or how page_number=None affects the crop (e.g., all pages vs first page). This is a significant gap for a mutating operation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, clear, front-loaded sentence with no filler. Every word contributes to the core purpose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Although an output schema exists, the description still lacks critical context: how page_number and padding behave, whether the operation is destructive, and what the return value signifies. The tool is simple enough, but ambiguity around default behavior and parameters makes it incomplete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description must explain the parameters. It only mentions 'page' generically, but fails to explain the required pdf_path, the meaning of padding, or the semantics of page_number being null. The schema provides no descriptions, so the agent is left guessing.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('crop') and resource ('PDF page'), and clearly states it auto-detects content boundaries to remove blank margins. This distinguishes it from the sibling manual pdf_crop_page tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies automatic cropping but provides no explicit guidance on when to choose this over pdf_crop_page or other PDF manipulation tools. No exclusions or alternative selection advice is given.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.