Pdf Redaction Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Pdf Redaction Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A Model Context Protocol (MCP) server that provides comprehensive PDF redaction capabilities using FastMCP and pymupdf.
This MCP server enables LLMs to:
# Clone or download the project
cd pdf-redaction-mcp
# Install dependencies
uv sync
# Run the server
uv run pdf-redaction-mcppip install -e .
pdf-redaction-mcpThe server supports multiple transport modes and configurations via command-line flags:
# Show all available options
uv run pdf-redaction-mcp --help
# STDIO mode (default) - for desktop clients
uv run pdf-redaction-mcp
# SSE mode - for mobile apps and remote clients
uv run pdf-redaction-mcp --transport sse --port 8000
# HTTP mode - for web-based clients
uv run pdf-redaction-mcp --transport http --host 0.0.0.0 --port 8080
# With custom PDF directory (relative paths resolved against this)
uv run pdf-redaction-mcp --pdf-dir /path/to/pdfs
# Combined options
uv run pdf-redaction-mcp --transport sse --port 8000 --pdf-dir ~/Documents/pdfs#### Command-Line Options
--transport {stdio,http,sse}: Transport mode (default: stdio)--host HOST: Host to bind to for HTTP/SSE mode (default: 127.0.0.1)--port PORT: Port to listen on for HTTP/SSE mode (default: 8000)--pdf-dir PDF_DIR: Base directory for PDF files. Relative paths in tools will be resolved against this directory.All tools work with in-memory PDF documents using a session-based workflow:
load_pdfsave_pdfThis approach avoids repeated file I/O and allows multiple operations on the same document efficiently.
#### 1. load_pdf
Load a PDF file into memory for session-based operations.
Parameters:
pdf_path (str): Path to the PDF file to loaddocument_id (str, optional): Identifier for this document (defaults to filename)Returns: JSON with document_id and basic info
Example:
load_pdf(
pdf_path="/path/to/document.pdf",
document_id="my_doc"
)
# Returns: {"document_id": "my_doc", "pages": 10, "status": "loaded"}#### 2. save_pdf
Save an in-memory PDF document to disk.
Parameters:
document_id (str): Identifier of the loaded documentoutput_path (str): Path where the PDF will be savedReturns: JSON with save confirmation
Example:
save_pdf(
document_id="my_doc",
output_path="/path/to/output.pdf"
)#### 3. close_pdf
Close and remove an in-memory PDF document to free memory.
Parameters:
document_id (str): Identifier of the loaded documentReturns: JSON with close confirmation
Example:
close_pdf(document_id="my_doc")#### 4. list_loaded_pdfs
List all currently loaded PDF documents in memory.
Returns: JSON with information about all loaded documents
Example:
list_loaded_pdfs()
# Returns: {"total_documents": 2, "documents": [{...}, {...}]}#### 5. extract_text_from_pdf
Extract text from a loaded PDF document.
Parameters:
document_id (str): Identifier of the loaded documentpage_number (int, optional): Specific page to extract (0-indexed)format (str): Output format - "text", "json", or "blocks"Example:
# Load document first
load_pdf(pdf_path="/path/to/document.pdf", document_id="doc1")
# Extract all text
extract_text_from_pdf(
document_id="doc1",
format="text"
)
# Extract specific page as JSON
extract_text_from_pdf(
document_id="doc1",
page_number=0,
format="json"
)#### 6. search_text_in_pdf
Search for text patterns and get their locations in a loaded PDF document.
Parameters:
document_id (str): Identifier of the loaded documentsearch_string (str): Text or regex pattern to search forcase_sensitive (bool): Whether search should be case sensitiveuse_regex (bool): Whether to treat search_string as regexpage_number (int, optional): Specific page to searchExample:
# Load document first
load_pdf(pdf_path="/path/to/document.pdf", document_id="doc1")
# Search for email addresses using regex
search_text_in_pdf(
document_id="doc1",
search_string=r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
use_regex=True
)#### 7. redact_text_by_search
Automatically find and redact all occurrences of specified strings in a loaded PDF document.
Parameters:
document_id (str): Identifier of the loaded documentsearch_strings (List[str]): List of strings to redactfill_color (Tuple[float, float, float]): RGB colour (0-1 range)overlay_text (str): Optional text over redacted areatext_color (Tuple[float, float, float]): RGB colour for overlay textExample:
# Load document
load_pdf(pdf_path="/path/to/input.pdf", document_id="doc1")
# Redact sensitive information (modifies in-memory document)
redact_text_by_search(
document_id="doc1",
search_strings=["CONFIDENTIAL", "[email protected]", "123-45-6789"],
fill_color=(0, 0, 0), # Black
overlay_text="[REDACTED]"
)
# Save the redacted document
save_pdf(document_id="doc1", output_path="/path/to/redacted.pdf")#### 8. redact_by_coordinates
Redact specific areas by their exact coordinates in a loaded PDF document.
Parameters:
document_id (str): Identifier of the loaded documentredactions (List[Dict]): List of redaction areas with page, bbox, and optional textfill_color (Tuple[float, float, float]): RGB colouroverlay_text (str): Default overlay textExample:
# Load document
load_pdf(pdf_path="/path/to/input.pdf", document_id="doc1")
# Redact specific areas (modifies in-memory document)
redact_by_coordinates(
document_id="doc1",
redactions=[
{"page": 0, "bbox": [100, 100, 300, 150], "text": "REDACTED"},
{"page": 1, "bbox": [50, 200, 250, 250]}
],
fill_color=(0, 0, 0)
)
# Save the redacted document
save_pdf(document_id="doc1", output_path="/path/to/redacted.pdf")#### 9. redact_images_in_pdf
Remove all images from specified pages of a loaded PDF document.
Parameters:
document_id (str): Identifier of the loaded documentpage_numbers (List[int], optional): Pages to process (all if None)fill_color (Tuple[float, float, float]): RGB colouroverlay_text (str): Text over redacted imagesExample:
# Load document
load_pdf(pdf_path="/path/to/input.pdf", document_id="doc1")
# Redact all images on first two pages (modifies in-memory document)
redact_images_in_pdf(
document_id="doc1",
page_numbers=[0, 1],
overlay_text="[IMAGE REMOVED]"
)
# Save the redacted document
save_pdf(document_id="doc1", output_path="/path/to/no_images.pdf")#### 10. verify_redactions
Verify that redactions were applied correctly by comparing two loaded PDF documents.
Parameters:
original_document_id (str): Identifier of the original documentredacted_document_id (str): Identifier of the redacted documentsearch_strings (List[str], optional): Strings that should be goneExample:
# Load both documents
load_pdf(pdf_path="/path/to/original.pdf", document_id="original")
load_pdf(pdf_path="/path/to/redacted.pdf", document_id="redacted")
# Verify sensitive data was removed
verify_redactions(
original_document_id="original",
redacted_document_id="redacted",
search_strings=["CONFIDENTIAL", "[email protected]"]
)#### 11. get_pdf_info
Get metadata and structure information about a loaded PDF document.
Parameters:
document_id (str): Identifier of the loaded documentExample:
# Load document first
load_pdf(pdf_path="/path/to/document.pdf", document_id="doc1")
# Get PDF information
get_pdf_info(document_id="doc1")This section covers how to configure the PDF Redaction MCP Server with various MCP clients.
Quick Links:
Add to your claude_desktop_config.json:
Basic Configuration (STDIO mode):
{
"mcpServers": {
"pdf-redaction": {
"command": "uv",
"args": [
"--directory",
"/path/to/pdf-redaction-mcp",
"run",
"pdf-redaction-mcp"
]
}
}
}With Custom PDF Directory:
{
"mcpServers": {
"pdf-redaction": {
"command": "uv",
"args": [
"--directory",
"/path/to/pdf-redaction-mcp",
"run",
"pdf-redaction-mcp",
"--pdf-dir",
"/Users/yourname/Documents/PDFs"
]
}
}
}This allows you to use relative paths like "document.pdf" instead of full paths.
Add to your .cursor/mcp.json:
{
"mcpServers": {
"pdf-redaction": {
"command": "uv",
"args": [
"--directory",
"/path/to/pdf-redaction-mcp",
"run",
"pdf-redaction-mcp"
]
}
}
}Add to your Cline MCP settings:
{
"mcpServers": {
"pdf-redaction": {
"command": "uv",
"args": [
"--directory",
"/path/to/pdf-redaction-mcp",
"run",
"pdf-redaction-mcp",
"--pdf-dir",
"${workspaceFolder}/pdfs"
]
}
}
}For any MCP client supporting STDIO transport, use:
Command: uv
Args:
--directory /path/to/pdf-redaction-mcp
run
pdf-redaction-mcp
[optional flags like --pdf-dir]For production deployments, you can use environment variables:
# Set PDF directory via environment
export PDF_DIR=/var/pdfs
# Then reference in your startup script
uv run pdf-redaction-mcp --pdf-dir "$PDF_DIR"Example 1: Personal Use with Claude Desktop
Store all PDFs in your Documents folder:
{
"mcpServers": {
"pdf-redaction": {
"command": "uv",
"args": [
"--directory",
"/Users/yourname/workspace/pdf-redaction-mcp",
"run",
"pdf-redaction-mcp",
"--pdf-dir",
"/Users/yourname/Documents"
]
}
}
}Now you can say: "Redact emails from report.pdf" instead of using full paths.
Example 2: Team Deployment with Shared PDFs
Deploy remotely with network-mounted PDF storage:
# On your server
uv run pdf-redaction-mcp \
--transport sse \
--host 0.0.0.0 \
--port 8000 \
--pdf-dir /mnt/shared-pdfsTeam members configure their clients to use the remote server.
Example 3: Development Setup
Use project-relative paths during development:
{
"mcpServers": {
"pdf-redaction": {
"command": "uv",
"args": [
"--directory",
"${workspaceFolder}/pdf-redaction-mcp",
"run",
"pdf-redaction-mcp",
"--pdf-dir",
"${workspaceFolder}/test-pdfs"
]
}
}
}Session-based workflow (new approach):
User: "Please redact all email addresses and phone numbers from report.pdf"
1. LLM loads the document:
load_pdf(pdf_path="report.pdf", document_id="report")
2. LLM searches for patterns:
search_text_in_pdf(
document_id="report",
search_string=r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
use_regex=True
)
3. LLM redacts in-memory:
redact_text_by_search(
document_id="report",
search_strings=["[email protected]", "555-123-4567", ...]
)
4. LLM saves the result:
save_pdf(document_id="report", output_path="report_redacted.pdf")
5. LLM reports: "Successfully redacted 5 email addresses and 3 phone numbers"Benefits of session-based approach:
1. User: "Redact the financial table on page 3 of the report"
2. LLM loads document:
load_pdf(pdf_path="report.pdf", document_id="report")
3. LLM extracts page structure:
extract_text_from_pdf(document_id="report", page_number=2, format="blocks")
4. LLM identifies table coordinates from block structure
5. LLM redacts in-memory:
redact_by_coordinates(
document_id="report",
redactions=[{"page": 2, "bbox": [100, 200, 500, 400]}]
)
6. LLM verifies by extracting text again:
extract_text_from_pdf(document_id="report", page_number=2)
7. LLM saves:
save_pdf(document_id="report", output_path="report_redacted.pdf")1. User: "Remove all images from the document but keep the text"
2. LLM loads document:
load_pdf(pdf_path="document.pdf", document_id="doc")
3. LLM checks for images:
get_pdf_info(document_id="doc")
4. LLM redacts images:
redact_images_in_pdf(document_id="doc")
5. LLM verifies and saves:
get_pdf_info(document_id="doc") # Verify images are gone
save_pdf(document_id="doc", output_path="document_no_images.pdf")
6. LLM cleans up:
close_pdf(document_id="doc") # Free memory1. User: "Redact all SSNs, then verify they're gone, then redact names too"
2. LLM loads document:
load_pdf(pdf_path="sensitive.pdf", document_id="sensitive")
3. LLM redacts SSNs:
redact_text_by_search(
document_id="sensitive",
search_strings=[r"\d{3}-\d{2}-\d{4}"],
use_regex=True
)
4. LLM creates checkpoint by saving:
save_pdf(document_id="sensitive", output_path="sensitive_step1.pdf")
5. LLM loads original for comparison:
load_pdf(pdf_path="sensitive.pdf", document_id="original")
6. LLM verifies:
verify_redactions(
original_document_id="original",
redacted_document_id="sensitive",
search_strings=["123-45-6789", "987-65-4321"]
)
7. LLM continues with name redaction:
redact_text_by_search(
document_id="sensitive",
search_strings=["John Doe", "Jane Smith"]
)
8. LLM saves final version:
save_pdf(document_id="sensitive", output_path="sensitive_final.pdf")
9. LLM cleans up:
close_pdf(document_id="original")
close_pdf(document_id="sensitive")
---
## Troubleshooting
### Claude Desktop Connection Issues
**Problem:** MCP server not connecting in Claude Desktop
**Solutions:**
1. Verify the path in `claude_desktop_config.json` is correct:ls -la /path/to/pdf-redaction-mcp
2. Test the server manually:cd /path/to/pdf-redaction-mcp uv run pdf-redaction-mcp --help
3. Check Claude Desktop logs:
- macOS: `~/Library/Logs/Claude/`
- Windows: `%APPDATA%\Claude\logs\`
- Linux: `~/.config/Claude/logs/`
### PDF Path Issues
**Problem:** "File not found" errors when using relative paths
**Solution:** Configure `--pdf-dir` flag in your MCP client config:{ "mcpServers": { "pdf-redaction": { "command": "uv", "args": [ "--directory", "/path/to/pdf-redaction-mcp", "run", "pdf-redaction-mcp", "--pdf-dir", "/your/pdf/directory" ] } } }
### Port Already in Use (HTTP/SSE mode)
**Problem:** `Address already in use` error when starting server
**Solution:**
1. Use a different port:uv run pdf-redaction-mcp --transport sse --port 8001
2. Or find and kill the process using the port:lsof -ti:8000 | xargs kill -9
netstat -ano | findstr :8000 taskkill /PID <PID> /F
### UV Not Found
**Problem:** `uv: command not found`
**Solution:** Install UV package manager:curl -LsSf https://astral.sh/uv/install.sh | sh
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
pip install uv
---
## Development
### Running Tests
uv run pytest
### Project Structure
pdf-redaction-mcp/ ├── src/ │ └── pdf_redaction_mcp/ │ ├── __init__.py │ └── server.py # Main MCP server implementation ├── tests/ │ └── test_server.py # Unit tests ├── pyproject.toml # Project dependencies └── README.md # This file
## Technical Details
### Redaction Implementation
The server uses pymupdf's redaction annotations, which:
1. **Add redaction annotations** to mark areas for removal
2. **Apply redactions** to permanently remove content
3. **Cannot be undone** once saved - content is truly deleted from PDF structure
### Colour Format
Colours are specified as RGB tuples with values from 0 to 1:
- Black: `(0, 0, 0)`
- White: `(1, 1, 1)`
- Red: `(1, 0, 0)`
- Green: `(0, 1, 0)`
- Blue: `(0, 0, 1)`
### Coordinate System
PDF coordinates use bottom-left origin:
- `x0, y0`: Bottom-left corner of rectangle
- `x1, y1`: Top-right corner of rectangle
Bounding boxes: `[x0, y0, x1, y1]`
## Security Considerations
1. **Permanent Removal**: Redactions permanently remove content from PDF structure
2. **Verify Redactions**: Always use `verify_redactions` to confirm sensitive data is gone
3. **Backup Original**: Keep original files backed up before redacting
4. **File Paths**: Ensure proper file path validation in production
5. **Access Control**: Implement appropriate access controls for sensitive documents
## Limitations
- Only works with PDF files (use pymupdf's supported formats)
- Encrypted PDFs may require password authentication
- Very large PDFs may require significant memory
- Redactions are permanent once saved
## Contributing
Contributions are welcome! Please ensure:
1. Code follows existing style
2. Tests pass (`uv run pytest`)
3. Documentation is updated
4. Commit messages are clear
## Licence
MIT Licence - see LICENCE file for details
## Acknowledgements
- [FastMCP](https://github.com/jlowin/fastmcp) - MCP framework
- [pymupdf](https://pymupdf.readthedocs.io/) - PDF manipulation library
- [Model Context Protocol](https://modelcontextprotocol.io/) - MCP specification
## Support
For issues, questions, or contributions:
- Open an issue on GitHub
- Check the [FastMCP documentation](https://github.com/jlowin/fastmcp)
- Check the [pymupdf documentation](https://pymupdf.readthedocs.io/)~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.