#
tokens: 11632/50000 16/16 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── claude_desktop_config.json
├── claude_document_mcp
│   ├── __init__.py
│   ├── __main__.py
│   └── server.py
├── CLAUDE_INTEGRATION.md
├── pyproject.toml
├── README.md
├── requirements.txt
├── run.py
├── run.sh
├── setup.sh
├── start_server.sh
├── test_client.py
├── test_server.sh
├── uv.lock
└── verify.py
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Python
 2 | __pycache__/
 3 | *.py[cod]
 4 | *$py.class
 5 | *.so
 6 | .Python
 7 | .venv/
 8 | env/
 9 | build/
10 | develop-eggs/
11 | dist/
12 | downloads/
13 | eggs/
14 | .eggs/
15 | lib/
16 | lib64/
17 | parts/
18 | sdist/
19 | var/
20 | *.egg-info/
21 | .installed.cfg
22 | *.egg
23 | 
24 | # Environment variables
25 | .env
26 | .env.*
27 | 
28 | # Logs
29 | logs/
30 | *.log
31 | 
32 | # macOS
33 | .DS_Store
34 | .AppleDouble
35 | .LSOverride
36 | ._*
37 | 
38 | # VS Code
39 | .vscode/
40 | 
41 | # PyCharm
42 | .idea/
43 | 
44 | # Jupiter
45 | .ipynb_checkpoints
46 | 
47 | # Testing
48 | .pytest_cache/
49 | htmlcov/
50 | .coverage
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Claude Document MCP Server
  2 | 
  3 | A Model Context Protocol (MCP) server that allows Claude Desktop to perform document operations on Microsoft Word, Excel, and PDF files.
  4 | 
  5 | ## Features
  6 | 
  7 | ### Microsoft Word Operations
  8 | - Create new Word documents from text
  9 | - Edit existing Word documents (add/edit/delete paragraphs and headings)
 10 | - Convert text files (.txt) to Word documents
 11 | 
 12 | ### Excel Operations
 13 | - Create new Excel spreadsheets from JSON or CSV-like text
 14 | - Edit existing Excel files (update cells, ranges, add/delete rows, columns, sheets)
 15 | - Convert CSV files to Excel
 16 | 
 17 | ### PDF Operations
 18 | - Create new PDF files from text
 19 | - Convert Word documents to PDF files
 20 | 
 21 | ## Setup
 22 | 
 23 | This MCP server requires Python 3.10 or higher.
 24 | 
 25 | ### Automatic Setup (Recommended)
 26 | 
 27 | Run the setup script to automatically install dependencies and configure for Claude Desktop:
 28 | 
 29 | ```bash
 30 | git clone https://github.com/alejandroBallesterosC/document-edit-mcp
 31 | cd document-edit-mcp
 32 | ./setup.sh
 33 | ```
 34 | 
 35 | This will:
 36 | 1. Create a virtual environment
 37 | 2. Install required dependencies
 38 | 3. Configure the server for Claude Desktop
 39 | 4. Create necessary directories
 40 | 
 41 | ### Manual Setup
 42 | 
 43 | If you prefer to set up manually:
 44 | 
 45 | 1. Install dependencies:
 46 | 
 47 | ```bash
 48 | cd claude-document-mcp
 49 | python -m venv .venv
 50 | source .venv/bin/activate  # On Windows: .venv\Scripts\activate
 51 | pip install -e .
 52 | ```
 53 | 
 54 | 2. Configure Claude Desktop:
 55 | 
 56 | Copy the `claude_desktop_config.json` file to:
 57 | - **Mac**: `~/Library/Application Support/Claude/`
 58 | - **Windows**: `%APPDATA%\Claude\`
 59 | 
 60 | 3. Restart Claude Desktop
 61 | 
 62 | ## Model Context Protocol Integration
 63 | 
 64 | This server follows the Model Context Protocol specification to provide document manipulation capabilities for Claude Desktop:
 65 | 
 66 | - **Tools**: Provides manipulations functions for Word, Excel, and PDF operations
 67 | - **Resources**: Provides information about capabilities
 68 | - **Prompts**: (none currently implemented)
 69 | 
 70 | ## API Reference
 71 | 
 72 | ### Microsoft Word
 73 | 
 74 | #### Create a Word Document
 75 | ```
 76 | create_word_document(filepath: str, content: str) -> Dict
 77 | ```
 78 | 
 79 | #### Edit a Word Document
 80 | ```
 81 | edit_word_document(filepath: str, operations: List[Dict]) -> Dict
 82 | ```
 83 | 
 84 | #### Convert TXT to Word
 85 | ```
 86 | convert_txt_to_word(source_path: str, target_path: str) -> Dict
 87 | ```
 88 | 
 89 | ### Excel
 90 | 
 91 | #### Create an Excel File
 92 | ```
 93 | create_excel_file(filepath: str, content: str) -> Dict
 94 | ```
 95 | 
 96 | #### Edit an Excel File
 97 | ```
 98 | edit_excel_file(filepath: str, operations: List[Dict]) -> Dict
 99 | ```
100 | 
101 | #### Convert CSV to Excel
102 | ```
103 | convert_csv_to_excel(source_path: str, target_path: str) -> Dict
104 | ```
105 | 
106 | ### PDF
107 | 
108 | #### Create a PDF File
109 | ```
110 | create_pdf_file(filepath: str, content: str) -> Dict
111 | ```
112 | 
113 | #### Convert Word to PDF
114 | ```
115 | convert_word_to_pdf(source_path: str, target_path: str) -> Dict
116 | ```
117 | 
118 | ## Logs
119 | 
120 | The server logs all operations to both the console and a `logs/document_mcp.log` file for troubleshooting.
121 | 
122 | ## License
123 | 
124 | MIT
125 | 
126 | ## Contributing
127 | 
128 | Contributions are welcome! Please feel free to submit a Pull Request.
129 | 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
1 | uvicorn
2 | fastapi
3 | python-docx
4 | pandas
5 | openpyxl
6 | reportlab
7 | pdf2docx
8 | docx2pdf
9 | 
```

