Skip to content
Merged
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
__pycache__/
*.py[cod]
*.so
.mcp.json
claude.md

# ---- Packaging ---------------------------------------------------------
*.egg-info/
Expand All @@ -22,4 +24,8 @@ venv/
client_secret.json

# ---- Logs --------------------------------------------------------------
mcp_server_debug.log
mcp_server_debug.log

# ---- Local development files -------------------------------------------
/.credentials
/.claude
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ A production-ready MCP server that integrates all major Google Workspace service
- **📅 Google Calendar**: Full calendar management with event CRUD operations
- **📁 Google Drive**: File operations with native Microsoft Office format support (.docx, .xlsx)
- **📧 Gmail**: Complete email management with search, send, and draft capabilities
- **📄 Google Docs**: Document operations including content extraction, creation, and comment management
- **📄 Google Docs**: Complete document management including content extraction, creation, full editing capabilities, and comment management
- **📊 Google Sheets**: Comprehensive spreadsheet management with flexible cell operations and comment management
- **🖼️ Google Slides**: Presentation management with slide creation, updates, content manipulation, and comment management
- **📝 Google Forms**: Form creation, retrieval, publish settings, and response management
Expand Down Expand Up @@ -499,6 +499,13 @@ When calling a tool:
| `get_doc_content` | Extract document text |
| `list_docs_in_folder` | List docs in folder |
| `create_doc` | Create new documents |
| `update_doc_text` | Insert or replace text at specific positions |
| `find_and_replace_doc` | Find and replace text throughout document |
| `format_doc_text` | Apply text formatting (bold, italic, underline, fonts) |
| `insert_doc_elements` | Add tables, lists, or page breaks |
| `insert_doc_image` | Insert images from Drive or URLs |
| `update_doc_headers_footers` | Modify document headers and footers |
| `batch_update_doc` | Execute multiple document operations atomically |
| `read_doc_comments` | Read all comments and replies |
| `create_doc_comment` | Create new comments |
| `reply_to_comment` | Reply to existing comments |
Expand Down
299 changes: 299 additions & 0 deletions gdocs/docs_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
"""
Google Docs Helper Functions

This module provides utility functions for common Google Docs operations
to simplify the implementation of document editing tools.
"""
import logging
from typing import Dict, Any, Optional, Tuple

logger = logging.getLogger(__name__)

def build_text_style(
bold: bool = None,
italic: bool = None,
underline: bool = None,
font_size: int = None,
font_family: str = None
) -> tuple[Dict[str, Any], list[str]]:
"""
Build text style object for Google Docs API requests.

Args:
bold: Whether text should be bold
italic: Whether text should be italic
underline: Whether text should be underlined
font_size: Font size in points
font_family: Font family name

Returns:
Tuple of (text_style_dict, list_of_field_names)
"""
text_style = {}
fields = []

if bold is not None:
text_style['bold'] = bold
fields.append('bold')

if italic is not None:
text_style['italic'] = italic
fields.append('italic')

if underline is not None:
text_style['underline'] = underline
fields.append('underline')

if font_size is not None:
text_style['fontSize'] = {'magnitude': font_size, 'unit': 'PT'}
fields.append('fontSize')

if font_family is not None:
text_style['weightedFontFamily'] = {'fontFamily': font_family}
fields.append('weightedFontFamily')

return text_style, fields

def create_insert_text_request(index: int, text: str) -> Dict[str, Any]:
"""
Create an insertText request for Google Docs API.

Args:
index: Position to insert text
text: Text to insert

Returns:
Dictionary representing the insertText request
"""
return {
'insertText': {
'location': {'index': index},
'text': text
}
}

def create_delete_range_request(start_index: int, end_index: int) -> Dict[str, Any]:
"""
Create a deleteContentRange request for Google Docs API.

Args:
start_index: Start position of content to delete
end_index: End position of content to delete

Returns:
Dictionary representing the deleteContentRange request
"""
return {
'deleteContentRange': {
'range': {
'startIndex': start_index,
'endIndex': end_index
}
}
}

