Fix page numbering: Switch to user-friendly 1-based indexing

**Problem**: Zero-based page numbers were confusing for users who naturally
think of pages starting from 1.

**Solution**:
- Updated `parse_pages_parameter()` to convert 1-based user input to 0-based internal representation
- All user-facing documentation now uses 1-based page numbering (page 1 = first page)
- Internal processing continues to use 0-based indexing for PyMuPDF compatibility
- Output page numbers are consistently displayed as 1-based for users

**Changes**:
- Enhanced documentation strings to clarify "1-based" page numbering
- Updated README examples with 1-based page numbers and clarifying comments
- Fixed split_pdf function to handle 1-based input correctly
- Updated test cases to verify 1-based -> 0-based conversion
- Added feature highlight: "User-Friendly: All page numbers use 1-based indexing"

**Impact**: Much more intuitive for users - no more confusion about which page is "page 0"\!

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ryan Malloy 2025-08-11 04:32:20 -06:00
parent f0365a0d75
commit f601d44d99
7 changed files with 166 additions and 36 deletions

11
.mcp.json Normal file
View File

@ -0,0 +1,11 @@
{
"mcpServers": {
"pdf-tools": {
"command": "uv",
"args": ["run", "mcp-pdf-tools"],
"env": {
"PDF_TEMP_DIR": "/tmp/mcp-pdf-processing"
}
}
}
}

88
CLAUDE_DESKTOP_SETUP.md Normal file
View File

@ -0,0 +1,88 @@
# Claude Desktop MCP Configuration
This document explains how the MCP PDF Tools server has been configured for Claude Desktop.
## Configuration Location
The MCP configuration has been added to:
```
/home/rpm/.config/Claude/claude_desktop_config.json
```
## PDF Tools Server Configuration
The following configuration has been added to your Claude Desktop:
```json
{
"mcpServers": {
"pdf-tools": {
"command": "uv",
"args": [
"--directory",
"/home/rpm/claude/mcp-pdf-tools",
"run",
"mcp-pdf-tools"
],
"env": {
"PDF_TEMP_DIR": "/tmp/mcp-pdf-processing"
}
}
}
}
```
## What This Enables
With this configuration, all your Claude sessions will have access to:
- **extract_text**: Extract text from PDFs with multiple method support
- **extract_tables**: Extract tables from PDFs with intelligent fallbacks
- **extract_images**: Extract and filter images from PDFs
- **extract_metadata**: Get comprehensive PDF metadata and file information
- **get_document_structure**: Analyze PDF structure, outline, and fonts
- **is_scanned_pdf**: Detect if PDFs are scanned/image-based
- **ocr_pdf**: Perform OCR on scanned PDFs with preprocessing
- **pdf_to_markdown**: Convert PDFs to clean markdown format
## Environment Variables
- `PDF_TEMP_DIR`: Set to `/tmp/mcp-pdf-processing` for temporary file processing
## Backup
A backup of your original configuration has been saved to:
```
/home/rpm/.config/Claude/claude_desktop_config.json.backup
```
## Testing
The server has been tested and is working correctly. You can verify it's available in new Claude sessions by checking for the `mcp__pdf-tools__*` functions.
## Troubleshooting
If you encounter issues:
1. **Server not starting**: Check that all dependencies are installed:
```bash
cd /home/rpm/claude/mcp-pdf-tools
uv sync --dev
```
2. **System dependencies missing**: Install required packages:
```bash
sudo apt-get install tesseract-ocr tesseract-ocr-eng poppler-utils ghostscript python3-tk default-jre-headless
```
3. **Permission issues**: Ensure temp directory exists:
```bash
mkdir -p /tmp/mcp-pdf-processing
chmod 755 /tmp/mcp-pdf-processing
```
4. **Test server manually**:
```bash
cd /home/rpm/claude/mcp-pdf-tools
uv run mcp-pdf-tools --help
```

View File