--------------------------------------------------------------------------------
/claude_document_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | Claude Document MCP - A Model Context Protocol server for document operations
3 | """
4 | 
5 | __version__ = "0.1.0"
6 | 
```

--------------------------------------------------------------------------------
/claude_document_mcp/__main__.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Claude Document MCP - CLI entry point
 3 | """
 4 | 
 5 | import sys
 6 | from claude_document_mcp.server import main
 7 | 
 8 | if __name__ == "__main__":
 9 |     sys.exit(main())
10 | 
```

--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | """
 3 | Run script for Claude Document MCP Server
 4 | """
 5 | 
 6 | from claude_document_mcp.server import main
 7 | 
 8 | if __name__ == "__main__":
 9 |     main()
10 | 
```

--------------------------------------------------------------------------------
/run.sh:
--------------------------------------------------------------------------------

```bash
1 | #!/bin/bash
2 | # Run the Claude Document MCP Server directly
3 | 
4 | # Get the project directory
5 | PROJECT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
6 | 
7 | # Run the server with UV (using --project parameter)
8 | echo "Starting Document MCP Server..."
9 | uv run --project "$PROJECT_DIR" -m claude_document_mcp.server
```

--------------------------------------------------------------------------------
/claude_desktop_config.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "mcpServers": {
 3 |     "document_operations": {
 4 |       "command": "python",
 5 |       "args": [
 6 |         "-m", 
 7 |         "claude_document_mcp.server"
 8 |       ],
 9 |       "cwd": "/Users/jandro/Documents/Coding-Projects/mcp-servers/claude-document-mcp",
10 |       "env": {
11 |         "PYTHONUNBUFFERED": "1"
12 |       }
13 |     }
14 |   }
15 | }
16 | 
```

--------------------------------------------------------------------------------
/start_server.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Start the Claude Document MCP Server
 4 | 
 5 | # Activate the virtual environment
 6 | if [ -d ".venv" ]; then
 7 |     source .venv/bin/activate
 8 | else
 9 |     echo "Creating virtual environment with UV..."
10 |     uv venv
11 |     source .venv/bin/activate
12 |     
13 |     echo "Installing dependencies with UV..."
14 |     uv pip install -e .
15 | fi
16 | 
17 | # Start the server
18 | echo "Starting Document MCP Server..."
19 | python -m claude_document_mcp
20 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [build-system]
 2 | requires = ["hatchling"]
 3 | build-backend = "hatchling.build"
 4 | 
 5 | [project]
 6 | name = "claude-document-mcp"
 7 | version = "0.1.0"
 8 | description = "Model Context Protocol server for document operations with Claude Desktop"
 9 | readme = "README.md"
10 | requires-python = ">=3.10"
11 | license = { text = "MIT" }
12 | authors = [
13 |     { name = "Your Name", email = "[email protected]" }
14 | ]
15 | 
16 | dependencies = [
17 |     "mcp[cli]>=1.5.0",
18 |     "python-docx>=0.8.11",
19 |     "pandas>=2.0.0",
20 |     "openpyxl>=3.1.0",
21 |     "reportlab>=3.6.0",
22 |     "pdf2docx>=0.5.6",
23 |     "docx2pdf>=0.1.8"
24 | ]
25 | 
26 | [project.optional-dependencies]
27 | dev = [
28 |     "pytest>=7.0.0",
29 |     "black>=22.0.0",
30 |     "isort>=5.10.0"
31 | ]
32 | 
33 | [tool.hatch.build.targets.wheel]
34 | packages = ["claude_document_mcp"]
35 | 
```

--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | # Setup script for Claude Document MCP Server
 3 | 
 4 | set -e  # Exit on error
 5 | 
 6 | echo "Setting up Claude Document MCP Server..."
 7 | 
 8 | # Check Python version
 9 | python_version=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
10 | min_version="3.10"
11 | 
12 | if [ "$(printf '%s\n' "$min_version" "$python_version" | sort -V | head -n1)" != "$min_version" ]; then 
13 |     echo "Error: Python $min_version or higher is required"
14 |     echo "Current version: $python_version"
15 |     exit 1
16 | fi
17 | 
18 | # Create virtual environment first (using UV)
19 | echo "Creating virtual environment with UV..."
20 | uv sync
21 | 
22 | # Now install the project in development mode
23 | echo "Installing project in development mode with UV..."
24 | uv pip install -e .
25 | 
26 | # Create logs directory
27 | mkdir -p logs
28 | 
29 | # Get the absolute path to the project directory
30 | PROJECT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
31 | 
32 | echo ""
33 | echo "Setup complete! You can now use the Document MCP Server with Claude Desktop."
34 | echo "Start Claude Desktop to use the document tools."
35 | echo ""
```

--------------------------------------------------------------------------------
/test_server.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # This script tests the Document MCP Server by running it and confirming it works
 4 | 
 5 | # Create temporary directory for test files
 6 | TEST_DIR=$(mktemp -d)
 7 | echo "Created test directory: $TEST_DIR"
 8 | 
 9 | # Create a test text file
10 | TEXT_FILE="$TEST_DIR/test.txt"
11 | echo "This is a test text file." > "$TEXT_FILE"
12 | echo "It has multiple lines." >> "$TEXT_FILE"
13 | echo "We'll convert it to Word." >> "$TEXT_FILE"
14 | echo "Created text file: $TEXT_FILE"
15 | 
16 | # The main test process
17 | # This will start the server in the background and run tests against it
18 | echo "Starting Document MCP Server validation..."
19 | 
20 | # Start the server with the MCP dev tool (this includes the inspector)
21 | echo "Starting server..."
22 | mcp dev claude_document_mcp/server.py:mcp &
23 | SERVER_PID=$!
24 | 
25 | # Wait for server to start
26 | sleep 3
27 | 
28 | echo "Server started with PID: $SERVER_PID"
29 | echo "To view the server in the MCP Inspector, visit: http://localhost:9000"
30 | echo ""
31 | echo "Press Ctrl+C to stop the server when you're done testing"
32 | 
33 | # Keep the script running so the server stays up
34 | wait $SERVER_PID
35 | 
36 | # Clean up
37 | echo "Cleaning up test files..."
38 | rm -rf "$TEST_DIR"
39 | echo "Done."
40 | 
```

