Office Word MCP Server
by GongRzhe
Verified
#!/usr/bin/env python3
import os
import io
import base64
import shutil
from typing import Dict, List, Optional, Any, Union, Tuple
import json
from docx import Document
from docx.shared import Pt, Inches, RGBColor
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.style import WD_STYLE_TYPE
from mcp.server.fastmcp import FastMCP
from docx.enum.text import WD_COLOR_INDEX
from docx.oxml.shared import OxmlElement, qn
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
import sys
# Initialize FastMCP server
mcp = FastMCP("word-document-server")
# Document cache to store opened documents
documents = {}
# Helper Functions
def get_document_properties(doc_path: str) -> Dict[str, Any]:
"""Get properties of a Word document."""
if not os.path.exists(doc_path):
return {"error": f"Document {doc_path} does not exist"}
try:
doc = Document(doc_path)
core_props = doc.core_properties
return {
"title": core_props.title or "",
"author": core_props.author or "",
"subject": core_props.subject or "",
"keywords": core_props.keywords or "",
"created": str(core_props.created) if core_props.created else "",
"modified": str(core_props.modified) if core_props.modified else "",
"last_modified_by": core_props.last_modified_by or "",
"revision": core_props.revision or 0,
"page_count": len(doc.sections),
"word_count": sum(len(paragraph.text.split()) for paragraph in doc.paragraphs),
"paragraph_count": len(doc.paragraphs),
"table_count": len(doc.tables)
}
except Exception as e:
return {"error": f"Failed to get document properties: {str(e)}"}
def extract_document_text(doc_path: str) -> str:
"""Extract all text from a Word document."""
if not os.path.exists(doc_path):
return f"Document {doc_path} does not exist"
try:
doc = Document(doc_path)
text = []
for paragraph in doc.paragraphs:
text.append(paragraph.text)
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
text.append(paragraph.text)
return "\n".join(text)
except Exception as e:
return f"Failed to extract text: {str(e)}"
def get_document_structure(doc_path: str) -> Dict[str, Any]:
"""Get the structure of a Word document."""
if not os.path.exists(doc_path):
return {"error": f"Document {doc_path} does not exist"}
try:
doc = Document(doc_path)
structure = {
"paragraphs": [],
"tables": []
}
# Get paragraphs
for i, para in enumerate(doc.paragraphs):
structure["paragraphs"].append({
"index": i,
"text": para.text[:100] + ("..." if len(para.text) > 100 else ""),
"style": para.style.name if para.style else "Normal"
})
# Get tables
for i, table in enumerate(doc.tables):
table_data = {
"index": i,
"rows": len(table.rows),
"columns": len(table.columns),
"preview": []
}
# Get sample of table data
max_rows = min(3, len(table.rows))
for row_idx in range(max_rows):
row_data = []
max_cols = min(3, len(table.columns))
for col_idx in range(max_cols):
try:
cell_text = table.cell(row_idx, col_idx).text
row_data.append(cell_text[:20] + ("..." if len(cell_text) > 20 else ""))
except IndexError:
row_data.append("N/A")
table_data["preview"].append(row_data)
structure["tables"].append(table_data)
return structure
except Exception as e:
return {"error": f"Failed to get document structure: {str(e)}"}
def check_file_writeable(filepath: str) -> Tuple[bool, str]:
"""
Check if a file can be written to.
Args:
filepath: Path to the file
Returns:
Tuple of (is_writeable, error_message)
"""
# If file doesn't exist, check if directory is writeable
if not os.path.exists(filepath):
directory = os.path.dirname(filepath)
if not os.path.exists(directory):
return False, f"Directory {directory} does not exist"
if not os.access(directory, os.W_OK):
return False, f"Directory {directory} is not writeable"
return True, ""
# If file exists, check if it's writeable
if not os.access(filepath, os.W_OK):
return False, f"File {filepath} is not writeable (permission denied)"
# Try to open the file for writing to see if it's locked
try:
with open(filepath, 'a'):
pass
return True, ""
except IOError as e:
return False, f"File {filepath} is not writeable: {str(e)}"
except Exception as e:
return False, f"Unknown error checking file permissions: {str(e)}"
def create_document_copy(source_path: str, dest_path: Optional[str] = None) -> Tuple[bool, str, Optional[str]]:
"""
Create a copy of a document.
Args:
source_path: Path to the source document
dest_path: Optional path for the new document. If not provided, will use source_path + '_copy.docx'
Returns:
Tuple of (success, message, new_filepath)
"""
if not os.path.exists(source_path):
return False, f"Source document {source_path} does not exist", None
if not dest_path:
# Generate a new filename if not provided
base, ext = os.path.splitext(source_path)
dest_path = f"{base}_copy{ext}"
try:
# Simple file copy
shutil.copy2(source_path, dest_path)
return True, f"Document copied to {dest_path}", dest_path
except Exception as e:
return False, f"Failed to copy document: {str(e)}", None
def ensure_heading_style(doc):
"""
Ensure Heading styles exist in the document.
Args:
doc: Document object
"""
for i in range(1, 10): # Create Heading 1 through Heading 9
style_name = f'Heading {i}'
try:
# Try to access the style to see if it exists
style = doc.styles[style_name]
except KeyError:
# Create the style if it doesn't exist
try:
style = doc.styles.add_style(style_name, WD_STYLE_TYPE.PARAGRAPH)
if i == 1:
style.font.size = Pt(16)
style.font.bold = True
elif i == 2:
style.font.size = Pt(14)
style.font.bold = True
else:
style.font.size = Pt(12)
style.font.bold = True
except Exception:
# If style creation fails, we'll just use default formatting
pass
def ensure_table_style(doc):
"""
Ensure Table Grid style exists in the document.
Args:
doc: Document object
"""
try:
# Try to access the style to see if it exists
style = doc.styles['Table Grid']
except KeyError:
# If style doesn't exist, we'll handle it at usage time
pass
# MCP Tools
@mcp.tool()
async def create_document(filename: str, title: Optional[str] = None, author: Optional[str] = None) -> str:
"""Create a new Word document with optional metadata.
Args:
filename: Name of the document to create (with or without .docx extension)
title: Optional title for the document metadata
author: Optional author for the document metadata
"""
if not filename.endswith('.docx'):
filename += '.docx'
# Check if file is writeable
is_writeable, error_message = check_file_writeable(filename)
if not is_writeable:
return f"Cannot create document: {error_message}"
try:
doc = Document()
# Set properties if provided
if title:
doc.core_properties.title = title
if author:
doc.core_properties.author = author
# Ensure necessary styles exist
ensure_heading_style(doc)
ensure_table_style(doc)
# Save the document
doc.save(filename)
return f"Document {filename} created successfully"
except Exception as e:
return f"Failed to create document: {str(e)}"
@mcp.tool()
async def add_heading(filename: str, text: str, level: int = 1) -> str:
"""Add a heading to a Word document.
Args:
filename: Path to the Word document
text: Heading text
level: Heading level (1-9, where 1 is the highest level)
"""
if not filename.endswith('.docx'):
filename += '.docx'
if not os.path.exists(filename):
return f"Document {filename} does not exist"
# Check if file is writeable
is_writeable, error_message = check_file_writeable(filename)
if not is_writeable:
# Suggest creating a copy
return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document."
try:
doc = Document(filename)
# Ensure heading styles exist
ensure_heading_style(doc)
# Try to add heading with style
try:
heading = doc.add_heading(text, level=level)
doc.save(filename)
return f"Heading '{text}' (level {level}) added to {filename}"
except Exception as style_error:
# If style-based approach fails, use direct formatting
paragraph = doc.add_paragraph(text)
paragraph.style = doc.styles['Normal']
run = paragraph.runs[0]
run.bold = True
# Adjust size based on heading level
if level == 1:
run.font.size = Pt(16)
elif level == 2:
run.font.size = Pt(14)
else:
run.font.size = Pt(12)
doc.save(filename)
return f"Heading '{text}' added to {filename} with direct formatting (style not available)"
except Exception as e:
return f"Failed to add heading: {str(e)}"
@mcp.tool()
async def add_paragraph(filename: str, text: str, style: Optional[str] = None) -> str:
"""Add a paragraph to a Word document.
Args:
filename: Path to the Word document
text: Paragraph text
style: Optional paragraph style name
"""
if not filename.endswith('.docx'):
filename += '.docx'
if not os.path.exists(filename):
return f"Document {filename} does not exist"
# Check if file is writeable
is_writeable, error_message = check_file_writeable(filename)
if not is_writeable:
# Suggest creating a copy
return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document."
try:
doc = Document(filename)
paragraph = doc.add_paragraph(text)
if style:
try:
paragraph.style = style
except KeyError:
# Style doesn't exist, use normal and report it
paragraph.style = doc.styles['Normal']
doc.save(filename)
return f"Style '{style}' not found, paragraph added with default style to {filename}"
doc.save(filename)
return f"Paragraph added to {filename}"
except Exception as e:
return f"Failed to add paragraph: {str(e)}"
@mcp.tool()
async def add_table(filename: str, rows: int, cols: int, data: Optional[List[List[str]]] = None) -> str:
"""Add a table to a Word document.
Args:
filename: Path to the Word document
rows: Number of rows in the table
cols: Number of columns in the table
data: Optional 2D array of data to fill the table
"""
if not filename.endswith('.docx'):
filename += '.docx'
if not os.path.exists(filename):
return f"Document {filename} does not exist"
# Check if file is writeable
is_writeable, error_message = check_file_writeable(filename)
if not is_writeable:
# Suggest creating a copy
return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document."
try:
doc = Document(filename)
table = doc.add_table(rows=rows, cols=cols)
# Try to set the table style
try:
table.style = 'Table Grid'
except KeyError:
# If style doesn't exist, add basic borders
# This is a simplified approach - complete border styling would require more code
pass
# Fill table with data if provided
if data:
for i, row_data in enumerate(data):
if i >= rows:
break
for j, cell_text in enumerate(row_data):
if j >= cols:
break
table.cell(i, j).text = str(cell_text)
doc.save(filename)
return f"Table ({rows}x{cols}) added to {filename}"
except Exception as e:
return f"Failed to add table: {str(e)}"
@mcp.tool()
async def add_picture(filename: str, image_path: str, width: Optional[float] = None) -> str:
"""Add an image to a Word document.
Args:
filename: Path to the Word document
image_path: Path to the image file
width: Optional width in inches (proportional scaling)
"""
if not filename.endswith('.docx'):
filename += '.docx'
# Validate document existence
if not os.path.exists(filename):
return f"Document {filename} does not exist"
# Get absolute paths for better diagnostics
abs_filename = os.path.abspath(filename)
abs_image_path = os.path.abspath(image_path)
# Validate image existence with improved error message
if not os.path.exists(abs_image_path):
return f"Image file not found: {abs_image_path}"
# Check image file size
try:
image_size = os.path.getsize(abs_image_path) / 1024 # Size in KB
if image_size <= 0:
return f"Image file appears to be empty: {abs_image_path} (0 KB)"
except Exception as size_error:
return f"Error checking image file: {str(size_error)}"
# Check if file is writeable
is_writeable, error_message = check_file_writeable(abs_filename)
if not is_writeable:
return f"Cannot modify document: {error_message}. Consider creating a copy first or creating a new document."
try:
doc = Document(abs_filename)
# Additional diagnostic info
diagnostic = f"Attempting to add image ({abs_image_path}, {image_size:.2f} KB) to document ({abs_filename})"
try:
if width:
doc.add_picture(abs_image_path, width=Inches(width))
else:
doc.add_picture(abs_image_path)
doc.save(abs_filename)
return f"Picture {image_path} added to {filename}"
except Exception as inner_error:
# More detailed error for the specific operation
error_type = type(inner_error).__name__
error_msg = str(inner_error)
return f"Failed to add picture: {error_type} - {error_msg or 'No error details available'}\nDiagnostic info: {diagnostic}"
except Exception as outer_error:
# Fallback error handling
error_type = type(outer_error).__name__
error_msg = str(outer_error)
return f"Document processing error: {error_type} - {error_msg or 'No error details available'}"
@mcp.tool()
async def get_document_info(filename: str) -> str:
"""Get information about a Word document.
Args:
filename: Path to the Word document
"""
if not filename.endswith('.docx'):
filename += '.docx'
if not os.path.exists(filename):
return f"Document {filename} does not exist"
try:
properties = get_document_properties(filename)
return json.dumps(properties, indent=2)
except Exception as e:
return f"Failed to get document info: {str(e)}"
@mcp.tool()
async def get_document_text(filename: str) -> str:
"""Extract all text from a Word document.
Args:
filename: Path to the Word document
"""
if not filename.endswith('.docx'):
filename += '.docx'
return extract_document_text(filename)
@mcp.tool()
async def get_document_outline(filename: str) -> str:
"""Get the structure of a Word document.
Args:
filename: Path to the Word document
"""
if not filename.endswith('.docx'):
filename += '.docx'
structure = get_document_structure(filename)
return json.dumps(structure, indent=2)
@mcp.tool()
async def list_available_documents(directory: str = ".") -> str:
"""List all .docx files in the specified directory.
Args:
directory: Directory to search for Word documents
"""
try:
if not os.path.exists(directory):
return f"Directory {directory} does not exist"
docx_files = [f for f in os.listdir(directory) if f.endswith('.docx')]
if not docx_files:
return f"No Word documents found in {directory}"
result = f"Found {len(docx_files)} Word documents in {directory}:\n"
for file in docx_files:
file_path = os.path.join(directory, file)
size = os.path.getsize(file_path) / 1024 # KB
result += f"- {file} ({size:.2f} KB)\n"
return result
except Exception as e:
return f"Failed to list documents: {str(e)}"
@mcp.tool()
async def copy_document(source_filename: str, destination_filename: Optional[str] = None) -> str:
"""Create a copy of a Word document.
Args:
source_filename: Path to the source document
destination_filename: Optional path for the copy. If not provided, a default name will be generated.
"""
if not source_filename.endswith('.docx'):
source_filename += '.docx'
if destination_filename and not destination_filename.endswith('.docx'):
destination_filename += '.docx'
success, message, new_path = create_document_copy(source_filename, destination_filename)
if success:
return message
else:
return f"Failed to copy document: {message}"
# Resources
@mcp.resource("docx:{path}")
async def document_resource(path: str) -> str:
"""Access Word document content."""
if not path.endswith('.docx'):
path += '.docx'
if not os.path.exists(path):
return f"Document {path} does not exist"
return extract_document_text(path)
def find_paragraph_by_text(doc, text, partial_match=False):
"""
Find paragraphs containing specific text.
Args:
doc: Document object
text: Text to search for
partial_match: If True, matches paragraphs containing the text; if False, matches exact text
Returns:
List of paragraph indices that match the criteria
"""
matching_paragraphs = []
for i, para in enumerate(doc.paragraphs):
if partial_match and text in para.text:
matching_paragraphs.append(i)
elif not partial_match and para.text == text:
matching_paragraphs.append(i)
return matching_paragraphs
def find_and_replace_text(doc, old_text, new_text):
"""
Find and replace text throughout the document.
Args:
doc: Document object
old_text: Text to find
new_text: Text to replace with
Returns:
Number of replacements made
"""
count = 0
# Search in paragraphs
for para in doc.paragraphs:
if old_text in para.text:
for run in para.runs:
if old_text in run.text:
run.text = run.text.replace(old_text, new_text)
count += 1
# Search in tables
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for para in cell.paragraphs:
if old_text in para.text:
for run in para.runs:
if old_text in run.text:
run.text = run.text.replace(old_text, new_text)
count += 1
return count
def set_cell_border(cell, **kwargs):
"""
Set cell border properties.
Args:
cell: The cell to modify
**kwargs: Border properties (top, bottom, left, right, val, color)
"""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
# Create border elements
for key, value in kwargs.items():
if key in ['top', 'left', 'bottom', 'right']:
tag = 'w:{}'.format(key)
element = OxmlElement(tag)
element.set(qn('w:val'), kwargs.get('val', 'single'))
element.set(qn('w:sz'), kwargs.get('sz', '4'))
element.set(qn('w:space'), kwargs.get('space', '0'))
element.set(qn('w:color'), kwargs.get('color', 'auto'))
tcBorders = tcPr.first_child_found_in("w:tcBorders")
if tcBorders is None:
tcBorders = OxmlElement('w:tcBorders')
tcPr.append(tcBorders)
tcBorders.append(element)
def create_style(doc, style_name, style_type, base_style=None, font_properties=None, paragraph_properties=None):
"""
Create a new style in the document.
Args:
doc: Document object
style_name: Name for the new style
style_type: Type of style (WD_STYLE_TYPE)
base_style: Optional base style to inherit from
font_properties: Dictionary of font properties (bold, italic, size, name, color)
paragraph_properties: Dictionary of paragraph properties (alignment, spacing)
Returns:
The created style
"""
try:
# Check if style already exists
style = doc.styles.get_by_id(style_name, WD_STYLE_TYPE.PARAGRAPH)
return style
except:
# Create new style
new_style = doc.styles.add_style(style_name, style_type)
# Set base style if specified
if base_style:
new_style.base_style = doc.styles[base_style]
# Set font properties
if font_properties:
font = new_style.font
if 'bold' in font_properties:
font.bold = font_properties['bold']
if 'italic' in font_properties:
font.italic = font_properties['italic']
if 'size' in font_properties:
font.size = Pt(font_properties['size'])
if 'name' in font_properties:
font.name = font_properties['name']
if 'color' in font_properties:
try:
# For RGB color
font.color.rgb = font_properties['color']
except:
# For named color
font.color.theme_color = font_properties['color']
# Set paragraph properties
if paragraph_properties:
if 'alignment' in paragraph_properties:
new_style.paragraph_format.alignment = paragraph_properties['alignment']
if 'spacing' in paragraph_properties:
new_style.paragraph_format.line_spacing = paragraph_properties['spacing']
return new_style
# Add these MCP tools to the existing set
@mcp.tool()
async def format_text(filename: str, paragraph_index: int, start_pos: int, end_pos: int,
bold: Optional[bool] = None, italic: Optional[bool] = None,
underline: Optional[bool] = None, color: Optional[str] = None,
font_size: Optional[int] = None, font_name: Optional[str] = None) -> str:
"""Format a specific range of text within a paragraph.
Args:
filename: Path to the Word document
paragraph_index: Index of the paragraph (0-based)
start_pos: Start position within the paragraph text
end_pos: End position within the paragraph text
bold: Set text bold (True/False)
italic: Set text italic (True/False)
underline: Set text underlined (True/False)
color: Text color (e.g., 'red', 'blue', etc.)
font_size: Font size in points
font_name: Font name/family
"""
if not filename.endswith('.docx'):
filename += '.docx'
if not os.path.exists(filename):
return f"Document {filename} does not exist"
# Check if file is writeable
is_writeable, error_message = check_file_writeable(filename)
if not is_writeable:
return f"Cannot modify document: {error_message}. Consider creating a copy first."
try:
doc = Document(filename)
# Validate paragraph index
if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs):
return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})."
paragraph = doc.paragraphs[paragraph_index]
text = paragraph.text
# Validate text positions
if start_pos < 0 or end_pos > len(text) or start_pos >= end_pos:
return f"Invalid text positions. Paragraph has {len(text)} characters."
# Get the text to format
target_text = text[start_pos:end_pos]
# Clear existing runs and create three runs: before, target, after
for run in paragraph.runs:
run.clear()
# Add text before target
if start_pos > 0:
run_before = paragraph.add_run(text[:start_pos])
# Add target text with formatting
run_target = paragraph.add_run(target_text)
if bold is not None:
run_target.bold = bold
if italic is not None:
run_target.italic = italic
if underline is not None:
run_target.underline = underline
if color:
try:
# Try to set color by name
run_target.font.color.rgb = RGBColor.from_string(color)
except:
# If color name doesn't work, try predefined colors
color_map = {
'red': WD_COLOR_INDEX.RED,
'blue': WD_COLOR_INDEX.BLUE,
'green': WD_COLOR_INDEX.GREEN,
'yellow': WD_COLOR_INDEX.YELLOW,
'black': WD_COLOR_INDEX.BLACK,
}
if color.lower() in color_map:
run_target.font.color.index = color_map[color.lower()]
if font_size:
run_target.font.size = Pt(font_size)
if font_name:
run_target.font.name = font_name
# Add text after target
if end_pos < len(text):
run_after = paragraph.add_run(text[end_pos:])
doc.save(filename)
return f"Text '{target_text}' formatted successfully in paragraph {paragraph_index}."
except Exception as e:
return f"Failed to format text: {str(e)}"
@mcp.tool()
async def search_and_replace(filename: str, find_text: str, replace_text: str) -> str:
"""Search for text and replace all occurrences.
Args:
filename: Path to the Word document
find_text: Text to search for
replace_text: Text to replace with
"""
if not filename.endswith('.docx'):
filename += '.docx'
if not os.path.exists(filename):
return f"Document {filename} does not exist"
# Check if file is writeable
is_writeable, error_message = check_file_writeable(filename)
if not is_writeable:
return f"Cannot modify document: {error_message}. Consider creating a copy first."
try:
doc = Document(filename)
# Perform find and replace
count = find_and_replace_text(doc, find_text, replace_text)
if count > 0:
doc.save(filename)
return f"Replaced {count} occurrence(s) of '{find_text}' with '{replace_text}'."
else:
return f"No occurrences of '{find_text}' found."
except Exception as e:
return f"Failed to search and replace: {str(e)}"
@mcp.tool()
async def delete_paragraph(filename: str, paragraph_index: int) -> str:
"""Delete a paragraph from a document.
Args:
filename: Path to the Word document
paragraph_index: Index of the paragraph to delete (0-based)
"""
if not filename.endswith('.docx'):
filename += '.docx'
if not os.path.exists(filename):
return f"Document {filename} does not exist"
# Check if file is writeable
is_writeable, error_message = check_file_writeable(filename)
if not is_writeable:
return f"Cannot modify document: {error_message}. Consider creating a copy first."
try:
doc = Document(filename)
# Validate paragraph index
if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs):
return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})."
# Delete the paragraph (by removing its content and setting it empty)
# Note: python-docx doesn't support true paragraph deletion, this is a workaround
paragraph = doc.paragraphs[paragraph_index]
p = paragraph._p
p.getparent().remove(p)
doc.save(filename)
return f"Paragraph at index {paragraph_index} deleted successfully."
except Exception as e:
return f"Failed to delete paragraph: {str(e)}"
@mcp.tool()
async def create_custom_style(filename: str, style_name: str,
bold: Optional[bool] = None, italic: Optional[bool] = None,
font_size: Optional[int] = None, font_name: Optional[str] = None,
color: Optional[str] = None, base_style: Optional[str] = None) -> str:
"""Create a custom style in the document.
Args:
filename: Path to the Word document
style_name: Name for the new style
bold: Set text bold (True/False)
italic: Set text italic (True/False)
font_size: Font size in points
font_name: Font name/family
color: Text color (e.g., 'red', 'blue')
base_style: Optional existing style to base this on
"""
if not filename.endswith('.docx'):
filename += '.docx'
if not os.path.exists(filename):
return f"Document {filename} does not exist"
# Check if file is writeable
is_writeable, error_message = check_file_writeable(filename)
if not is_writeable:
return f"Cannot modify document: {error_message}. Consider creating a copy first."
try:
doc = Document(filename)
# Build font properties dictionary
font_properties = {}
if bold is not None:
font_properties['bold'] = bold
if italic is not None:
font_properties['italic'] = italic
if font_size is not None:
font_properties['size'] = font_size
if font_name is not None:
font_properties['name'] = font_name
if color is not None:
font_properties['color'] = color
# Create the style
new_style = create_style(
doc,
style_name,
WD_STYLE_TYPE.PARAGRAPH,
base_style=base_style,
font_properties=font_properties
)
doc.save(filename)
return f"Style '{style_name}' created successfully."
except Exception as e:
return f"Failed to create style: {str(e)}"
@mcp.tool()
async def format_table(filename: str, table_index: int,
has_header_row: Optional[bool] = None,
border_style: Optional[str] = None,
shading: Optional[List[List[str]]] = None) -> str:
"""Format a table with borders, shading, and structure.
Args:
filename: Path to the Word document
table_index: Index of the table (0-based)
has_header_row: If True, formats the first row as a header
border_style: Style for borders ('none', 'single', 'double', 'thick')
shading: 2D list of cell background colors (by row and column)
"""
if not filename.endswith('.docx'):
filename += '.docx'
if not os.path.exists(filename):
return f"Document {filename} does not exist"
# Check if file is writeable
is_writeable, error_message = check_file_writeable(filename)
if not is_writeable:
return f"Cannot modify document: {error_message}. Consider creating a copy first."
try:
doc = Document(filename)
# Validate table index
if table_index < 0 or table_index >= len(doc.tables):
return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
table = doc.tables[table_index]
# Format header row if requested
if has_header_row and table.rows:
header_row = table.rows[0]
for cell in header_row.cells:
for paragraph in cell.paragraphs:
if paragraph.runs:
for run in paragraph.runs:
run.bold = True
# Apply border style if specified
if border_style:
val_map = {
'none': 'nil',
'single': 'single',
'double': 'double',
'thick': 'thick'
}
val = val_map.get(border_style.lower(), 'single')
# Apply to all cells
for row in table.rows:
for cell in row.cells:
set_cell_border(
cell,
top=True,
bottom=True,
left=True,
right=True,
val=val,
color="000000"
)
# Apply cell shading if specified
if shading:
for i, row_colors in enumerate(shading):
if i >= len(table.rows):
break
for j, color in enumerate(row_colors):
if j >= len(table.rows[i].cells):
break
try:
# Apply shading to cell
cell = table.rows[i].cells[j]
shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{color}"/>')
cell._tc.get_or_add_tcPr().append(shading_elm)
except:
# Skip if color format is invalid
pass
doc.save(filename)
return f"Table at index {table_index} formatted successfully."
except Exception as e:
return f"Failed to format table: {str(e)}"
@mcp.tool()
async def add_page_break(filename: str) -> str:
"""Add a page break to the document.
Args:
filename: Path to the Word document
"""
if not filename.endswith('.docx'):
filename += '.docx'
if not os.path.exists(filename):
return f"Document {filename} does not exist"
# Check if file is writeable
is_writeable, error_message = check_file_writeable(filename)
if not is_writeable:
return f"Cannot modify document: {error_message}. Consider creating a copy first."
try:
doc = Document(filename)
doc.add_page_break()
doc.save(filename)
return f"Page break added to {filename}."
except Exception as e:
return f"Failed to add page break: {str(e)}"
# Main execution point
def main():
"""Entry point for the MCP server."""
# Run the server
mcp.run(transport='stdio')
if __name__ == "__main__":
main()