# 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 | ```