--------------------------------------------------------------------------------
/CLAUDE_INTEGRATION.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Integrating with Claude Desktop
 2 | 
 3 | To integrate this Document MCP Server with Claude Desktop, follow these steps:
 4 | 
 5 | ## 1. Install the Server
 6 | 
 7 | First, make sure you have installed the server with dependencies:
 8 | 
 9 | ```bash
10 | cd claude-document-mcp
11 | uv venv
12 | source .venv/bin/activate  # On Windows: .venv\Scripts\activate
13 | uv pip install -e .
14 | ```
15 | 
16 | ## 2. Configure Claude Desktop
17 | 
18 | 1. Find your Claude Desktop configuration file:
19 |    - **Mac**: `~/Library/Application Support/Claude/claude_desktop_config.json`
20 |    - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
21 | 
22 | 2. Create or edit this file to include the following configuration:
23 | 
24 | ```json
25 | {
26 |   "mcpServers": {
27 |     "document_operations": {
28 |       "command": "uv",
29 |       "args": [
30 |         "--directory", "/ABSOLUTE/PATH/TO/claude-document-mcp", 
31 |         "run", 
32 |         "mcp", 
33 |         "run", 
34 |         "claude_document_mcp/server.py:mcp"
35 |       ]
36 |     }
37 |   }
38 | }
39 | ```
40 | 
41 | Replace `/ABSOLUTE/PATH/TO/claude-document-mcp` with the actual absolute path to your project directory.
42 | 
43 | 3. Restart Claude Desktop
44 | 
45 | ## 3. Test the Integration
46 | 
47 | 1. Open Claude Desktop
48 | 2. You should see a hammer icon in the UI if the MCP server is detected
49 | 3. Try a test request like: "Can you create a new Word document with a simple hello world message and save it to my Desktop?"
50 | 
51 | ## Troubleshooting
52 | 
53 | If Claude Desktop doesn't detect the server:
54 | 
55 | 1. Check the logs directory for errors
56 | 2. Verify your Claude Desktop config file has the correct path
57 | 3. Make sure the MCP server runs correctly on its own (use `./test_server.sh`)
58 | 4. Restart Claude Desktop after any changes
59 | 
```

--------------------------------------------------------------------------------
/test_client.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Simple test client for the Document MCP Server
  4 | """
  5 | 
  6 | import asyncio
  7 | import json
  8 | import os
  9 | import sys
 10 | import tempfile
 11 | from pathlib import Path
 12 | from contextlib import asynccontextmanager
 13 | 
 14 | from mcp import ClientSession
 15 | from mcp.client.stdio import StdioServerParameters, stdio_client
 16 | 
 17 | 
 18 | async def test_word_operations(session):
 19 |     print("\n\nTesting Word operations...")
 20 |     
 21 |     # Create a temp directory for test files
 22 |     with tempfile.TemporaryDirectory() as temp_dir:
 23 |         # Create a text file
 24 |         txt_path = os.path.join(temp_dir, "test.txt")
 25 |         with open(txt_path, "w") as f:
 26 |             f.write("This is a test text file.\nIt has multiple lines.\nWe'll convert it to Word.")
 27 |         
 28 |         # Create a Word document
 29 |         word_path = os.path.join(temp_dir, "test_created.docx")
 30 |         
 31 |         print("Creating Word document...")
 32 |         result = await session.call_tool(
 33 |             "create_word_document",
 34 |             {"filepath": word_path, "content": "This is a test Word document created by the MCP server."}
 35 |         )
 36 |         print(f"Result: {json.dumps(result, indent=2)}")
 37 |         
 38 |         if result.get("success"):
 39 |             # Edit the Word document
 40 |             print("\nEditing Word document...")
 41 |             result = await session.call_tool(
 42 |                 "edit_word_document",
 43 |                 {
 44 |                     "filepath": word_path,
 45 |                     "operations": [
 46 |                         {"type": "add_paragraph", "text": "This is a new paragraph."},
 47 |                         {"type": "add_heading", "text": "Test Heading", "level": 1}
 48 |                     ]
 49 |                 }
 50 |             )
 51 |             print(f"Result: {json.dumps(result, indent=2)}")
 52 |         
 53 |         # Convert text to Word
 54 |         word_converted_path = os.path.join(temp_dir, "test_converted.docx")
 55 |         print("\nConverting TXT to Word...")
 56 |         result = await session.call_tool(
 57 |             "convert_txt_to_word",
 58 |             {"source_path": txt_path, "target_path": word_converted_path}
 59 |         )
 60 |         print(f"Result: {json.dumps(result, indent=2)}")
 61 | 
 62 | 
 63 | async def test_capabilities(session):
 64 |     print("\n\nGetting server capabilities...")
 65 |     result = await session.get_resource("capabilities://")
 66 |     print(f"Capabilities: {json.dumps(json.loads(result), indent=2)}")
 67 | 
 68 | 
 69 | @asynccontextmanager
 70 | async def connect_to_server():
 71 |     """Helper to connect to the MCP server."""
 72 |     server_params = StdioServerParameters(
 73 |         command="python",
 74 |         args=["-m", "claude_document_mcp.server"],
 75 |         cwd=os.path.dirname(os.path.abspath(__file__))
 76 |     )
 77 |     
 78 |     client = stdio_client(server_params)
 79 |     session = await client.connect()
 80 |     
 81 |     try:
 82 |         yield session
 83 |     finally:
 84 |         await session.close()
 85 | 
 86 | 
 87 | async def main():
 88 |     print("Document MCP Server Test Client")
 89 |     print("===============================")
 90 |     
 91 |     try:
 92 |         # Connect to the MCP server using stdio
 93 |         print("Connecting to server...")
 94 |         
 95 |         async with connect_to_server() as session:
 96 |             print("Connected!")
 97 |             
 98 |             # Get server capabilities
 99 |             await test_capabilities(session)
100 |             
101 |             # Test Word operations
102 |             await test_word_operations(session)
103 |             
104 |             print("\nAll tests completed!")
105 |     except Exception as e:
106 |         print(f"Error: {str(e)}")
107 |         import traceback
108 |         traceback.print_exc()
109 | 
110 | 
111 | if __name__ == "__main__":
112 |     asyncio.run(main())
113 | 
```

