Skip to main content
Glama
comments.py11.8 kB
""" Comment operations for Google Docs MCP Server. Handles listing, creating, and managing comments on documents. """ import json from fastmcp.exceptions import ToolError from google_docs_mcp.auth import get_docs_client, get_drive_client from google_docs_mcp.utils import log def list_comments(document_id: str) -> str: """ List all comments in a Google Document. Args: document_id: The ID of the Google Document Returns: Formatted string with comment information Raises: UserError: For permission/not found errors """ log(f"Listing comments for document {document_id}") try: # Use Drive API v3 for comments drive = get_drive_client() response = ( drive.comments() .list( fileId=document_id, fields="comments(id,content,quotedFileContent,author,createdTime,resolved)", pageSize=100, ) .execute() ) comments = response.get("comments", []) if not comments: return "No comments found in this document." # Format comments for display result_parts = [] for index, comment in enumerate(comments): author = comment.get("author", {}).get("displayName", "Unknown") created = comment.get("createdTime", "Unknown date") if created != "Unknown date": # Simplify date format created = created[:10] # Just get YYYY-MM-DD status = " [RESOLVED]" if comment.get("resolved") else "" # Get quoted text quoted_content = comment.get("quotedFileContent", {}) quoted_text = quoted_content.get("value", "") anchor = "" if quoted_text: truncated = ( quoted_text[:100] + "..." if len(quoted_text) > 100 else quoted_text ) anchor = f' (anchored to: "{truncated}")' content = comment.get("content", "") comment_id = comment.get("id", "") result_parts.append( f"\n{index + 1}. **{author}** ({created}){status}{anchor}\n" f" {content}\n" f" Comment ID: {comment_id}" ) return f"Found {len(comments)} comment{'s' if len(comments) != 1 else ''}:\n{''.join(result_parts)}" except Exception as e: error_message = str(e) log(f"Error listing comments: {error_message}") raise ToolError(f"Failed to list comments: {error_message}") def get_comment(document_id: str, comment_id: str) -> str: """ Get a specific comment with its full thread of replies. Args: document_id: The ID of the Google Document comment_id: The ID of the comment to retrieve Returns: Formatted string with comment and replies Raises: UserError: For permission/not found errors """ log(f"Getting comment {comment_id} from document {document_id}") try: drive = get_drive_client() response = ( drive.comments() .get( fileId=document_id, commentId=comment_id, fields="id,content,quotedFileContent,author,createdTime,resolved,replies(id,content,author,createdTime)", ) .execute() ) author = response.get("author", {}).get("displayName", "Unknown") created = response.get("createdTime", "Unknown date") if created != "Unknown date": created = created[:10] status = " [RESOLVED]" if response.get("resolved") else "" quoted_text = response.get("quotedFileContent", {}).get("value", "") anchor = f'\nAnchored to: "{quoted_text}"' if quoted_text else "" content = response.get("content", "") result = f"**{author}** ({created}){status}{anchor}\n{content}" # Add replies replies = response.get("replies", []) if replies: result += "\n\n**Replies:**" for index, reply in enumerate(replies): reply_author = reply.get("author", {}).get("displayName", "Unknown") reply_date = reply.get("createdTime", "Unknown date") if reply_date != "Unknown date": reply_date = reply_date[:10] reply_content = reply.get("content", "") result += f"\n{index + 1}. **{reply_author}** ({reply_date})\n {reply_content}" return result except Exception as e: error_message = str(e) log(f"Error getting comment: {error_message}") raise ToolError(f"Failed to get comment: {error_message}") def add_comment( document_id: str, start_index: int, end_index: int, comment_text: str ) -> str: """ Add a comment anchored to a specific text range. NOTE: Due to Google API limitations, comments created programmatically appear in the "All Comments" list but may not be visibly anchored to text in the document UI. Args: document_id: The ID of the Google Document start_index: Starting index of text range (inclusive, 1-based) end_index: Ending index of text range (exclusive) comment_text: Content of the comment Returns: Success message with comment ID Raises: UserError: For permission/not found errors """ log(f"Adding comment to range {start_index}-{end_index} in doc {document_id}") if end_index <= start_index: raise ToolError("End index must be greater than start index.") try: # First get the quoted text from the document docs = get_docs_client() doc = docs.documents().get(documentId=document_id).execute() # Extract quoted text quoted_text = "" content = doc.get("body", {}).get("content", []) for element in content: paragraph = element.get("paragraph", {}) for pe in paragraph.get("elements", []): text_run = pe.get("textRun", {}) if text_run: element_start = pe.get("startIndex", 0) element_end = pe.get("endIndex", 0) text = text_run.get("content", "") # Check if this element overlaps with our range if element_end > start_index and element_start < end_index: start_offset = max(0, start_index - element_start) end_offset = min(len(text), end_index - element_start) quoted_text += text[start_offset:end_offset] # Use Drive API v3 for comments drive = get_drive_client() response = ( drive.comments() .create( fileId=document_id, fields="id,content,quotedFileContent,author,createdTime,resolved", body={ "content": comment_text, "quotedFileContent": { "value": quoted_text, "mimeType": "text/html", }, "anchor": json.dumps( { "r": document_id, "a": [ { "txt": { "o": start_index - 1, # 0-based "l": end_index - start_index, "ml": end_index - start_index, } } ], } ), }, ) .execute() ) return f"Comment added successfully. Comment ID: {response.get('id')}" except ToolError: raise except Exception as e: error_message = str(e) log(f"Error adding comment: {error_message}") raise ToolError(f"Failed to add comment: {error_message}") def reply_to_comment(document_id: str, comment_id: str, reply_text: str) -> str: """ Add a reply to an existing comment. Args: document_id: The ID of the Google Document comment_id: The ID of the comment to reply to reply_text: Content of the reply Returns: Success message with reply ID Raises: UserError: For permission/not found errors """ log(f"Adding reply to comment {comment_id} in doc {document_id}") try: drive = get_drive_client() response = ( drive.replies() .create( fileId=document_id, commentId=comment_id, fields="id,content,author,createdTime", body={"content": reply_text}, ) .execute() ) return f"Reply added successfully. Reply ID: {response.get('id')}" except Exception as e: error_message = str(e) log(f"Error adding reply: {error_message}") raise ToolError(f"Failed to add reply: {error_message}") def resolve_comment(document_id: str, comment_id: str) -> str: """ Mark a comment as resolved. NOTE: Due to Google API limitations, the resolved status may not persist in the Google Docs UI for all document types. Args: document_id: The ID of the Google Document comment_id: The ID of the comment to resolve Returns: Success message Raises: UserError: For permission/not found errors """ log(f"Resolving comment {comment_id} in doc {document_id}") try: drive = get_drive_client() # Get current comment content (required by API) current = ( drive.comments() .get(fileId=document_id, commentId=comment_id, fields="content") .execute() ) # Update with resolved status drive.comments().update( fileId=document_id, commentId=comment_id, fields="id,resolved", body={"content": current.get("content"), "resolved": True}, ).execute() # Verify verify = ( drive.comments() .get(fileId=document_id, commentId=comment_id, fields="resolved") .execute() ) if verify.get("resolved"): return f"Comment {comment_id} has been marked as resolved." else: return ( f"Attempted to resolve comment {comment_id}, but the resolved status " f"may not persist in the Google Docs UI due to API limitations. " f"The comment can be resolved manually in the Google Docs interface." ) except Exception as e: error_message = str(e) log(f"Error resolving comment: {error_message}") raise ToolError(f"Failed to resolve comment: {error_message}") def delete_comment(document_id: str, comment_id: str) -> str: """ Delete a comment from a document. Args: document_id: The ID of the Google Document comment_id: The ID of the comment to delete Returns: Success message Raises: UserError: For permission/not found errors """ log(f"Deleting comment {comment_id} from doc {document_id}") try: drive = get_drive_client() drive.comments().delete(fileId=document_id, commentId=comment_id).execute() return f"Comment {comment_id} has been deleted." except Exception as e: error_message = str(e) log(f"Error deleting comment: {error_message}") raise ToolError(f"Failed to delete comment: {error_message}")

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nickweedon/google-docs-mcp-docker'

If you have feedback or need assistance with the MCP directory API, please join our Discord server