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
|
- **Format Conversion**: Convert PDFs to clean Markdown format
|
||||||
- **URL Support**: Process PDFs directly from HTTPS URLs with intelligent caching
|
- **URL Support**: Process PDFs directly from HTTPS URLs with intelligent caching
|
||||||
- **Smart Detection**: Automatically detect the best method for each operation
|
- **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
|
## URL Support
|
||||||
|
|
||||||
@ -133,7 +134,7 @@ result = await extract_text(
|
|||||||
# Extract specific pages with layout preservation
|
# Extract specific pages with layout preservation
|
||||||
result = await extract_text(
|
result = await extract_text(
|
||||||
pdf_path="/path/to/document.pdf",
|
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,
|
preserve_layout=True,
|
||||||
method="pdfplumber" # Or "auto", "pymupdf", "pypdf"
|
method="pdfplumber" # Or "auto", "pymupdf", "pypdf"
|
||||||
)
|
)
|
||||||
@ -150,7 +151,7 @@ result = await extract_tables(
|
|||||||
# Extract tables from specific pages in markdown format
|
# Extract tables from specific pages in markdown format
|
||||||
result = await extract_tables(
|
result = await extract_tables(
|
||||||
pdf_path="/path/to/document.pdf",
|
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"
|
output_format="markdown" # Or "json", "csv"
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@ -231,13 +232,13 @@ result = await classify_content(
|
|||||||
result = await summarize_content(
|
result = await summarize_content(
|
||||||
pdf_path="/path/to/document.pdf",
|
pdf_path="/path/to/document.pdf",
|
||||||
summary_length="medium", # "short", "medium", "long"
|
summary_length="medium", # "short", "medium", "long"
|
||||||
pages="1,2,3" # Specific pages
|
pages="1,2,3" # Specific pages (1-based numbering)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Analyze page layout
|
# Analyze page layout
|
||||||
result = await analyze_layout(
|
result = await analyze_layout(
|
||||||
pdf_path="/path/to/document.pdf",
|
pdf_path="/path/to/document.pdf",
|
||||||
pages="1,2,3",
|
pages="1,2,3", # Specific pages (1-based numbering)
|
||||||
include_coordinates=True
|
include_coordinates=True
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@ -253,7 +254,7 @@ result = await extract_form_data(
|
|||||||
# Split PDF into separate files
|
# Split PDF into separate files
|
||||||
result = await split_pdf(
|
result = await split_pdf(
|
||||||
pdf_path="/path/to/document.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"
|
output_prefix="section"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -266,7 +267,7 @@ result = await merge_pdfs(
|
|||||||
# Rotate specific pages
|
# Rotate specific pages
|
||||||
result = await rotate_pages(
|
result = await rotate_pages(
|
||||||
pdf_path="/path/to/document.pdf",
|
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
|
# Extract charts and diagrams
|
||||||
result = await extract_charts(
|
result = await extract_charts(
|
||||||
pdf_path="/path/to/report.pdf",
|
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
|
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)
|
CACHE_DIR.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
def parse_pages_parameter(pages: Union[str, List[int], None]) -> Optional[List[int]]:
|
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:
|
if pages is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(pages, list):
|
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):
|
if isinstance(pages, str):
|
||||||
try:
|
try:
|
||||||
# Handle string representations like "[1, 2, 3]" or "1,2,3"
|
# Handle string representations like "[1, 2, 3]" or "1,2,3"
|
||||||
if pages.strip().startswith('[') and pages.strip().endswith(']'):
|
if pages.strip().startswith('[') and pages.strip().endswith(']'):
|
||||||
return ast.literal_eval(pages.strip())
|
page_list = ast.literal_eval(pages.strip())
|
||||||
elif ',' in pages:
|
elif ',' in pages:
|
||||||
return [int(p.strip()) for p in pages.split(',')]
|
page_list = [int(p.strip()) for p in pages.split(',')]
|
||||||
else:
|
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):
|
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
|
return None
|
||||||
|
|
||||||
@ -1282,22 +1290,25 @@ async def split_pdf(
|
|||||||
path = await validate_pdf_path(pdf_path)
|
path = await validate_pdf_path(pdf_path)
|
||||||
doc = fitz.open(str(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):
|
if isinstance(split_points, str):
|
||||||
try:
|
try:
|
||||||
if ',' in split_points:
|
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:
|
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:
|
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:
|
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))
|
split_list = sorted(set(split_list))
|
||||||
page_count = len(doc)
|
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:
|
if not split_list:
|
||||||
return {"error": "No valid split points provided"}
|
return {"error": "No valid split points provided"}
|
||||||
@ -1341,7 +1352,7 @@ async def split_pdf(
|
|||||||
return {
|
return {
|
||||||
"original_file": str(path),
|
"original_file": str(path),
|
||||||
"original_page_count": page_count,
|
"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,
|
"output_files": output_files,
|
||||||
"total_parts": len(output_files),
|
"total_parts": len(output_files),
|
||||||
"split_time": round(time.time() - start_time, 2)
|
"split_time": round(time.time() - start_time, 2)
|
||||||
@ -1438,7 +1449,7 @@ async def rotate_pages(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
pdf_path: Path to PDF file or HTTPS URL
|
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)
|
rotation: Rotation angle (90, 180, or 270 degrees)
|
||||||
output_filename: Name for the output file
|
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
|
pdf_path: Path to PDF file or HTTPS URL
|
||||||
format: Output image format (png, jpeg, tiff)
|
format: Output image format (png, jpeg, tiff)
|
||||||
dpi: Resolution for image conversion
|
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
|
output_prefix: Prefix for output image files
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -2040,7 +2051,7 @@ async def summarize_content(
|
|||||||
Args:
|
Args:
|
||||||
pdf_path: Path to PDF file or HTTPS URL
|
pdf_path: Path to PDF file or HTTPS URL
|
||||||
summary_length: Length of summary (short, medium, long)
|
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:
|
Returns:
|
||||||
Dictionary containing summary and key insights
|
Dictionary containing summary and key insights
|
||||||
@ -2220,7 +2231,7 @@ async def analyze_layout(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
pdf_path: Path to PDF file or HTTPS URL
|
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
|
include_coordinates: Whether to include detailed coordinate information
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -2428,7 +2439,7 @@ async def extract_charts(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
pdf_path: Path to PDF file or HTTPS URL
|
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
|
min_size: Minimum size (width or height) for chart detection in pixels
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -13,18 +13,18 @@ sys.path.insert(0, 'src')
|
|||||||
from mcp_pdf_tools.server import parse_pages_parameter
|
from mcp_pdf_tools.server import parse_pages_parameter
|
||||||
|
|
||||||
def test_page_parsing():
|
def test_page_parsing():
|
||||||
"""Test page parameter parsing"""
|
"""Test page parameter parsing (1-based user input -> 0-based internal)"""
|
||||||
print("Testing page parameter parsing...")
|
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 = [
|
test_cases = [
|
||||||
(None, None),
|
(None, None),
|
||||||
("1,2,3", [1, 2, 3]),
|
("1,2,3", [0, 1, 2]), # 1-based input -> 0-based internal
|
||||||
("[2, 3]", [2, 3]), # This is the problematic case from the user
|
("[2, 3]", [1, 2]), # This is the problematic case from the user
|
||||||
("5", [5]),
|
("5", [4]), # Page 5 becomes index 4
|
||||||
([0, 1, 2], [0, 1, 2]),
|
([1, 2, 3], [0, 1, 2]), # List input also converted
|
||||||
("0,1,2", [0, 1, 2]),
|
("2,3,4", [1, 2, 3]), # Pages 2,3,4 -> indexes 1,2,3
|
||||||
("[0,1,2]", [0, 1, 2])
|
("[1,2,3]", [0, 1, 2]) # Another format
|
||||||
]
|
]
|
||||||
|
|
||||||
all_passed = True
|
all_passed = True
|
||||||
|
Loading…
x
Reference in New Issue
Block a user