--------------------------------------------------------------------------------
/verify.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Verification script for Claude Document MCP Server
  4 | 
  5 | This script verifies that the environment is correctly set up and that
  6 | all dependencies are installed properly.
  7 | """
  8 | 
  9 | import sys
 10 | import importlib
 11 | import os
 12 | import subprocess
 13 | from pathlib import Path
 14 | 
 15 | def run_uv_command(args):
 16 |     """Run a UV command and return the output."""
 17 |     cmd = ["uv"] + args
 18 |     try:
 19 |         return subprocess.check_output(cmd, universal_newlines=True)
 20 |     except subprocess.CalledProcessError as e:
 21 |         print(f"Error running UV command: {e}")
 22 |         return None
 23 | 
 24 | def check_dependency(module_name):
 25 |     """Check if a Python module is installed."""
 26 |     try:
 27 |         importlib.import_module(module_name)
 28 |         return True
 29 |     except ImportError:
 30 |         return False
 31 | 
 32 | def main():
 33 |     """Main verification function."""
 34 |     print("Claude Document MCP Server Verification")
 35 |     print("======================================")
 36 |     
 37 |     # Check if UV is installed
 38 |     try:
 39 |         uv_version = subprocess.check_output(["uv", "--version"], universal_newlines=True).strip()
 40 |         print(f"UV installed: Yes (version {uv_version})")
 41 |     except (subprocess.CalledProcessError, FileNotFoundError):
 42 |         print("UV installed: No")
 43 |         print("ERROR: UV is not installed or not in PATH. Please install UV first.")
 44 |         return False
 45 |     
 46 |     # Check Python version
 47 |     print(f"Python version: {sys.version}")
 48 |     
 49 |     # Check if the project is installed
 50 |     print("\nChecking dependencies...")
 51 |     
 52 |     # Try to import the project
 53 |     can_import = check_dependency("claude_document_mcp")
 54 |     print(f"claude_document_mcp importable: {'Yes' if can_import else 'No'}")
 55 |     
 56 |     # Check MCP dependency
 57 |     if check_dependency("mcp"):
 58 |         import mcp
 59 |         print(f"MCP installed: Yes (version {getattr(mcp, '__version__', 'unknown')})")
 60 |     else:
 61 |         print("MCP installed: No")
 62 |     
 63 |     # Check other dependencies
 64 |     dependencies = [
 65 |         "docx", 
 66 |         "pandas", 
 67 |         "openpyxl", 
 68 |         "reportlab", 
 69 |         "pdf2docx", 
 70 |         "docx2pdf"
 71 |     ]
 72 |     
 73 |     missing_deps = []
 74 |     for dep in dependencies:
 75 |         installed = check_dependency(dep)
 76 |         print(f"{dep} installed: {'Yes' if installed else 'No'}")
 77 |         if not installed:
 78 |             missing_deps.append(dep)
 79 |     
 80 |     if missing_deps:
 81 |         print(f"\nMissing dependencies: {', '.join(missing_deps)}")
 82 |         print("Run 'uv sync' to install missing dependencies")
 83 |     
 84 |     # Check if logs directory exists
 85 |     logs_dir = Path(__file__).parent / "logs"
 86 |     if logs_dir.exists():
 87 |         print(f"\nLogs directory exists: Yes ({logs_dir})")
 88 |     else:
 89 |         print(f"\nLogs directory exists: No")
 90 |         print(f"Creating logs directory at {logs_dir}")
 91 |         logs_dir.mkdir(exist_ok=True)
 92 |     
 93 |     # Check if Claude Desktop config exists
 94 |     if sys.platform == "darwin":
 95 |         config_path = Path.home() / "Library/Application Support/Claude/claude_desktop_config.json"
 96 |     elif sys.platform == "win32":
 97 |         config_path = Path(os.environ.get("APPDATA", "")) / "Claude/claude_desktop_config.json"
 98 |     else:
 99 |         config_path = Path(__file__).parent / "claude_desktop_config.json"
100 |     
101 |     if config_path.exists():
102 |         print(f"Claude Desktop config exists: Yes ({config_path})")
103 |     else:
104 |         print(f"Claude Desktop config exists: No")
105 |         print(f"ERROR: Claude Desktop config does not exist at {config_path}")
106 |         print("Run './setup.sh' to create the configuration")
107 |         return False
108 |     
109 |     # Test running the server with UV
110 |     print("\nTesting MCP server execution with UV...")
111 |     try:
112 |         # Just check if the command would run, don't actually run it
113 |         cmd = ["uv", "run", "-m", "claude_document_mcp.server", "--help"]
114 |         subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
115 |         print("Server command is executable: Yes")
116 |     except subprocess.CalledProcessError:
117 |         print("Server command is executable: No")
118 |         print("ERROR: Cannot execute the MCP server")
119 |         return False
120 |     
121 |     print("\nVerification successful! Your environment is properly set up.")
122 |     print("To run the server, use: ./run.sh")
123 |     print("Or directly with UV: uv run -m claude_document_mcp.server")
124 |     return True
125 | 
126 | if __name__ == "__main__":
127 |     success = main()
128 |     sys.exit(0 if success else 1)
129 | 
```

--------------------------------------------------------------------------------
/claude_document_mcp/server.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Claude Document MCP Server - Model Context Protocol server for Claude Desktop
  3 | 
  4 | Features:
  5 | - Microsoft Word file operations (create, edit, convert from txt)
  6 | - Excel file operations (create, edit, convert from csv)
  7 | - PDF file operations (create, convert from Word)
  8 | 
  9 | This is a headless server with no UI, designed to be used with Claude Desktop.
 10 | """
 11 | 
 12 | import os
 13 | import sys
 14 | import json
 15 | import logging
 16 | from pathlib import Path
 17 | from typing import Dict, Any, List, Optional
 18 | 
 19 | from mcp.server.fastmcp import FastMCP
 20 | 
 21 | # Document processing libraries
 22 | try:
 23 |     import docx
 24 |     from docx import Document
 25 |     from docx.shared import Pt, Inches
 26 | except ImportError:
 27 |     raise ImportError("Please install python-docx with: uv pip install python-docx")
 28 | 
 29 | try:
 30 |     import pandas as pd
 31 |     import openpyxl
 32 | except ImportError:
 33 |     raise ImportError("Please install pandas and openpyxl with: uv pip install pandas openpyxl")
 34 | 
 35 | try:
 36 |     from reportlab.lib.pagesizes import letter
 37 |     from reportlab.pdfgen import canvas
 38 | except ImportError:
 39 |     raise ImportError("Please install reportlab with: uv pip install reportlab")
 40 | 
 41 | try:
 42 |     import docx2pdf
 43 | except ImportError:
 44 |     raise ImportError("Please install docx2pdf with: uv pip install docx2pdf")
 45 | 
 46 | # Set up logging
 47 | log_dir = Path(__file__).parent.parent / "logs"
 48 | log_dir.mkdir(exist_ok=True)
 49 | log_file = log_dir / "document_mcp.log"
 50 | 
 51 | logging.basicConfig(
 52 |     level=logging.INFO,
 53 |     format="%(asctime)s [%(levelname)s] %(message)s",
 54 |     handlers=[
 55 |         logging.StreamHandler(),
 56 |         logging.FileHandler(log_file)
 57 |     ]
 58 | )
 59 | logger = logging.getLogger(__name__)
 60 | 
 61 | # Initialize the FastMCP server
 62 | # Make sure to use a standard variable name that can be discovered automatically
 63 | server = FastMCP(
 64 |     "Document Operations", 
 65 |     description="MCP server for document operations (Word, Excel, PDF)",
 66 |     dependencies=[
 67 |         "python-docx", 
 68 |         "pandas", 
 69 |         "openpyxl", 
 70 |         "reportlab", 
 71 |         "docx2pdf",
 72 |     ]
 73 | )
 74 | 
 75 | # Also expose as mcp for current code compatibility
 76 | mcp = server
 77 | 
 78 | # ---- Microsoft Word Operations ----
 79 | 
 80 | @server.tool()
 81 | def create_word_document(filepath: str, content: str) -> Dict[str, Any]:
 82 |     """
 83 |     Create a new Microsoft Word document with the provided content.
 84 |     
 85 |     Args:
 86 |         filepath: Path where to save the document
 87 |         content: Text content for the document
 88 |         
 89 |     Returns:
 90 |         Operation result with success status, message, and filepath
 91 |     """
 92 |     try:
 93 |         # Create a new document
 94 |         doc = Document()
 95 |         
 96 |         # Add content
 97 |         doc.add_paragraph(content)
 98 |         
 99 |         # Ensure the directory exists
