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:
parent
f0365a0d75
commit
f601d44d99
11
.mcp.json
Normal file
11
.mcp.json
Normal 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
88
CLAUDE_DESKTOP_SETUP.md
Normal 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
|
||||
```
|
15
README.md
15
README.md
@ -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
|
||||
)
|
||||
|
||||
|
16
claude_desktop_config.json
Normal file
16
claude_desktop_config.json
Normal 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
3
mcp-pdf-tools-launcher.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd /home/rpm/claude/mcp-pdf-tools
|
||||
exec uv run mcp-pdf-tools "$@"
|
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user