diff --git a/.gitignore b/.gitignore
index 59278b6..c73adf2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,8 @@
 __pycache__/
 *.py[cod]
 *.so
+.mcp.json
+claude.md
 
 # ---- Packaging ---------------------------------------------------------
 *.egg-info/
@@ -22,4 +24,8 @@ venv/
 client_secret.json
 
 # ---- Logs --------------------------------------------------------------
-mcp_server_debug.log
\ No newline at end of file
+mcp_server_debug.log
+
+# ---- Local development files -------------------------------------------
+/.credentials
+/.claude
diff --git a/README.md b/README.md
index 2f50915..ee57ebf 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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 |
diff --git a/gdocs/docs_helpers.py b/gdocs/docs_helpers.py
new file mode 100644
index 0000000..b3baf98
--- /dev/null
+++ b/gdocs/docs_helpers.py
@@ -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 List, 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['fontFamily'] = font_family
+        fields.append('fontFamily')
+    
+    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]:
+    """
+    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, ""
+
diff --git a/gdocs/docs_structure.py b/gdocs/docs_structure.py
new file mode 100644
index 0000000..8feec4e
--- /dev/null
+++ b/gdocs/docs_structure.py
@@ -0,0 +1,340 @@
+"""
+Google Docs Document Structure Parsing and Analysis
+
+This module provides utilities for parsing and analyzing the structure
+of Google Docs documents, including finding tables, cells, and other elements.
+"""
+import logging
+from typing import Dict, Any, List, Optional, Tuple
+
+logger = logging.getLogger(__name__)
+
+
+def parse_document_structure(doc_data: Dict[str, Any]) -> Dict[str, Any]:
+    """
+    Parse the full document structure into a navigable format.
+    
+    Args:
+        doc_data: Raw document data from Google Docs API
+    
+    Returns:
+        Dictionary containing parsed structure with elements and their positions
+    """
+    structure = {
+        'title': doc_data.get('title', ''),
+        'body': [],
+        'tables': [],
+        'headers': {},
+        'footers': {},
+        'total_length': 0
+    }
+    
+    body = doc_data.get('body', {})
+    content = body.get('content', [])
+    
+    for element in content:
+        element_info = _parse_element(element)
+        if element_info:
+            structure['body'].append(element_info)
+            if element_info['type'] == 'table':
+                structure['tables'].append(element_info)
+    
+    # Calculate total document length
+    if structure['body']:
+        last_element = structure['body'][-1]
+        structure['total_length'] = last_element.get('end_index', 0)
+    
+    # Parse headers and footers
+    for header_id, header_data in doc_data.get('headers', {}).items():
+        structure['headers'][header_id] = _parse_segment(header_data)
+    
+    for footer_id, footer_data in doc_data.get('footers', {}).items():
+        structure['footers'][footer_id] = _parse_segment(footer_data)
+    
+    return structure
+
+
+def _parse_element(element: Dict[str, Any]) -> Optional[Dict[str, Any]]:
+    """
+    Parse a single document element.
+    
+    Args:
+        element: Element data from document
+    
+    Returns:
+        Parsed element information or None
+    """
+    element_info = {
+        'start_index': element.get('startIndex', 0),
+        'end_index': element.get('endIndex', 0)
+    }
+    
+    if 'paragraph' in element:
+        paragraph = element['paragraph']
+        element_info['type'] = 'paragraph'
+        element_info['text'] = _extract_paragraph_text(paragraph)
+        element_info['style'] = paragraph.get('paragraphStyle', {})
+        
+    elif 'table' in element:
+        table = element['table']
+        element_info['type'] = 'table'
+        element_info['rows'] = len(table.get('tableRows', []))
+        element_info['columns'] = len(table.get('tableRows', [{}])[0].get('tableCells', []))
+        element_info['cells'] = _parse_table_cells(table)
+        element_info['table_style'] = table.get('tableStyle', {})
+        
+    elif 'sectionBreak' in element:
+        element_info['type'] = 'section_break'
+        element_info['section_style'] = element['sectionBreak'].get('sectionStyle', {})
+        
+    elif 'tableOfContents' in element:
+        element_info['type'] = 'table_of_contents'
+        
+    else:
+        return None
+    
+    return element_info
+
+
+def _parse_table_cells(table: Dict[str, Any]) -> List[List[Dict[str, Any]]]:
+    """
+    Parse table cells with their positions and content.
+    
+    Args:
+        table: Table element data
+    
+    Returns:
+        2D list of cell information
+    """
+    cells = []
+    for row_idx, row in enumerate(table.get('tableRows', [])):
+        row_cells = []
+        for col_idx, cell in enumerate(row.get('tableCells', [])):
+            # Find the first paragraph in the cell for insertion
+            insertion_index = cell.get('startIndex', 0) + 1  # Default fallback
+            
+            # Look for the first paragraph in cell content
+            content_elements = cell.get('content', [])
+            for element in content_elements:
+                if 'paragraph' in element:
+                    paragraph = element['paragraph']
+                    # Get the first element in the paragraph
+                    para_elements = paragraph.get('elements', [])
+                    if para_elements:
+                        first_element = para_elements[0]
+                        if 'startIndex' in first_element:
+                            insertion_index = first_element['startIndex']
+                            break
+            
+            cell_info = {
+                'row': row_idx,
+                'column': col_idx,
+                'start_index': cell.get('startIndex', 0),
+                'end_index': cell.get('endIndex', 0),
+                'insertion_index': insertion_index,  # Where to insert text in this cell
+                'content': _extract_cell_text(cell),
+                'content_elements': content_elements
+            }
+            row_cells.append(cell_info)
+        cells.append(row_cells)
+    return cells
+
+
+def _extract_paragraph_text(paragraph: Dict[str, Any]) -> str:
+    """Extract text from a paragraph element."""
+    text_parts = []
+    for element in paragraph.get('elements', []):
+        if 'textRun' in element:
+            text_parts.append(element['textRun'].get('content', ''))
+    return ''.join(text_parts)
+
+
+def _extract_cell_text(cell: Dict[str, Any]) -> str:
+    """Extract text content from a table cell."""
+    text_parts = []
+    for element in cell.get('content', []):
+        if 'paragraph' in element:
+            text_parts.append(_extract_paragraph_text(element['paragraph']))
+    return ''.join(text_parts)
+
+
+def _parse_segment(segment_data: Dict[str, Any]) -> Dict[str, Any]:
+    """Parse a document segment (header/footer)."""
+    return {
+        'content': segment_data.get('content', []),
+        'start_index': segment_data.get('content', [{}])[0].get('startIndex', 0) if segment_data.get('content') else 0,
+        'end_index': segment_data.get('content', [{}])[-1].get('endIndex', 0) if segment_data.get('content') else 0
+    }
+
+
+def find_tables(doc_data: Dict[str, Any]) -> List[Dict[str, Any]]:
+    """
+    Find all tables in the document with their positions and dimensions.
+    
+    Args:
+        doc_data: Raw document data from Google Docs API
+    
+    Returns:
+        List of table information dictionaries
+    """
+    tables = []
+    structure = parse_document_structure(doc_data)
+    
+    for idx, table_info in enumerate(structure['tables']):
+        tables.append({
+            'index': idx,
+            'start_index': table_info['start_index'],
+            'end_index': table_info['end_index'],
+            'rows': table_info['rows'],
+            'columns': table_info['columns'],
+            'cells': table_info['cells']
+        })
+    
+    return tables
+
+
+def get_table_cell_indices(doc_data: Dict[str, Any], table_index: int = 0) -> Optional[List[List[Tuple[int, int]]]]:
+    """
+    Get content indices for all cells in a specific table.
+    
+    Args:
+        doc_data: Raw document data from Google Docs API
+        table_index: Index of the table (0-based)
+    
+    Returns:
+        2D list of (start_index, end_index) tuples for each cell, or None if table not found
+    """
+    tables = find_tables(doc_data)
+    
+    if table_index >= len(tables):
+        logger.warning(f"Table index {table_index} not found. Document has {len(tables)} tables.")
+        return None
+    
+    table = tables[table_index]
+    cell_indices = []
+    
+    for row in table['cells']:
+        row_indices = []
+        for cell in row:
+            # Each cell contains at least one paragraph
+            # Find the first paragraph in the cell for content insertion
+            cell_content = cell.get('content_elements', [])
+            if cell_content:
+                # Look for the first paragraph in cell content
+                first_para = None
+                for element in cell_content:
+                    if 'paragraph' in element:
+                        first_para = element['paragraph']
+                        break
+                
+                if first_para and 'elements' in first_para and first_para['elements']:
+                    # Insert at the start of the first text run in the paragraph
+                    first_text_element = first_para['elements'][0]
+                    if 'textRun' in first_text_element:
+                        start_idx = first_text_element.get('startIndex', cell['start_index'] + 1)
+                        end_idx = first_text_element.get('endIndex', start_idx + 1)
+                        row_indices.append((start_idx, end_idx))
+                        continue
+            
+            # Fallback: use cell boundaries with safe margins
+            content_start = cell['start_index'] + 1  
+            content_end = cell['end_index'] - 1
+            row_indices.append((content_start, content_end))
+        cell_indices.append(row_indices)
+    
+    return cell_indices
+
+
+def find_element_at_index(doc_data: Dict[str, Any], index: int) -> Optional[Dict[str, Any]]:
+    """
+    Find what element exists at a given index in the document.
+    
+    Args:
+        doc_data: Raw document data from Google Docs API
+        index: Position in the document
+    
+    Returns:
+        Information about the element at that position, or None
+    """
+    structure = parse_document_structure(doc_data)
+    
+    for element in structure['body']:
+        if element['start_index'] <= index < element['end_index']:
+            element_copy = element.copy()
+            
+            # If it's a table, find which cell contains the index
+            if element['type'] == 'table' and 'cells' in element:
+                for row_idx, row in enumerate(element['cells']):
+                    for col_idx, cell in enumerate(row):
+                        if cell['start_index'] <= index < cell['end_index']:
+                            element_copy['containing_cell'] = {
+                                'row': row_idx,
+                                'column': col_idx,
+                                'cell_start': cell['start_index'],
+                                'cell_end': cell['end_index']
+                            }
+                            break
+            
+            return element_copy
+    
+    return None
+
+
+def get_next_paragraph_index(doc_data: Dict[str, Any], after_index: int = 0) -> int:
+    """
+    Find the next safe position to insert content after a given index.
+    
+    Args:
+        doc_data: Raw document data from Google Docs API
+        after_index: Index after which to find insertion point
+    
+    Returns:
+        Safe index for insertion
+    """
+    structure = parse_document_structure(doc_data)
+    
+    # Find the first paragraph element after the given index
+    for element in structure['body']:
+        if element['type'] == 'paragraph' and element['start_index'] > after_index:
+            # Insert at the end of the previous element or start of this paragraph
+            return element['start_index']
+    
+    # If no paragraph found, return the end of document
+    return structure['total_length'] - 1 if structure['total_length'] > 0 else 1
+
+
+def analyze_document_complexity(doc_data: Dict[str, Any]) -> Dict[str, Any]:
+    """
+    Analyze document complexity and provide statistics.
+    
+    Args:
+        doc_data: Raw document data from Google Docs API
+    
+    Returns:
+        Dictionary with document statistics
+    """
+    structure = parse_document_structure(doc_data)
+    
+    stats = {
+        'total_elements': len(structure['body']),
+        'tables': len(structure['tables']),
+        'paragraphs': sum(1 for e in structure['body'] if e.get('type') == 'paragraph'),
+        'section_breaks': sum(1 for e in structure['body'] if e.get('type') == 'section_break'),
+        'total_length': structure['total_length'],
+        'has_headers': bool(structure['headers']),
+        'has_footers': bool(structure['footers'])
+    }
+    
+    # Add table statistics
+    if structure['tables']:
+        total_cells = sum(
+            table['rows'] * table['columns'] 
+            for table in structure['tables']
+        )
+        stats['total_table_cells'] = total_cells
+        stats['largest_table'] = max(
+            (t['rows'] * t['columns'] for t in structure['tables']),
+            default=0
+        )
+    
+    return stats
\ No newline at end of file
diff --git a/gdocs/docs_tables.py b/gdocs/docs_tables.py
new file mode 100644
index 0000000..0014121
--- /dev/null
+++ b/gdocs/docs_tables.py
@@ -0,0 +1,442 @@
+"""
+Google Docs Table Operations
+
+This module provides utilities for creating and manipulating tables
+in Google Docs, including population with data and formatting.
+"""
+import logging
+from typing import Dict, Any, List, Optional, Union, Tuple
+
+logger = logging.getLogger(__name__)
+
+
+def build_table_population_requests(
+    table_info: Dict[str, Any], 
+    data: List[List[str]],
+    bold_headers: bool = True
+) -> List[Dict[str, Any]]:
+    """
+    Build batch requests to populate a table with data.
+    
+    Args:
+        table_info: Table information from document structure including cell indices
+        data: 2D array of data to insert into table
+        bold_headers: Whether to make the first row bold
+    
+    Returns:
+        List of request dictionaries for batch update
+    """
+    requests = []
+    cells = table_info.get('cells', [])
+    
+    if not cells:
+        logger.warning("No cell information found in table_info")
+        return requests
+    
+    # Process each cell - ONLY INSERT, DON'T DELETE
+    for row_idx, row_data in enumerate(data):
+        if row_idx >= len(cells):
+            logger.warning(f"Data has more rows ({len(data)}) than table ({len(cells)})")
+            break
+            
+        for col_idx, cell_text in enumerate(row_data):
+            if col_idx >= len(cells[row_idx]):
+                logger.warning(f"Data has more columns ({len(row_data)}) than table row {row_idx} ({len(cells[row_idx])})")
+                break
+            
+            cell = cells[row_idx][col_idx]
+            
+            # For new empty tables, use the insertion index
+            # For tables with existing content, check if cell only contains newline
+            existing_content = cell.get('content', '').strip()
+            
+            # Only insert if we have text to insert
+            if cell_text:
+                # Use the specific insertion index for this cell
+                insertion_index = cell.get('insertion_index', cell['start_index'] + 1)
+                
+                # If cell only contains a newline, replace it
+                if existing_content == '' or existing_content == '\n':
+                    # Cell is empty (just newline), insert at the insertion index
+                    requests.append({
+                        'insertText': {
+                            'location': {'index': insertion_index},
+                            'text': cell_text
+                        }
+                    })
+                    
+                    # Apply bold formatting to first row if requested
+                    if bold_headers and row_idx == 0:
+                        requests.append({
+                            'updateTextStyle': {
+                                'range': {
+                                    'startIndex': insertion_index,
+                                    'endIndex': insertion_index + len(cell_text)
+                                },
+                                'textStyle': {'bold': True},
+                                'fields': 'bold'
+                            }
+                        })
+                else:
+                    # Cell has content, append after existing content
+                    # Find the end of existing content
+                    cell_end = cell['end_index'] - 1  # Don't include cell end marker
+                    requests.append({
+                        'insertText': {
+                            'location': {'index': cell_end},
+                            'text': cell_text
+                        }
+                    })
+                    
+                    # Apply bold formatting to first row if requested
+                    if bold_headers and row_idx == 0:
+                        requests.append({
+                            'updateTextStyle': {
+                                'range': {
+                                    'startIndex': cell_end,
+                                    'endIndex': cell_end + len(cell_text)
+                                },
+                                'textStyle': {'bold': True},
+                                'fields': 'bold'
+                            }
+                        })
+    
+    return requests
+
+
+def calculate_cell_positions(
+    table_start_index: int, 
+    rows: int, 
+    cols: int,
+    existing_table_data: Optional[Dict[str, Any]] = None
+) -> List[List[Dict[str, int]]]:
+    """
+    Calculate estimated positions for each cell in a table.
+    
+    Args:
+        table_start_index: Starting index of the table
+        rows: Number of rows
+        cols: Number of columns
+        existing_table_data: Optional existing table data with actual positions
+    
+    Returns:
+        2D list of cell position dictionaries
+    """
+    if existing_table_data and 'cells' in existing_table_data:
+        # Use actual positions from existing table
+        return existing_table_data['cells']
+    
+    # Estimate positions for a new table
+    # Note: These are estimates; actual positions depend on content
+    cells = []
+    current_index = table_start_index + 2  # Account for table start
+    
+    for row_idx in range(rows):
+        row_cells = []
+        for col_idx in range(cols):
+            # Each cell typically starts with a paragraph marker
+            cell_start = current_index
+            cell_end = current_index + 2  # Minimum cell size
+            
+            row_cells.append({
+                'row': row_idx,
+                'column': col_idx,
+                'start_index': cell_start,
+                'end_index': cell_end
+            })
+            
+            current_index = cell_end + 1
+        
+        cells.append(row_cells)
+    
+    return cells
+
+
+def format_table_data(raw_data: Union[List[List[str]], List[str], str]) -> List[List[str]]:
+    """
+    Normalize various data formats into a 2D array for table insertion.
+    
+    Args:
+        raw_data: Data in various formats (2D list, 1D list, or delimited string)
+    
+    Returns:
+        Normalized 2D list of strings
+    """
+    if isinstance(raw_data, str):
+        # Parse delimited string (detect delimiter)
+        lines = raw_data.strip().split('\n')
+        if '\t' in raw_data:
+            # Tab-delimited
+            return [line.split('\t') for line in lines]
+        elif ',' in raw_data:
+            # Comma-delimited (simple CSV)
+            return [line.split(',') for line in lines]
+        else:
+            # Space-delimited or single column
+            return [[cell.strip() for cell in line.split()] for line in lines]
+    
+    elif isinstance(raw_data, list):
+        if not raw_data:
+            return [[]]
+        
+        # Check if it's already a 2D list
+        if isinstance(raw_data[0], list):
+            # Ensure all cells are strings
+            return [[str(cell) for cell in row] for row in raw_data]
+        else:
+            # Convert 1D list to single-column table
+            return [[str(cell)] for cell in raw_data]
+    
+    else:
+        # Convert single value to 1x1 table
+        return [[str(raw_data)]]
+
+
+def create_table_with_data(
+    index: int,
+    data: List[List[str]],
+    headers: Optional[List[str]] = None,
+    bold_headers: bool = True
+) -> List[Dict[str, Any]]:
+    """
+    Create a table and populate it with data in one operation.
+    
+    Args:
+        index: Position to insert the table
+        data: 2D array of table data
+        headers: Optional header row (will be prepended to data)
+        bold_headers: Whether to make headers bold
+    
+    Returns:
+        List of request dictionaries for batch update
+    """
+    requests = []
+    
+    # Prepare data with headers if provided
+    if headers:
+        full_data = [headers] + data
+    else:
+        full_data = data
+    
+    # Normalize the data
+    full_data = format_table_data(full_data)
+    
+    if not full_data or not full_data[0]:
+        raise ValueError("Cannot create table with empty data")
+    
+    rows = len(full_data)
+    cols = len(full_data[0])
+    
+    # Ensure all rows have the same number of columns
+    for row in full_data:
+        while len(row) < cols:
+            row.append('')
+    
+    # Create the table
+    requests.append({
+        'insertTable': {
+            'location': {'index': index},
+            'rows': rows,
+            'columns': cols
+        }
+    })
+    
+    # We need to calculate where cells will be after table creation
+    # This is approximate - better to get actual positions after creation
+    estimated_cells = calculate_cell_positions(index, rows, cols)
+    
+    # Build text insertion requests for each cell
+    # Note: In practice, we'd need to get the actual document structure
+    # after table creation to get accurate indices
+    
+    return requests
+
+
+def build_table_style_requests(
+    table_start_index: int,
+    style_options: Dict[str, Any]
+) -> List[Dict[str, Any]]:
+    """
+    Build requests to style a table.
+    
+    Args:
+        table_start_index: Starting index of the table
+        style_options: Dictionary of style options
+            - border_width: Width of borders in points
+            - border_color: RGB color for borders
+            - background_color: RGB color for cell backgrounds
+            - header_background: RGB color for header row background
+    
+    Returns:
+        List of request dictionaries for styling
+    """
+    requests = []
+    
+    # Table cell style update
+    if any(k in style_options for k in ['border_width', 'border_color', 'background_color']):
+        table_cell_style = {}
+        fields = []
+        
+        if 'border_width' in style_options:
+            border_width = {'magnitude': style_options['border_width'], 'unit': 'PT'}
+            table_cell_style['borderTop'] = {'width': border_width}
+            table_cell_style['borderBottom'] = {'width': border_width}
+            table_cell_style['borderLeft'] = {'width': border_width}
+            table_cell_style['borderRight'] = {'width': border_width}
+            fields.extend(['borderTop', 'borderBottom', 'borderLeft', 'borderRight'])
+        
+        if 'border_color' in style_options:
+            border_color = {'color': {'rgbColor': style_options['border_color']}}
+            if 'borderTop' in table_cell_style:
+                table_cell_style['borderTop']['color'] = border_color['color']
+                table_cell_style['borderBottom']['color'] = border_color['color']
+                table_cell_style['borderLeft']['color'] = border_color['color']
+                table_cell_style['borderRight']['color'] = border_color['color']
+        
+        if 'background_color' in style_options:
+            table_cell_style['backgroundColor'] = {
+                'color': {'rgbColor': style_options['background_color']}
+            }
+            fields.append('backgroundColor')
+        
+        if table_cell_style and fields:
+            requests.append({
+                'updateTableCellStyle': {
+                    'tableStartLocation': {'index': table_start_index},
+                    'tableCellStyle': table_cell_style,
+                    'fields': ','.join(fields)
+                }
+            })
+    
+    # Header row specific styling
+    if 'header_background' in style_options:
+        requests.append({
+            'updateTableCellStyle': {
+                'tableRange': {
+                    'tableCellLocation': {
+                        'tableStartLocation': {'index': table_start_index},
+                        'rowIndex': 0,
+                        'columnIndex': 0
+                    },
+                    'rowSpan': 1,
+                    'columnSpan': 100  # Large number to cover all columns
+                },
+                'tableCellStyle': {
+                    'backgroundColor': {
+                        'color': {'rgbColor': style_options['header_background']}
+                    }
+                },
+                'fields': 'backgroundColor'
+            }
+        })
+    
+    return requests
+
+
+def extract_table_as_data(table_info: Dict[str, Any]) -> List[List[str]]:
+    """
+    Extract table content as a 2D array of strings.
+    
+    Args:
+        table_info: Table information from document structure
+    
+    Returns:
+        2D list of cell contents
+    """
+    data = []
+    cells = table_info.get('cells', [])
+    
+    for row in cells:
+        row_data = []
+        for cell in row:
+            row_data.append(cell.get('content', '').strip())
+        data.append(row_data)
+    
+    return data
+
+
+def find_table_by_content(
+    tables: List[Dict[str, Any]], 
+    search_text: str,
+    case_sensitive: bool = False
+) -> Optional[int]:
+    """
+    Find a table index by searching for content within it.
+    
+    Args:
+        tables: List of table information from document
+        search_text: Text to search for in table cells
+        case_sensitive: Whether to do case-sensitive search
+    
+    Returns:
+        Index of the first matching table, or None
+    """
+    search_text = search_text if case_sensitive else search_text.lower()
+    
+    for idx, table in enumerate(tables):
+        for row in table.get('cells', []):
+            for cell in row:
+                cell_content = cell.get('content', '')
+                if not case_sensitive:
+                    cell_content = cell_content.lower()
+                
+                if search_text in cell_content:
+                    return idx
+    
+    return None
+
+
+def validate_table_data(data: List[List[str]]) -> Tuple[bool, str]:
+    """
+    Validates table data format and provides specific error messages for LLMs.
+    
+    WHAT THIS CHECKS:
+    - Data is a 2D list (list of lists)
+    - All rows have consistent column counts
+    - Dimensions are within Google Docs limits
+    - No None or undefined values
+    
+    VALID FORMAT EXAMPLE:
+    [
+        ["Header1", "Header2"],     # Row 0 - 2 columns
+        ["Data1", "Data2"],        # Row 1 - 2 columns  
+        ["Data3", "Data4"]         # Row 2 - 2 columns
+    ]
+    
+    INVALID FORMATS:
+    - [["col1"], ["col1", "col2"]]  # Inconsistent column counts
+    - ["col1", "col2"]              # Not 2D (missing inner lists)
+    - [["col1", None]]              # Contains None values
+    - [] or [[]]                    # Empty data
+    
+    Args:
+        data: 2D array of data to validate
+    
+    Returns:
+        Tuple of (is_valid, error_message_with_examples)
+    """
+    if not data:
+        return False, "Data is empty. Use format: [['col1', 'col2'], ['row1col1', 'row1col2']]"
+    
+    if not isinstance(data, list):
+        return False, f"Data must be a list, got {type(data).__name__}. Use format: [['col1', 'col2'], ['row1col1', 'row1col2']]"
+    
+    if not all(isinstance(row, list) for row in data):
+        return False, f"Data must be a 2D list (list of lists). Each row must be a list. Check your format: {data}"
+    
+    # Check for consistent column count
+    col_counts = [len(row) for row in data]
+    if len(set(col_counts)) > 1:
+        return False, f"All rows must have same number of columns. Found: {col_counts}. Fix your data format."
+    
+    # Check for reasonable size
+    rows = len(data)
+    cols = col_counts[0] if col_counts else 0
+    
+    if rows > 1000:
+        return False, f"Too many rows ({rows}). Google Docs limit is 1000 rows."
+    
+    if cols > 20:
+        return False, f"Too many columns ({cols}). Google Docs limit is 20 columns."
+    
+    return True, f"Valid table data: {rows}x{cols} table format"
\ No newline at end of file
diff --git a/gdocs/docs_tools.py b/gdocs/docs_tools.py
index 822cf4f..7227b18 100644
--- a/gdocs/docs_tools.py
+++ b/gdocs/docs_tools.py
@@ -15,6 +15,35 @@ from core.utils import extract_office_xml_text, handle_http_errors
 from core.server import server
 from core.comments import create_comment_tools
 
+# Import helper functions for document operations
+from gdocs.docs_helpers import (
+    create_insert_text_request,
+    create_delete_range_request,
+    create_format_text_request,
+    create_find_replace_request,
+    create_insert_table_request,
+    create_insert_page_break_request,
+    create_insert_image_request,
+    create_bullet_list_request,
+    validate_operation
+)
+
+# Import document structure and table utilities
+from gdocs.docs_structure import (
+    parse_document_structure,
+    find_tables,
+    get_table_cell_indices,
+    find_element_at_index,
+    analyze_document_complexity
+)
+from gdocs.docs_tables import (
+    build_table_population_requests,
+    format_table_data,
+    validate_table_data,
+    extract_table_as_data,
+    find_table_by_content
+)
+
 logger = logging.getLogger(__name__)
 
 @server.tool()
@@ -273,6 +302,940 @@ async def create_doc(
     return msg
 
 
+@server.tool()
+@handle_http_errors("update_doc_text", service_type="docs")
+@require_google_service("docs", "docs_write")
+async def update_doc_text(
+    service,
+    user_google_email: str,
+    document_id: str,
+    text: str,
+    start_index: int,
+    end_index: int = None,
+) -> str:
+    """
+    Updates text at a specific location in a Google Doc.
+
+    Args:
+        user_google_email: User's Google email address
+        document_id: ID of the document to update
+        text: New text to insert or replace with
+        start_index: Start position for text update (0-based)
+        end_index: End position for text replacement (if not provided, text is inserted)
+
+    Returns:
+        str: Confirmation message with update details
+    """
+    logger.info(f"[update_doc_text] Doc={document_id}, start={start_index}, end={end_index}")
+
+    requests = []
+
+    if end_index is not None and end_index > start_index:
+        # Replace text: delete old text, then insert new text
+        requests.extend([
+            create_delete_range_request(start_index, end_index),
+            create_insert_text_request(start_index, text)
+        ])
+        operation = f"Replaced text from index {start_index} to {end_index}"
+    else:
+        # Insert text at position
+        requests.append(create_insert_text_request(start_index, text))
+        operation = f"Inserted text at index {start_index}"
+
+    await asyncio.to_thread(
+        service.documents().batchUpdate(
+            documentId=document_id,
+            body={'requests': requests}
+        ).execute
+    )
+
+    link = f"https://docs.google.com/document/d/{document_id}/edit"
+    return f"{operation} in document {document_id}. Text length: {len(text)} characters. Link: {link}"
+
+@server.tool()
+@handle_http_errors("find_and_replace_doc", service_type="docs")
+@require_google_service("docs", "docs_write")
+async def find_and_replace_doc(
+    service,
+    user_google_email: str,
+    document_id: str,
+    find_text: str,
+    replace_text: str,
+    match_case: bool = False,
+) -> str:
+    """
+    Finds and replaces text throughout a Google Doc.
+
+    Args:
+        user_google_email: User's Google email address
+        document_id: ID of the document to update
+        find_text: Text to search for
+        replace_text: Text to replace with
+        match_case: Whether to match case exactly
+
+    Returns:
+        str: Confirmation message with replacement count
+    """
+    logger.info(f"[find_and_replace_doc] Doc={document_id}, find='{find_text}', replace='{replace_text}'")
+
+    requests = [create_find_replace_request(find_text, replace_text, match_case)]
+
+    result = await asyncio.to_thread(
+        service.documents().batchUpdate(
+            documentId=document_id,
+            body={'requests': requests}
+        ).execute
+    )
+
+    # Extract number of replacements from response
+    replacements = 0
+    if 'replies' in result and result['replies']:
+        reply = result['replies'][0]
+        if 'replaceAllText' in reply:
+            replacements = reply['replaceAllText'].get('occurrencesChanged', 0)
+
+    link = f"https://docs.google.com/document/d/{document_id}/edit"
+    return f"Replaced {replacements} occurrence(s) of '{find_text}' with '{replace_text}' in document {document_id}. Link: {link}"
+
+@server.tool()
+@handle_http_errors("format_doc_text", service_type="docs")
+@require_google_service("docs", "docs_write")
+async def format_doc_text(
+    service,
+    user_google_email: str,
+    document_id: str,
+    start_index: int,
+    end_index: int,
+    bold: bool = None,
+    italic: bool = None,
+    underline: bool = None,
+    font_size: int = None,
+    font_family: str = None,
+) -> str:
+    """
+    Applies text formatting to a specific range in a Google Doc.
+
+    Args:
+        user_google_email: User's Google email address
+        document_id: ID of the document to update
+        start_index: Start position of text to format (0-based)
+        end_index: End position of text to format
+        bold: Whether to make text bold (True/False/None to leave unchanged)
+        italic: Whether to make text italic (True/False/None to leave unchanged)
+        underline: Whether to underline text (True/False/None to leave unchanged)
+        font_size: Font size in points
+        font_family: Font family name (e.g., "Arial", "Times New Roman")
+
+    Returns:
+        str: Confirmation message with formatting details
+    """
+    logger.info(f"[format_doc_text] Doc={document_id}, range={start_index}-{end_index}")
+
+    # Use helper to create format request
+    format_request = create_format_text_request(
+        start_index, end_index, bold, italic, underline, font_size, font_family
+    )
+
+    if not format_request:
+        return "No formatting changes specified. Please provide at least one formatting option."
+
+    requests = [format_request]
+
+    # Build format_changes list for the return message
+    format_changes = []
+    if bold is not None:
+        format_changes.append(f"bold: {bold}")
+    if italic is not None:
+        format_changes.append(f"italic: {italic}")
+    if underline is not None:
+        format_changes.append(f"underline: {underline}")
+    if font_size is not None:
+        format_changes.append(f"font size: {font_size}pt")
+    if font_family is not None:
+        format_changes.append(f"font family: {font_family}")
+
+    await asyncio.to_thread(
+        service.documents().batchUpdate(
+            documentId=document_id,
+            body={'requests': requests}
+        ).execute
+    )
+
+    changes_str = ', '.join(format_changes)
+    link = f"https://docs.google.com/document/d/{document_id}/edit"
+    return f"Applied formatting ({changes_str}) to text from index {start_index} to {end_index} in document {document_id}. Link: {link}"
+
+@server.tool()
+@handle_http_errors("insert_doc_elements", service_type="docs")
+@require_google_service("docs", "docs_write")
+async def insert_doc_elements(
+    service,
+    user_google_email: str,
+    document_id: str,
+    element_type: str,
+    index: int,
+    rows: int = None,
+    columns: int = None,
+    list_type: str = None,
+    text: str = None,
+) -> str:
+    """
+    Inserts structural elements like tables, lists, or page breaks into a Google Doc.
+
+    Args:
+        user_google_email: User's Google email address
+        document_id: ID of the document to update
+        element_type: Type of element to insert ("table", "list", "page_break")
+        index: Position to insert element (0-based)
+        rows: Number of rows for table (required for table)
+        columns: Number of columns for table (required for table)
+        list_type: Type of list ("UNORDERED", "ORDERED") (required for list)
+        text: Initial text content for list items
+
+    Returns:
+        str: Confirmation message with insertion details
+    """
+    logger.info(f"[insert_doc_elements] Doc={document_id}, type={element_type}, index={index}")
+
+    requests = []
+
+    if element_type == "table":
+        if not rows or not columns:
+            return "Error: 'rows' and 'columns' parameters are required for table insertion."
+
+        requests.append(create_insert_table_request(index, rows, columns))
+        description = f"table ({rows}x{columns})"
+
+    elif element_type == "list":
+        if not list_type:
+            return "Error: 'list_type' parameter is required for list insertion ('UNORDERED' or 'ORDERED')."
+
+        if not text:
+            text = "List item"
+
+        # Insert text first, then create list
+        requests.extend([
+            create_insert_text_request(index, text + '\n'),
+            create_bullet_list_request(index, index + len(text), list_type)
+        ])
+        description = f"{list_type.lower()} list"
+
+    elif element_type == "page_break":
+        requests.append(create_insert_page_break_request(index))
+        description = "page break"
+
+    else:
+        return f"Error: Unsupported element type '{element_type}'. Supported types: 'table', 'list', 'page_break'."
+
+    await asyncio.to_thread(
+        service.documents().batchUpdate(
+            documentId=document_id,
+            body={'requests': requests}
+        ).execute
+    )
+
+    link = f"https://docs.google.com/document/d/{document_id}/edit"
+    return f"Inserted {description} at index {index} in document {document_id}. Link: {link}"
+
+@server.tool()
+@handle_http_errors("insert_doc_image", service_type="docs")
+@require_multiple_services([
+    {"service_type": "docs", "scopes": "docs_write", "param_name": "docs_service"},
+    {"service_type": "drive", "scopes": "drive_read", "param_name": "drive_service"}
+])
+async def insert_doc_image(
+    docs_service,
+    drive_service,
+    user_google_email: str,
+    document_id: str,
+    image_source: str,
+    index: int,
+    width: int = None,
+    height: int = None,
+) -> str:
+    """
+    Inserts an image into a Google Doc from Drive or a URL.
+
+    Args:
+        user_google_email: User's Google email address
+        document_id: ID of the document to update
+        image_source: Drive file ID or public image URL
+        index: Position to insert image (0-based)
+        width: Image width in points (optional)
+        height: Image height in points (optional)
+
+    Returns:
+        str: Confirmation message with insertion details
+    """
+    logger.info(f"[insert_doc_image] Doc={document_id}, source={image_source}, index={index}")
+
+    # Determine if source is a Drive file ID or URL
+    is_drive_file = not (image_source.startswith('http://') or image_source.startswith('https://'))
+
+    if is_drive_file:
+        # Verify Drive file exists and get metadata
+        try:
+            file_metadata = await asyncio.to_thread(
+                drive_service.files().get(
+                    fileId=image_source,
+                    fields="id, name, mimeType"
+                ).execute
+            )
+            mime_type = file_metadata.get('mimeType', '')
+            if not mime_type.startswith('image/'):
+                return f"Error: File {image_source} is not an image (MIME type: {mime_type})."
+
+            image_uri = f"https://drive.google.com/uc?id={image_source}"
+            source_description = f"Drive file {file_metadata.get('name', image_source)}"
+        except Exception as e:
+            return f"Error: Could not access Drive file {image_source}: {str(e)}"
+    else:
+        image_uri = image_source
+        source_description = "URL image"
+
+    # Use helper to create image request
+    requests = [create_insert_image_request(index, image_uri, width, height)]
+
+    await asyncio.to_thread(
+        docs_service.documents().batchUpdate(
+            documentId=document_id,
+            body={'requests': requests}
+        ).execute
+    )
+
+    size_info = ""
+    if width or height:
+        size_info = f" (size: {width or 'auto'}x{height or 'auto'} points)"
+
+    link = f"https://docs.google.com/document/d/{document_id}/edit"
+    return f"Inserted {source_description}{size_info} at index {index} in document {document_id}. Link: {link}"
+
+@server.tool()
+@handle_http_errors("update_doc_headers_footers", service_type="docs")
+@require_google_service("docs", "docs_write")
+async def update_doc_headers_footers(
+    service,
+    user_google_email: str,
+    document_id: str,
+    section_type: str,
+    content: str,
+    header_footer_type: str = "DEFAULT",
+) -> str:
+    """
+    Updates headers or footers in a Google Doc.
+
+    Args:
+        user_google_email: User's Google email address
+        document_id: ID of the document to update
+        section_type: Type of section to update ("header" or "footer")
+        content: Text content for the header/footer
+        header_footer_type: Type of header/footer ("DEFAULT", "FIRST_PAGE_ONLY", "EVEN_PAGE")
+
+    Returns:
+        str: Confirmation message with update details
+    """
+    logger.info(f"[update_doc_headers_footers] Doc={document_id}, type={section_type}")
+
+    if section_type not in ["header", "footer"]:
+        return "Error: section_type must be 'header' or 'footer'."
+
+    if header_footer_type not in ["DEFAULT", "FIRST_PAGE_ONLY", "EVEN_PAGE"]:
+        return "Error: header_footer_type must be 'DEFAULT', 'FIRST_PAGE_ONLY', or 'EVEN_PAGE'."
+
+    # First, get the document to find existing header/footer
+    doc = await asyncio.to_thread(
+        service.documents().get(documentId=document_id).execute
+    )
+
+    # Find the appropriate header or footer
+    headers = doc.get('headers', {})
+    footers = doc.get('footers', {})
+
+    target_section = None
+    section_id = None
+
+    if section_type == "header":
+        # Look for existing header of the specified type
+        for hid, header in headers.items():
+            target_section = header
+            section_id = hid
+            break  # Use first available header for now
+    else:
+        # Look for existing footer of the specified type
+        for fid, footer in footers.items():
+            target_section = footer
+            section_id = fid
+            break  # Use first available footer for now
+
+    if not target_section:
+        return f"Error: No {section_type} found in document. Please create a {section_type} first in Google Docs."
+
+    # Clear existing content and insert new content
+    content_elements = target_section.get('content', [])
+    if content_elements:
+        # Find the first paragraph to replace content
+        first_para = None
+        for element in content_elements:
+            if 'paragraph' in element:
+                first_para = element
+                break
+
+        if first_para:
+            # Calculate content range to replace
+            start_index = first_para.get('startIndex', 0)
+            end_index = first_para.get('endIndex', 0)
+
+            requests = []
+
+            # Delete existing content if any
+            if end_index > start_index:
+                requests.append({
+                    'deleteContentRange': {
+                        'range': {
+                            'startIndex': start_index,
+                            'endIndex': end_index - 1  # Keep the paragraph end
+                        }
+                    }
+                })
+
+            # Insert new content
+            requests.append({
+                'insertText': {
+                    'location': {'index': start_index},
+                    'text': content
+                }
+            })
+
+            await asyncio.to_thread(
+                service.documents().batchUpdate(
+                    documentId=document_id,
+                    body={'requests': requests}
+                ).execute
+            )
+
+            link = f"https://docs.google.com/document/d/{document_id}/edit"
+            return f"Updated {section_type} content in document {document_id}. Link: {link}"
+
+    return f"Error: Could not find content structure in {section_type} to update."
+
+@server.tool()
+@handle_http_errors("batch_update_doc", service_type="docs")
+@require_google_service("docs", "docs_write")
+async def batch_update_doc(
+    service,
+    user_google_email: str,
+    document_id: str,
+    operations: list,
+) -> str:
+    """
+    Executes multiple document operations in a single atomic batch update.
+
+    Args:
+        user_google_email: User's Google email address
+        document_id: ID of the document to update
+        operations: List of operation dictionaries. Each operation should contain:
+                   - type: Operation type ('insert_text', 'delete_text', 'replace_text', 'format_text', 'insert_table', 'insert_page_break')
+                   - Additional parameters specific to each operation type
+
+    Example operations:
+        [
+            {"type": "insert_text", "index": 1, "text": "Hello World"},
+            {"type": "format_text", "start_index": 1, "end_index": 12, "bold": true},
+            {"type": "insert_table", "index": 20, "rows": 2, "columns": 3}
+        ]
+
+    Returns:
+        str: Confirmation message with batch operation results
+    """
+    logger.info(f"[batch_update_doc] Doc={document_id}, operations={len(operations)}")
+
+    if not operations:
+        return "Error: No operations provided. Please provide at least one operation."
+
+    requests = []
+    operation_descriptions = []
+
+    for i, op in enumerate(operations):
+        # Validate operation first
+        is_valid, error_msg = validate_operation(op)
+        if not is_valid:
+            return f"Error: Operation {i+1}: {error_msg}"
+
+        op_type = op.get('type')
+
+        try:
+            if op_type == 'insert_text':
+                requests.append(create_insert_text_request(op['index'], op['text']))
+                operation_descriptions.append(f"insert text at {op['index']}")
+
+            elif op_type == 'delete_text':
+                requests.append(create_delete_range_request(op['start_index'], op['end_index']))
+                operation_descriptions.append(f"delete text {op['start_index']}-{op['end_index']}")
+
+            elif op_type == 'replace_text':
+                requests.extend([
+                    create_delete_range_request(op['start_index'], op['end_index']),
+                    create_insert_text_request(op['start_index'], op['text'])
+                ])
+                operation_descriptions.append(f"replace text {op['start_index']}-{op['end_index']}")
+
+            elif op_type == 'format_text':
+                format_request = create_format_text_request(
+                    op['start_index'], op['end_index'],
+                    op.get('bold'), op.get('italic'), op.get('underline'),
+                    op.get('font_size'), op.get('font_family')
+                )
+                if format_request:
+                    requests.append(format_request)
+                    # Build format description
+                    format_changes = []
+                    if op.get('bold') is not None:
+                        format_changes.append(f"bold: {op['bold']}")
+                    if op.get('italic') is not None:
+                        format_changes.append(f"italic: {op['italic']}")
+                    if op.get('underline') is not None:
+                        format_changes.append(f"underline: {op['underline']}")
+                    if op.get('font_size') is not None:
+                        format_changes.append(f"font size: {op['font_size']}pt")
+                    if op.get('font_family') is not None:
+                        format_changes.append(f"font family: {op['font_family']}")
+                    operation_descriptions.append(f"format text {op['start_index']}-{op['end_index']} ({', '.join(format_changes)})")
+
+            elif op_type == 'insert_table':
+                requests.append(create_insert_table_request(op['index'], op['rows'], op['columns']))
+                operation_descriptions.append(f"insert {op['rows']}x{op['columns']} table at {op['index']}")
+
+            elif op_type == 'insert_page_break':
+                requests.append(create_insert_page_break_request(op['index']))
+                operation_descriptions.append(f"insert page break at {op['index']}")
+
+            elif op_type == 'find_replace':
+                requests.append(create_find_replace_request(
+                    op['find_text'], op['replace_text'], op.get('match_case', False)
+                ))
+                operation_descriptions.append(f"find/replace '{op['find_text']}' → '{op['replace_text']}'")
+
+            else:
+                return f"Error: Unsupported operation type '{op_type}' in operation {i+1}. Supported types: insert_text, delete_text, replace_text, format_text, insert_table, insert_page_break, find_replace."
+
+        except KeyError as e:
+            return f"Error: Operation {i+1} ({op_type}) missing required field: {e}"
+        except Exception as e:
+            return f"Error: Operation {i+1} ({op_type}) failed validation: {str(e)}"
+
+    # Execute all operations in a single batch
+    result = await asyncio.to_thread(
+        service.documents().batchUpdate(
+            documentId=document_id,
+            body={'requests': requests}
+        ).execute
+    )
+
+    # Extract results information
+    replies_count = len(result.get('replies', []))
+
+    operations_summary = ', '.join(operation_descriptions[:3])  # Show first 3 operations
+    if len(operation_descriptions) > 3:
+        operations_summary += f" and {len(operation_descriptions) - 3} more"
+
+    link = f"https://docs.google.com/document/d/{document_id}/edit"
+    return f"Successfully executed {len(operations)} operations ({operations_summary}) on document {document_id}. API replies: {replies_count}. Link: {link}"
+
+@server.tool()
+@handle_http_errors("inspect_doc_structure", is_read_only=True, service_type="docs")
+@require_google_service("docs", "docs_read")
+async def inspect_doc_structure(
+    service,
+    user_google_email: str,
+    document_id: str,
+    detailed: bool = False,
+) -> str:
+    """
+    Essential tool for finding safe insertion points and understanding document structure.
+
+    USE THIS FOR:
+    - Finding the correct index for table insertion
+    - Understanding document layout before making changes
+    - Locating existing tables and their positions
+    - Getting document statistics and complexity info
+
+    CRITICAL FOR TABLE OPERATIONS:
+    ALWAYS call this BEFORE creating tables to get a safe insertion index.
+    Look for "total_length" in the output - use values less than this for insertion.
+
+    WHAT THE OUTPUT SHOWS:
+    - total_elements: Number of document elements
+    - total_length: Maximum safe index for insertion
+    - tables: Number of existing tables
+    - table_details: Position and dimensions of each table
+
+    WORKFLOW:
+    Step 1: Call this function
+    Step 2: Note the "total_length" value
+    Step 3: Use an index < total_length for table insertion
+    Step 4: Create your table
+
+    Args:
+        user_google_email: User's Google email address
+        document_id: ID of the document to inspect
+        detailed: Whether to return detailed structure information
+
+    Returns:
+        str: JSON string containing document structure and safe insertion indices
+    """
+    logger.info(f"[inspect_doc_structure] Doc={document_id}, detailed={detailed}")
+
+    # Get the document
+    doc = await asyncio.to_thread(
+        service.documents().get(documentId=document_id).execute
+    )
+
+    if detailed:
+        # Return full parsed structure
+        structure = parse_document_structure(doc)
+
+        # Simplify for JSON serialization
+        result = {
+            'title': structure['title'],
+            'total_length': structure['total_length'],
+            'statistics': {
+                'elements': len(structure['body']),
+                'tables': len(structure['tables']),
+                'paragraphs': sum(1 for e in structure['body'] if e.get('type') == 'paragraph'),
+                'has_headers': bool(structure['headers']),
+                'has_footers': bool(structure['footers'])
+            },
+            'elements': []
+        }
+
+        # Add element summaries
+        for element in structure['body']:
+            elem_summary = {
+                'type': element['type'],
+                'start_index': element['start_index'],
+                'end_index': element['end_index']
+            }
+
+            if element['type'] == 'table':
+                elem_summary['rows'] = element['rows']
+                elem_summary['columns'] = element['columns']
+                elem_summary['cell_count'] = len(element.get('cells', []))
+            elif element['type'] == 'paragraph':
+                elem_summary['text_preview'] = element.get('text', '')[:100]
+
+            result['elements'].append(elem_summary)
+
+        # Add table details
+        if structure['tables']:
+            result['tables'] = []
+            for i, table in enumerate(structure['tables']):
+                table_data = extract_table_as_data(table)
+                result['tables'].append({
+                    'index': i,
+                    'position': {'start': table['start_index'], 'end': table['end_index']},
+                    'dimensions': {'rows': table['rows'], 'columns': table['columns']},
+                    'preview': table_data[:3] if table_data else []  # First 3 rows
+                })
+
+    else:
+        # Return basic analysis
+        result = analyze_document_complexity(doc)
+
+        # Add table information
+        tables = find_tables(doc)
+        if tables:
+            result['table_details'] = []
+            for i, table in enumerate(tables):
+                result['table_details'].append({
+                    'index': i,
+                    'rows': table['rows'],
+                    'columns': table['columns'],
+                    'start_index': table['start_index'],
+                    'end_index': table['end_index']
+                })
+
+    import json
+    link = f"https://docs.google.com/document/d/{document_id}/edit"
+    return f"Document structure analysis for {document_id}:\n\n{json.dumps(result, indent=2)}\n\nLink: {link}"
+
+@server.tool()
+@handle_http_errors("create_table_with_data", service_type="docs")
+@require_google_service("docs", "docs_write")
+async def create_table_with_data(
+    service,
+    user_google_email: str,
+    document_id: str,
+    table_data: list,
+    index: int,
+    bold_headers: bool = True,
+) -> str:
+    """
+    Creates a table and populates it with data in one reliable operation.
+
+    CRITICAL: YOU MUST CALL inspect_doc_structure FIRST TO GET THE INDEX!
+
+    MANDATORY WORKFLOW - DO THESE STEPS IN ORDER:
+
+    Step 1: ALWAYS call inspect_doc_structure first
+    Step 2: Use the 'total_length' value from inspect_doc_structure as your index
+    Step 3: Format data as 2D list: [["col1", "col2"], ["row1col1", "row1col2"]]
+    Step 4: Call this function with the correct index and data
+
+    EXAMPLE DATA FORMAT:
+    table_data = [
+        ["Header1", "Header2", "Header3"],    # Row 0 - headers
+        ["Data1", "Data2", "Data3"],          # Row 1 - first data row
+        ["Data4", "Data5", "Data6"]           # Row 2 - second data row
+    ]
+
+    CRITICAL INDEX REQUIREMENTS:
+    - NEVER use index values like 1, 2, 10 without calling inspect_doc_structure first
+    - ALWAYS get index from inspect_doc_structure 'total_length' field
+    - Index must be a valid insertion point in the document
+
+    DATA FORMAT REQUIREMENTS:
+    - Must be 2D list of strings only
+    - Each inner list = one table row
+    - All rows MUST have same number of columns
+    - Use empty strings "" for empty cells, never None
+    - Use debug_table_structure after creation to verify results
+
+    Args:
+        user_google_email: User's Google email address
+        document_id: ID of the document to update
+        table_data: 2D list of strings - EXACT format: [["col1", "col2"], ["row1col1", "row1col2"]]
+        index: Document position (MANDATORY: get from inspect_doc_structure 'total_length')
+        bold_headers: Whether to make first row bold (default: true)
+
+    Returns:
+        str: Confirmation with table details and link
+    """
+    logger.info(f"[create_table_with_data] Doc={document_id}, index={index}")
+    logger.info(f"Received table_data: {table_data}")
+    logger.info(f"Bold headers: {bold_headers}")
+
+    # Critical validation: Check if index is suspiciously low (common LLM mistake)
+    # NOTE: Removed strict validation since index=1 can be valid for simple documents
+    if index < 0:
+        return f"ERROR: Index {index} is negative. You MUST call inspect_doc_structure first to get the proper insertion index."
+
+    # Strict validation with helpful error messages
+    is_valid, error_msg = validate_table_data(table_data)
+    if not is_valid:
+        return f"ERROR: {error_msg}\n\nRequired format: [['col1', 'col2'], ['row2col1', 'row2col2']]"
+
+    # Additional debugging: Print the exact structure we received
+    logger.info(f"Table data structure validation:")
+    for i, row in enumerate(table_data):
+        logger.info(f"  Row {i}: {row} (type: {type(row)}, length: {len(row)})")
+        for j, cell in enumerate(row):
+            logger.info(f"    Cell ({i},{j}): '{cell}' (type: {type(cell)})")
+
+    rows = len(table_data)
+    cols = len(table_data[0])
+    logger.info(f"Table dimensions: {rows}x{cols}")
+
+    # Validate all rows have same column count
+    for i, row in enumerate(table_data):
+        if len(row) != cols:
+            return f"ERROR: Row {i} has {len(row)} columns, but first row has {cols} columns. All rows must have the same number of columns."
+        # Also validate each cell is a string
+        for j, cell in enumerate(row):
+            if not isinstance(cell, str):
+                return f"ERROR: Cell ({i},{j}) is {type(cell).__name__}, not string. All cells must be strings. Value: {repr(cell)}"
+
+    # Step 1: Create empty table
+    logger.info(f"Creating {rows}x{cols} table at index {index}")
+    logger.info(f"Table data being used: {table_data}")
+    create_result = await asyncio.to_thread(
+        service.documents().batchUpdate(
+            documentId=document_id,
+            body={'requests': [create_insert_table_request(index, rows, cols)]}
+        ).execute
+    )
+
+    # Step 2: Get fresh document structure to find actual cell positions
+    doc = await asyncio.to_thread(
+        service.documents().get(documentId=document_id).execute
+    )
+
+    # Find the table we just created
+    tables = find_tables(doc)
+    if not tables:
+        return f"ERROR: Could not find table after creation in document {document_id}"
+
+    # Use the last table (newly created one)
+    table_info = tables[-1]
+    cells = table_info.get('cells', [])
+    logger.info(f"Found table with {len(cells)} rows, cells structure: {[[f'({r},{c})' for c in range(len(row))] for r, row in enumerate(cells)]}")
+
+    # Step 3: Populate each cell individually, refreshing indices after each insertion
+    population_count = 0
+    logger.info(f"Starting cell population for {len(table_data)} rows, {len(table_data[0]) if table_data else 0} columns")
+
+    for row_idx, row_data in enumerate(table_data):
+        logger.info(f"Processing row {row_idx}: {row_data}")
+        for col_idx, cell_text in enumerate(row_data):
+            if cell_text:  # Only populate non-empty cells
+                logger.info(f"Processing cell ({row_idx},{col_idx}) with text '{cell_text}'")
+
+                # CRITICAL: Refresh document structure before each insertion
+                # This prevents index shifting issues
+                fresh_doc = await asyncio.to_thread(
+                    service.documents().get(documentId=document_id).execute
+                )
+                fresh_tables = find_tables(fresh_doc)
+                if not fresh_tables:
+                    return f"ERROR: Could not find table after refresh for cell ({row_idx},{col_idx})"
+
+                fresh_table = fresh_tables[-1]  # Use the last table (newly created one)
+                fresh_cells = fresh_table.get('cells', [])
+
+                # Bounds checking with fresh data
+                if row_idx >= len(fresh_cells) or col_idx >= len(fresh_cells[row_idx]):
+                    logger.error(f"Cell ({row_idx},{col_idx}) out of bounds after refresh")
+                    continue
+
+                cell = fresh_cells[row_idx][col_idx]
+                insertion_index = cell.get('insertion_index')
+                logger.info(f"Cell ({row_idx},{col_idx}) fresh insertion_index: {insertion_index}")
+
+                if insertion_index:
+                    try:
+                        # Insert text
+                        await asyncio.to_thread(
+                            service.documents().batchUpdate(
+                                documentId=document_id,
+                                body={'requests': [{
+                                    'insertText': {
+                                        'location': {'index': insertion_index},
+                                        'text': cell_text
+                                    }
+                                }]}
+                            ).execute
+                        )
+                        population_count += 1
+                        logger.info(f"Successfully inserted '{cell_text}' at index {insertion_index}")
+
+                        # Apply bold to first row if requested
+                        if bold_headers and row_idx == 0:
+                            # Need to get updated position after text insertion
+                            updated_end_index = insertion_index + len(cell_text)
+                            await asyncio.to_thread(
+                                service.documents().batchUpdate(
+                                    documentId=document_id,
+                                    body={'requests': [{
+                                        'updateTextStyle': {
+                                            'range': {
+                                                'startIndex': insertion_index,
+                                                'endIndex': updated_end_index
+                                            },
+                                            'textStyle': {'bold': True},
+                                            'fields': 'bold'
+                                        }
+                                    }]}
+                                ).execute
+                            )
+                            logger.info(f"Applied bold formatting to '{cell_text}' from {insertion_index} to {updated_end_index}")
+
+                    except Exception as e:
+                        logger.error(f"Failed to populate cell ({row_idx},{col_idx}): {str(e)}")
+                        return f"ERROR: Failed to populate cell ({row_idx},{col_idx}) with '{cell_text}': {str(e)}"
+                else:
+                    logger.warning(f"No insertion_index for cell ({row_idx},{col_idx})")
+
+    link = f"https://docs.google.com/document/d/{document_id}/edit"
+    logger.info(f"Completed table creation. Populated {population_count} cells out of expected {sum(1 for row in table_data for cell in row if cell)}")
+    return f"SUCCESS: Created {rows}x{cols} table and populated {population_count} cells at index {index}. Bold headers: {bold_headers}. Link: {link}"
+
+
+@server.tool()
+@handle_http_errors("debug_table_structure", is_read_only=True, service_type="docs")
+@require_google_service("docs", "docs_read")
+async def debug_table_structure(
+    service,
+    user_google_email: str,
+    document_id: str,
+    table_index: int = 0,
+) -> str:
+    """
+    ESSENTIAL DEBUGGING TOOL - Use this whenever tables don't work as expected.
+
+    USE THIS IMMEDIATELY WHEN:
+    - Table population put data in wrong cells
+    - You get "table not found" errors
+    - Data appears concatenated in first cell
+    - Need to understand existing table structure
+    - Planning to use populate_existing_table
+
+    WHAT THIS SHOWS YOU:
+    - Exact table dimensions (rows × columns)
+    - Each cell's position coordinates (row,col)
+    - Current content in each cell
+    - Insertion indices for each cell
+    - Table boundaries and ranges
+
+    HOW TO READ THE OUTPUT:
+    - "dimensions": "2x3" = 2 rows, 3 columns
+    - "position": "(0,0)" = first row, first column
+    - "current_content": What's actually in each cell right now
+    - "insertion_index": Where new text would be inserted in that cell
+
+    WORKFLOW INTEGRATION:
+    1. After creating table → Use this to verify structure
+    2. Before populating → Use this to plan your data format
+    3. After population fails → Use this to see what went wrong
+    4. When debugging → Compare your data array to actual table structure
+
+    Args:
+        user_google_email: User's Google email address
+        document_id: ID of the document to inspect
+        table_index: Which table to debug (0 = first table, 1 = second table, etc.)
+
+    Returns:
+        str: Detailed JSON structure showing table layout, cell positions, and current content
+    """
+    logger.info(f"[debug_table_structure] Doc={document_id}, table_index={table_index}")
+
+    # Get the document
+    doc = await asyncio.to_thread(
+        service.documents().get(documentId=document_id).execute
+    )
+
+    # Find tables
+    tables = find_tables(doc)
+    if table_index >= len(tables):
+        return f"Error: Table index {table_index} not found. Document has {len(tables)} table(s)."
+
+    table_info = tables[table_index]
+
+    import json
+
+    # Extract detailed cell information
+    debug_info = {
+        'table_index': table_index,
+        'dimensions': f"{table_info['rows']}x{table_info['columns']}",
+        'table_range': f"[{table_info['start_index']}-{table_info['end_index']}]",
+        'cells': []
+    }
+
+    for row_idx, row in enumerate(table_info['cells']):
+        row_info = []
+        for col_idx, cell in enumerate(row):
+            cell_debug = {
+                'position': f"({row_idx},{col_idx})",
+                'range': f"[{cell['start_index']}-{cell['end_index']}]",
+                'insertion_index': cell.get('insertion_index', 'N/A'),
+                'current_content': repr(cell.get('content', '')),
+                'content_elements_count': len(cell.get('content_elements', []))
+            }
+            row_info.append(cell_debug)
+        debug_info['cells'].append(row_info)
+
+    link = f"https://docs.google.com/document/d/{document_id}/edit"
+    return f"Table structure debug for table {table_index}:\n\n{json.dumps(debug_info, indent=2)}\n\nLink: {link}"
+
+
 # Create comment management tools for documents
 _comment_tools = create_comment_tools("document", "document_id")
 