100 |         os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
101 |         
102 |         # Save the document
103 |         doc.save(filepath)
104 |         
105 |         logger.info(f"Created Word document: {filepath}")
106 |         return {
107 |             "success": True,
108 |             "message": "Successfully created Word document",
109 |             "filepath": filepath
110 |         }
111 |     except Exception as e:
112 |         logger.error(f"Error creating Word document: {str(e)}")
113 |         return {
114 |             "success": False,
115 |             "message": f"Error creating Word document: {str(e)}",
116 |             "filepath": None
117 |         }
118 | 
119 | @server.tool()
120 | def edit_word_document(filepath: str, operations: List[Dict[str, Any]]) -> Dict[str, Any]:
121 |     """
122 |     Edit an existing Microsoft Word document using the specified operations.
123 |     
124 |     Args:
125 |         filepath: Path to the Word document
126 |         operations: List of operations to perform, where each operation is a dictionary with:
127 |             - type: Operation type (add_paragraph, add_heading, edit_paragraph, delete_paragraph)
128 |             - Additional parameters depending on the operation type
129 |             
130 |     Returns:
131 |         Operation result with success status, message, and filepath
132 |     """
133 |     try:
134 |         # Load the document
135 |         if not os.path.exists(filepath):
136 |             return {
137 |                 "success": False,
138 |                 "message": f"File not found: {filepath}",
139 |                 "filepath": None
140 |             }
141 |         
142 |         doc = Document(filepath)
143 |         
144 |         # Apply operations
145 |         for op in operations:
146 |             op_type = op.get("type")
147 |             
148 |             if op_type == "add_paragraph":
149 |                 doc.add_paragraph(op.get("text", ""))
150 |             
151 |             elif op_type == "add_heading":
152 |                 doc.add_heading(op.get("text", ""), level=op.get("level", 1))
153 |             
154 |             elif op_type == "edit_paragraph":
155 |                 idx = op.get("index", 0)
156 |                 new_text = op.get("text", "")
157 |                 
158 |                 if 0 <= idx < len(doc.paragraphs):
159 |                     doc.paragraphs[idx].text = new_text
160 |                 else:
161 |                     logger.warning(f"Paragraph index out of range: {idx}")
162 |             
163 |             elif op_type == "delete_paragraph":
164 |                 idx = op.get("index", 0)
165 |                 
166 |                 if 0 <= idx < len(doc.paragraphs):
167 |                     p = doc.paragraphs[idx]
168 |                     p_elem = p._element
169 |                     p_elem.getparent().remove(p_elem)
170 |                 else:
171 |                     logger.warning(f"Paragraph index out of range: {idx}")
172 |             
173 |             else:
174 |                 logger.warning(f"Unknown operation type: {op_type}")
175 |         
176 |         # Save the document
177 |         doc.save(filepath)
178 |         
179 |         logger.info(f"Edited Word document: {filepath}")
180 |         return {
181 |             "success": True,
182 |             "message": "Successfully edited Word document",
183 |             "filepath": filepath
184 |         }
185 |     except Exception as e:
186 |         logger.error(f"Error editing Word document: {str(e)}")
187 |         return {
188 |             "success": False,
189 |             "message": f"Error editing Word document: {str(e)}",
190 |             "filepath": None
191 |         }
192 | 
193 | @server.tool()
194 | def convert_txt_to_word(source_path: str, target_path: str) -> Dict[str, Any]:
195 |     """
196 |     Convert a text file to a Microsoft Word document.
197 |     
198 |     Args:
199 |         source_path: Path to the text file
200 |         target_path: Path where to save the Word document
201 |         
202 |     Returns:
203 |         Operation result with success status, message, and filepath
204 |     """
205 |     try:
206 |         # Check if source file exists
207 |         if not os.path.exists(source_path):
208 |             return {
209 |                 "success": False,
210 |                 "message": f"Source file not found: {source_path}",
211 |                 "filepath": None
212 |             }
213 |         
214 |         # Read the text file
215 |         with open(source_path, 'r', encoding='utf-8') as file:
216 |             text_content = file.read()
217 |         
218 |         # Create a new document
219 |         doc = Document()
220 |         
221 |         # Add content as paragraphs (split by newlines)
222 |         for paragraph in text_content.split('\n'):
223 |             if paragraph.strip():  # Skip empty paragraphs
224 |                 doc.add_paragraph(paragraph)
225 |         
226 |         # Ensure the directory exists
227 |         os.makedirs(os.path.dirname(os.path.abspath(target_path)), exist_ok=True)
228 |         
229 |         # Save the document
230 |         doc.save(target_path)
231 |         
232 |         logger.info(f"Converted text to Word: {source_path} -> {target_path}")
233 |         return {
234 |             "success": True,
235 |             "message": "Successfully converted text to Word document",
236 |             "filepath": target_path
237 |         }
238 |     except Exception as e:
239 |         logger.error(f"Error converting text to Word: {str(e)}")
240 |         return {
241 |             "success": False,
242 |             "message": f"Error converting text to Word: {str(e)}",
243 |             "filepath": None
244 |         }
245 | 
246 | # ---- Excel Operations ----
247 | 
248 | @server.tool()
249 | def create_excel_file(filepath: str, content: str) -> Dict[str, Any]:
250 |     """
251 |     Create a new Excel file with the provided content.
252 |     
253 |     Args:
254 |         filepath: Path where to save the Excel file
255 |         content: Data content, either JSON string or CSV-like string
256 |         
257 |     Returns:
258 |         Operation result with success status, message, and filepath
259 |     """
260 |     try:
261 |         # Parse the content as JSON data
262 |         try:
263 |             data = json.loads(content)
264 |         except json.JSONDecodeError:
265 |             # If not valid JSON, treat as CSV
266 |             data = [line.split(',') for line in content.strip().split('\n')]
267 |         
268 |         # Convert to DataFrame
269 |         df = pd.DataFrame(data)
270 |         
271 |         # Ensure the directory exists
272 |         os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
273 |         
274 |         # Save to Excel
275 |         df.to_excel(filepath, index=False)
276 |         
277 |         logger.info(f"Created Excel file: {filepath}")
278 |         return {
279 |             "success": True,
280 |             "message": "Successfully created Excel file",
281 |             "filepath": filepath
282 |         }
283 |     except Exception as e:
284 |         logger.error(f"Error creating Excel file: {str(e)}")
285 |         return {
286 |             "success": False,
287 |             "message": f"Error creating Excel file: {str(e)}",
288 |             "filepath": None
289 |         }
290 | 
291 | @server.tool()
292 | def edit_excel_file(filepath: str, operations: List[Dict[str, Any]]) -> Dict[str, Any]:
293 |     """
294 |     Edit an existing Excel file using the specified operations.
295 |     
296 |     Args:
297 |         filepath: Path to the Excel file
298 |         operations: List of operations to perform, where each operation is a dictionary with:
299 |             - type: Operation type (update_cell, update_range, delete_row, delete_column, add_sheet, delete_sheet)
300 |             - Additional parameters depending on the operation type
301 |             
302 |     Returns:
303 |         Operation result with success status, message, and filepath
304 |     """
305 |     try:
306 |         # Check if file exists
307 |         if not os.path.exists(filepath):
308 |             return {
309 |                 "success": False,
310 |                 "message": f"File not found: {filepath}",
311 |                 "filepath": None
312 |             }
313 |         
314 |         # Load the Excel file
315 |         wb = openpyxl.load_workbook(filepath)
316 |         
317 |         # Apply operations
318 |         for op in operations:
319 |             op_type = op.get("type")
320 |             sheet_name = op.get("sheet", wb.sheetnames[0])
321 |             
322 |             # Get the sheet, create if it doesn't exist
323 |             if sheet_name not in wb.sheetnames:
324 |                 wb.create_sheet(sheet_name)
325 |             
326 |             sheet = wb[sheet_name]
327 |             
328 |             if op_type == "update_cell":
329 |                 row = op.get("row", 1)
330 |                 col = op.get("col", 1)
331 |                 value = op.get("value", "")
332 |                 
333 |                 sheet.cell(row=row, column=col, value=value)
334 |             
335 |             elif op_type == "update_range":
336 |                 start_row = op.get("start_row", 1)
337 |                 start_col = op.get("start_col", 1)
338 |                 values = op.get("values", [])
339 |                 
340 |                 for i, row_values in enumerate(values):
341 |                     for j, value in enumerate(row_values):
342 |                         sheet.cell(row=start_row + i, column=start_col + j, value=value)
343 |             
344 |             elif op_type == "delete_row":
345 |                 row = op.get("row", 1)
346 |                 sheet.delete_rows(row)
347 |             
348 |             elif op_type == "delete_column":
349 |                 col = op.get("col", 1)
350 |                 sheet.delete_cols(col)
351 |             
352 |             elif op_type == "add_sheet":
353 |                 new_sheet_name = op.get("name", "NewSheet")
354 |                 if new_sheet_name not in wb.sheetnames:
355 |                     wb.create_sheet(new_sheet_name)
356 |             
357 |             elif op_type == "delete_sheet":
358 |                 if sheet_name in wb.sheetnames and len(wb.sheetnames) > 1:
359 |                     del wb[sheet_name]
360 |             
361 |             else:
362 |                 logger.warning(f"Unknown operation type: {op_type}")
363 |         
364 |         # Save the workbook
365 |         wb.save(filepath)
366 |         
367 |         logger.info(f"Edited Excel file: {filepath}")
368 |         return {
369 |             "success": True,
370 |             "message": "Successfully edited Excel file",
371 |             "filepath": filepath
372 |         }
373 |     except Exception as e:
374 |         logger.error(f"Error editing Excel file: {str(e)}")
375 |         return {
376 |             "success": False,
377 |             "message": f"Error editing Excel file: {str(e)}",
378 |             "filepath": None
379 |         }
380 | 
381 | @server.tool()
382 | def convert_csv_to_excel(source_path: str, target_path: str) -> Dict[str, Any]:
383 |     """
384 |     Convert a CSV file to an Excel file.
385 |     
386 |     Args:
387 |         source_path: Path to the CSV file
388 |         target_path: Path where to save the Excel file
389 |         
390 |     Returns:
391 |         Operation result with success status, message, and filepath
392 |     """
393 |     try:
394 |         # Check if source file exists
395 |         if not os.path.exists(source_path):
396 |             return {
397 |                 "success": False,
398 |                 "message": f"Source file not found: {source_path}",
399 |                 "filepath": None
400 |             }
401 |         
402 |         # Read the CSV file
403 |         df = pd.read_csv(source_path)
404 |         
405 |         # Ensure the directory exists
406 |         os.makedirs(os.path.dirname(os.path.abspath(target_path)), exist_ok=True)
407 |         
408 |         # Save to Excel
409 |         df.to_excel(target_path, index=False)
410 |         
411 |         logger.info(f"Converted CSV to Excel: {source_path} -> {target_path}")
412 |         return {
413 |             "success": True,
414 |             "message": "Successfully converted CSV to Excel",
415 |             "filepath": target_path
416 |         }
417 |     except Exception as e:
418 |         logger.error(f"Error converting CSV to Excel: {str(e)}")
419 |         return {
420 |             "success": False,
421 |             "message": f"Error converting CSV to Excel: {str(e)}",
422 |             "filepath": None
423 |         }
424 | 
425 | # ---- PDF Operations ----
426 | 
427 | @server.tool()
428 | def create_pdf_file(filepath: str, content: str) -> Dict[str, Any]:
429 |     """
430 |     Create a new PDF file with the provided text content.
431 |     
432 |     Args:
433 |         filepath: Path where to save the PDF file
434 |         content: Text content for the PDF
435 |         
436 |     Returns:
437 |         Operation result with success status, message, and filepath
438 |     """
439 |     try:
440 |         # Ensure the directory exists
441 |         os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True)
442 |         
443 |         # Create a new PDF with ReportLab
444 |         c = canvas.Canvas(filepath, pagesize=letter)
445 |         width, height = letter
446 |         
447 |         # Process text content
448 |         lines = content.split('\n')
449 |         
450 |         y_position = height - 40  # Start position from top
451 |         for line in lines:
452 |             if y_position < 40:  # If we're at the bottom of the page
453 |                 c.showPage()  # Create a new page
454 |                 y_position = height - 40  # Reset position
455 |             
456 |             c.drawString(40, y_position, line)
457 |             y_position -= 15  # Move down for next line
458 |         
459 |         c.save()
460 |         
461 |         logger.info(f"Created PDF file: {filepath}")
462 |         return {
463 |             "success": True,
464 |             "message": "Successfully created PDF file",
465 |             "filepath": filepath
466 |         }
467 |     except Exception as e:
468 |         logger.error(f"Error creating PDF file: {str(e)}")
469 |         return {
470 |             "success": False,
471 |             "message": f"Error creating PDF file: {str(e)}",
472 |             "filepath": None
473 |         }
474 | 
475 | @server.tool()
476 | def convert_word_to_pdf(source_path: str, target_path: str) -> Dict[str, Any]:
477 |     """
478 |     Convert a Microsoft Word document to a PDF file.
479 |     
480 |     Args:
481 |         source_path: Path to the Word document
482 |         target_path: Path where to save the PDF file
483 |         
484 |     Returns:
485 |         Operation result with success status, message, and filepath
486 |     """
487 |     try:
488 |         # Check if source file exists
489 |         if not os.path.exists(source_path):
490 |             return {
491 |                 "success": False,
492 |                 "message": f"Source file not found: {source_path}",
493 |                 "filepath": None
494 |             }
495 |         
496 |         # Ensure the directory exists
497 |         os.makedirs(os.path.dirname(os.path.abspath(target_path)), exist_ok=True)
498 |         
499 |         # Convert Word to PDF using docx2pdf
500 |         docx2pdf.convert(source_path, target_path)
501 |         
502 |         logger.info(f"Converted Word to PDF: {source_path} -> {target_path}")
503 |         return {
504 |             "success": True,
505 |             "message": "Successfully converted Word to PDF",
506 |             "filepath": target_path
507 |         }
508 |     except Exception as e:
509 |         logger.error(f"Error converting Word to PDF: {str(e)}")
510 |         return {
511 |             "success": False,
512 |             "message": f"Error converting Word to PDF: {str(e)}",
513 |             "filepath": None
514 |         }
515 | 
516 | # ---- Resources ----
517 | 
518 | @server.resource("capabilities://")
519 | def get_capabilities() -> Dict[str, Any]:
520 |     """
521 |     Provide information about this MCP server's capabilities.
522 |     
523 |     Returns:
524 |         Dictionary containing capabilities information
525 |     """
526 |     return {
527 |         "name": "Document Operations",
528 |         "version": "0.1.0",
529 |         "description": "Model Context Protocol server for document operations (Word, Excel, PDF)",
530 |         "document_operations": {
531 |             "word": {
532 |                 "create": True,
533 |                 "edit": True,
534 |                 "convert_from_txt": True
535 |             },
536 |             "excel": {
537 |                 "create": True,
538 |                 "edit": True,
539 |                 "convert_from_csv": True
540 |             },
541 |             "pdf": {
542 |                 "create": True,
543 |                 "convert_from_word": True
544 |             }
545 |         }
546 |     }
547 | 
548 | def main():
549 |     """Main entry point for the server."""
550 |     try:
551 |         # Setup logging directory
552 |         log_dir = Path(__file__).parent.parent / "logs"
553 |         log_dir.mkdir(exist_ok=True)
554 |         
555 |         # Log to file instead of stdout
556 |         startup_logger = logging.getLogger("startup")
557 |         startup_logger.setLevel(logging.INFO)
558 |         
559 |         # Make sure startup logger doesn't also log to console
560 |         startup_logger.propagate = False
561 |         
562 |         # Add file handler for startup logs
563 |         startup_log_file = log_dir / "startup.log"
564 |         file_handler = logging.FileHandler(startup_log_file)
565 |         file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
566 |         startup_logger.addHandler(file_handler)
567 |         
568 |         # Log startup information to file only
569 |         startup_logger.info("Starting Document Operations MCP Server...")
570 |         startup_logger.info(f"Python version: {sys.version}")
571 |         startup_logger.info(f"Python executable: {sys.executable}")
572 |         startup_logger.info(f"Working directory: {os.getcwd()}")
573 |         startup_logger.info(f"Logs directory: {log_dir}")
574 |         
575 |         # Verify environment
576 |         if sys.prefix == sys.base_prefix:
577 |             startup_logger.warning("Not running in a virtual environment")
578 |             
579 |         startup_logger.info("Server is ready to accept connections from Claude Desktop!")
580 |         
581 |         # Run the server
582 |         server.run()
583 |     except Exception as e:
584 |         logger.error(f"Error starting server: {str(e)}")
585 |         import traceback
586 |         logger.error(traceback.format_exc())
587 |         sys.exit(1)
588 | 
589 | if __name__ == "__main__":
590 |     main()
591 | 
```