def create_format_text_request(
start_index: int,
end_index: int,
bold: bool = None,
italic: bool = None,
underline: bool = None,
font_size: int = None,
font_family: str = None
) -> Optional[Dict[str, Any]]:
"""
Create an updateTextStyle request for Google Docs API.

Args:
start_index: Start position of text to format
end_index: End position of text to format
bold: Whether text should be bold
italic: Whether text should be italic
underline: Whether text should be underlined
font_size: Font size in points
font_family: Font family name

Returns:
Dictionary representing the updateTextStyle request, or None if no styles provided
"""
text_style, fields = build_text_style(bold, italic, underline, font_size, font_family)

if not text_style:
return None

return {
'updateTextStyle': {
'range': {
'startIndex': start_index,
'endIndex': end_index
},
'textStyle': text_style,
'fields': ','.join(fields)
}
}

def create_find_replace_request(
find_text: str,
replace_text: str,
match_case: bool = False
) -> Dict[str, Any]:
"""
Create a replaceAllText request for Google Docs API.

Args:
find_text: Text to find
replace_text: Text to replace with
match_case: Whether to match case exactly

Returns:
Dictionary representing the replaceAllText request
"""
return {
'replaceAllText': {
'containsText': {
'text': find_text,
'matchCase': match_case
},
'replaceText': replace_text
}
}

def create_insert_table_request(index: int, rows: int, columns: int) -> Dict[str, Any]:
"""
Create an insertTable request for Google Docs API.

Args:
index: Position to insert table
rows: Number of rows
columns: Number of columns

Returns:
Dictionary representing the insertTable request
"""
return {
'insertTable': {
'location': {'index': index},
'rows': rows,
'columns': columns
}
}

def create_insert_page_break_request(index: int) -> Dict[str, Any]:
"""
Create an insertPageBreak request for Google Docs API.

Args:
index: Position to insert page break

Returns:
Dictionary representing the insertPageBreak request
"""
return {
'insertPageBreak': {
'location': {'index': index}
}
}

def create_insert_image_request(
index: int,
image_uri: str,
width: int = None,
height: int = None
) -> Dict[str, Any]:
"""
Create an insertInlineImage request for Google Docs API.

Args:
index: Position to insert image
image_uri: URI of the image (Drive URL or public URL)
width: Image width in points
height: Image height in points

Returns:
Dictionary representing the insertInlineImage request
"""
request = {
'insertInlineImage': {
'location': {'index': index},
'uri': image_uri
}
}

# Add size properties if specified
object_size = {}
if width is not None:
object_size['width'] = {'magnitude': width, 'unit': 'PT'}
if height is not None:
object_size['height'] = {'magnitude': height, 'unit': 'PT'}

if object_size:
request['insertInlineImage']['objectSize'] = object_size

return request

def create_bullet_list_request(
start_index: int,
end_index: int,
list_type: str = "UNORDERED"
) -> Dict[str, Any]:
"""
Create a createParagraphBullets request for Google Docs API.

Args:
start_index: Start of text range to convert to list
end_index: End of text range to convert to list
list_type: Type of list ("UNORDERED" or "ORDERED")

Returns:
Dictionary representing the createParagraphBullets request
"""
bullet_preset = (
'BULLET_DISC_CIRCLE_SQUARE'
if list_type == "UNORDERED"
else 'NUMBERED_DECIMAL_ALPHA_ROMAN'
)

return {
'createParagraphBullets': {
'range': {
'startIndex': start_index,
'endIndex': end_index
},
'bulletPreset': bullet_preset
}
}

def validate_operation(operation: Dict[str, Any]) -> Tuple[bool, str]:
Copy link

Copilot AI Aug 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing import for Tuple. The function uses Tuple in its return type annotation but Tuple is not imported from the typing module at the top of the file.

Copilot uses AI. Check for mistakes.
"""
Validate a batch operation dictionary.

Args:
operation: Operation dictionary to validate

Returns:
Tuple of (is_valid, error_message)
"""
op_type = operation.get('type')
if not op_type:
return False, "Missing 'type' field"

# Validate required fields for each operation type
required_fields = {
'insert_text': ['index', 'text'],
'delete_text': ['start_index', 'end_index'],
'replace_text': ['start_index', 'end_index', 'text'],
'format_text': ['start_index', 'end_index'],
'insert_table': ['index', 'rows', 'columns'],
'insert_page_break': ['index'],
'find_replace': ['find_text', 'replace_text']
}

if op_type not in required_fields:
return False, f"Unsupported operation type: {op_type or 'None'}"

for field in required_fields[op_type]:
if field not in operation:
return False, f"Missing required field: {field}"

return True, ""

Loading