@ -12,6 +12,7 @@ A comprehensive FastMCP server for PDF processing operations. This server provid
- **Format Conversion**: Convert PDFs to clean Markdown format
- **URL Support**: Process PDFs directly from HTTPS URLs with intelligent caching
- **Smart Detection**: Automatically detect the best method for each operation
- **User-Friendly**: All page numbers use 1-based indexing (page 1 = first page)
## URL Support
@ -133,7 +134,7 @@ result = await extract_text(
# Extract specific pages with layout preservation
result = await extract_text(
pdf_path="/path/to/document.pdf",
pages=[0, 1, 2], # First 3 pages
pages=[1, 2, 3], # First 3 pages (1-based numbering)
preserve_layout=True,
method="pdfplumber" # Or "auto", "pymupdf", "pypdf"
)
@ -150,7 +151,7 @@ result = await extract_tables(
# Extract tables from specific pages in markdown format
result = await extract_tables(
pdf_path="/path/to/document.pdf",
pages=[2, 3],
pages=[2, 3], # Pages 2 and 3 (1-based numbering)
output_format="markdown" # Or "json", "csv"
)
```
@ -231,13 +232,13 @@ result = await classify_content(
result = await summarize_content(
pdf_path="/path/to/document.pdf",
summary_length="medium", # "short", "medium", "long"
pages="1,2,3" # Specific pages
pages="1,2,3" # Specific pages (1-based numbering)
)
# Analyze page layout
result = await analyze_layout(
pdf_path="/path/to/document.pdf",
pages="1,2,3",
pages="1,2,3", # Specific pages (1-based numbering)
include_coordinates=True
)
```
@ -253,7 +254,7 @@ result = await extract_form_data(
# Split PDF into separate files
result = await split_pdf(
pdf_path="/path/to/document.pdf",
split_pages="5,10,15", # Split after pages 5, 10, 15
split_pages="5,10,15", # Split after pages 5, 10, 15 (1-based)
output_prefix="section"
)
@ -266,7 +267,7 @@ result = await merge_pdfs(
# Rotate specific pages
result = await rotate_pages(
pdf_path="/path/to/document.pdf",
page_rotations={"1": 90, "3": 180} # Page 1: 90°, Page 3: 180°
page_rotations={"1": 90, "3": 180} # Page 1: 90°, Page 3: 180° (1-based)
)
```
@ -299,7 +300,7 @@ result = await compare_pdfs(
# Extract charts and diagrams
result = await extract_charts(
pdf_path="/path/to/report.pdf",
pages="2,3,4",
pages="2,3,4", # Pages 2, 3, 4 (1-based numbering)
min_size=150 # Minimum size for chart detection
)

View File

@ -0,0 +1,16 @@
{
"mcpServers": {
"pdf-tools": {
"command": "uv",
"args": [
"--directory",
"/home/rpm/claude/mcp-pdf-tools",
"run",
"mcp-pdf-tools"
],
"env": {
"PDF_TEMP_DIR": "/tmp/mcp-pdf-processing"
}
}
}
}

3
mcp-pdf-tools-launcher.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
cd /home/rpm/claude/mcp-pdf-tools
exec uv run mcp-pdf-tools "$@"

View File

@ -63,24 +63,32 @@ CACHE_DIR = Path(os.environ.get("PDF_TEMP_DIR", "/tmp/mcp-pdf-processing"))
CACHE_DIR.mkdir(exist_ok=True, parents=True)
def parse_pages_parameter(pages: Union[str, List[int], None]) -> Optional[List[int]]:
"""Parse pages parameter that might come as string or list"""
"""
Parse pages parameter from various formats into a list of 0-based integers.
User input is 1-based (page 1 = first page), converted to 0-based internally.
"""
if pages is None:
return None
if isinstance(pages, list):
return [int(p) for p in pages]
# Convert 1-based user input to 0-based internal representation
return [max(0, int(p) - 1) for p in pages]
if isinstance(pages, str):
try:
# Handle string representations like "[1, 2, 3]" or "1,2,3"
if pages.strip().startswith('[') and pages.strip().endswith(']'):
return ast.literal_eval(pages.strip())
page_list = ast.literal_eval(pages.strip())
elif ',' in pages:
return [int(p.strip()) for p in pages.split(',')]
page_list = [int(p.strip()) for p in pages.split(',')]
else:
return [int(pages.strip())]
page_list = [int(pages.strip())]
# Convert 1-based user input to 0-based internal representation
return [max(0, int(p) - 1) for p in page_list]
except (ValueError, SyntaxError):
raise ValueError(f"Invalid pages format: {pages}. Use format like [1,2,3] or 1,2,3")
raise ValueError(f"Invalid pages format: {pages}. Use 1-based page numbers like [1,2,3] or 1,2,3")
return None
@ -1282,22 +1290,25 @@ async def split_pdf(
path = await validate_pdf_path(pdf_path)
doc = fitz.open(str(path))
# Parse split points
# Parse split points (convert from 1-based user input to 0-based internal)
if isinstance(split_points, str):
try:
if ',' in split_points:
split_list = [int(p.strip()) for p in split_points.split(',')]
user_split_list = [int(p.strip()) for p in split_points.split(',')]
else:
split_list = [int(split_points.strip())]
user_split_list = [int(split_points.strip())]
# Convert to 0-based for internal processing
split_list = [max(0, p - 1) for p in user_split_list]
except ValueError:
return {"error": f"Invalid split points format: {split_points}. Use comma-separated numbers like '2,5,8'"}
return {"error": f"Invalid split points format: {split_points}. Use 1-based page numbers like '2,5,8'"}
else:
split_list = split_points
# Assume it's already parsed list, convert from 1-based to 0-based
split_list = [max(0, p - 1) for p in split_points]
# Sort and validate split points
# Sort and validate split points (now 0-based)
split_list = sorted(set(split_list))
page_count = len(doc)
split_list = [p for p in split_list if 0 < p < page_count] # Remove invalid pages
split_list = [p for p in split_list if 0 <= p < page_count] # Remove invalid pages
if not split_list:
return {"error": "No valid split points provided"}
@ -1341,7 +1352,7 @@ async def split_pdf(
return {
"original_file": str(path),
"original_page_count": page_count,
"split_points": split_list,
"split_points": [p + 1 for p in split_list], # Convert back to 1-based for display
"output_files": output_files,
"total_parts": len(output_files),
"split_time": round(time.time() - start_time, 2)
@ -1438,7 +1449,7 @@ async def rotate_pages(
Args:
pdf_path: Path to PDF file or HTTPS URL
pages: Page numbers to rotate (comma-separated), None for all pages
pages: Page numbers to rotate (comma-separated, 1-based), None for all pages
rotation: Rotation angle (90, 180, or 270 degrees)
output_filename: Name for the output file
@ -1509,7 +1520,7 @@ async def convert_to_images(
pdf_path: Path to PDF file or HTTPS URL
format: Output image format (png, jpeg, tiff)
dpi: Resolution for image conversion
pages: Page numbers to convert (comma-separated), None for all pages
pages: Page numbers to convert (comma-separated, 1-based), None for all pages
output_prefix: Prefix for output image files
Returns:
@ -2040,7 +2051,7 @@ async def summarize_content(
Args:
pdf_path: Path to PDF file or HTTPS URL
summary_length: Length of summary (short, medium, long)
pages: Specific pages to summarize (comma-separated), None for all pages
pages: Specific pages to summarize (comma-separated, 1-based), None for all pages
Returns:
Dictionary containing summary and key insights
@ -2220,7 +2231,7 @@ async def analyze_layout(
Args:
pdf_path: Path to PDF file or HTTPS URL
pages: Specific pages to analyze (comma-separated), None for all pages
pages: Specific pages to analyze (comma-separated, 1-based), None for all pages
include_coordinates: Whether to include detailed coordinate information
Returns:
@ -2428,7 +2439,7 @@ async def extract_charts(
Args:
pdf_path: Path to PDF file or HTTPS URL
pages: Specific pages to analyze (comma-separated), None for all pages
pages: Specific pages to analyze (comma-separated, 1-based), None for all pages
min_size: Minimum size (width or height) for chart detection in pixels
Returns:

View File

@ -13,18 +13,18 @@ sys.path.insert(0, 'src')
from mcp_pdf_tools.server import parse_pages_parameter
def test_page_parsing():
"""Test page parameter parsing"""
print("Testing page parameter parsing...")
"""Test page parameter parsing (1-based user input -> 0-based internal)"""
print("Testing page parameter parsing (1-based user input -> 0-based internal)...")
# Test different input formats
# Test different input formats - all converted from 1-based user input to 0-based internal
test_cases = [
(None, None),
("1,2,3", [1, 2, 3]),
("[2, 3]", [2, 3]), # This is the problematic case from the user
("5", [5]),
([0, 1, 2], [0, 1, 2]),
("0,1,2", [0, 1, 2]),
("[0,1,2]", [0, 1, 2])
("1,2,3", [0, 1, 2]), # 1-based input -> 0-based internal
("[2, 3]", [1, 2]), # This is the problematic case from the user
("5", [4]), # Page 5 becomes index 4
([1, 2, 3], [0, 1, 2]), # List input also converted
("2,3,4", [1, 2, 3]), # Pages 2,3,4 -> indexes 1,2,3
("[1,2,3]", [0, 1, 2]) # Another format
]
all_passed = True