Files
google-mcp/core/comments.py

306 lines
11 KiB
Python
Raw Permalink Normal View History

2025-07-01 17:14:30 -07:00
"""
Core Comments Module
This module provides reusable comment management functions for Google Workspace applications.
All Google Workspace apps (Docs, Sheets, Slides) use the Drive API for comment operations.
"""
import logging
import asyncio
from typing import Optional
2025-07-01 17:14:30 -07:00
from auth.service_decorator import require_google_service
from core.server import server
from core.utils import handle_http_errors
logger = logging.getLogger(__name__)
async def _manage_comment_dispatch(
2026-03-01 17:54:47 +00:00
service,
app_name: str,
file_id: str,
action: str,
comment_content: Optional[str] = None,
comment_id: Optional[str] = None,
) -> str:
"""Route comment management actions to the appropriate implementation."""
action_lower = action.lower().strip()
if action_lower == "create":
if not comment_content:
raise ValueError("comment_content is required for create action")
return await _create_comment_impl(service, app_name, file_id, comment_content)
elif action_lower == "reply":
if not comment_id or not comment_content:
2026-03-01 17:54:47 +00:00
raise ValueError(
"comment_id and comment_content are required for reply action"
)
return await _reply_to_comment_impl(
service, app_name, file_id, comment_id, comment_content
)
elif action_lower == "resolve":
if not comment_id:
raise ValueError("comment_id is required for resolve action")
return await _resolve_comment_impl(service, app_name, file_id, comment_id)
else:
2026-03-01 17:54:47 +00:00
raise ValueError(
f"Invalid action '{action_lower}'. Must be 'create', 'reply', or 'resolve'."
)
2025-07-01 17:14:30 -07:00
def create_comment_tools(app_name: str, file_id_param: str):
"""
Factory function to create comment management tools for a specific Google Workspace app.
2025-07-01 18:56:53 -07:00
2025-07-01 17:14:30 -07:00
Args:
2025-07-01 18:56:53 -07:00
app_name: Name of the app (e.g., "document", "spreadsheet", "presentation")
2025-07-01 17:14:30 -07:00
file_id_param: Parameter name for the file ID (e.g., "document_id", "spreadsheet_id", "presentation_id")
2025-07-01 18:56:53 -07:00
2025-07-01 17:14:30 -07:00
Returns:
Dict containing the comment management functions with unique names
2025-07-01 17:14:30 -07:00
"""
2025-07-01 18:56:53 -07:00
# --- Consolidated tools ---
list_func_name = f"list_{app_name}_comments"
manage_func_name = f"manage_{app_name}_comment"
2025-07-01 18:56:53 -07:00
if file_id_param == "document_id":
2025-12-13 13:49:28 -08:00
2025-07-01 18:56:53 -07:00
@require_google_service("drive", "drive_read")
@handle_http_errors(list_func_name, service_type="drive")
async def list_comments(
2025-12-13 13:49:28 -08:00
service, user_google_email: str, document_id: str
) -> str:
"""List all comments from a Google Document."""
2025-07-01 18:56:53 -07:00
return await _read_comments_impl(service, app_name, document_id)
@require_google_service("drive", "drive_file")
@handle_http_errors(manage_func_name, service_type="drive")
async def manage_comment(
2026-03-01 17:54:47 +00:00
service,
user_google_email: str,
document_id: str,
action: str,
comment_content: Optional[str] = None,
comment_id: Optional[str] = None,
2025-12-13 13:49:28 -08:00
) -> str:
"""Manage comments on a Google Document.
Actions:
- create: Create a new comment. Requires comment_content.
- reply: Reply to a comment. Requires comment_id and comment_content.
- resolve: Resolve a comment. Requires comment_id.
"""
return await _manage_comment_dispatch(
service, app_name, document_id, action, comment_content, comment_id
2025-12-13 13:49:28 -08:00
)
2025-07-01 18:56:53 -07:00
elif file_id_param == "spreadsheet_id":
2025-12-13 13:49:28 -08:00
2025-07-01 18:56:53 -07:00
@require_google_service("drive", "drive_read")
@handle_http_errors(list_func_name, service_type="drive")
async def list_comments(
2025-12-13 13:49:28 -08:00
service, user_google_email: str, spreadsheet_id: str
) -> str:
"""List all comments from a Google Spreadsheet."""
2025-07-01 18:56:53 -07:00
return await _read_comments_impl(service, app_name, spreadsheet_id)
@require_google_service("drive", "drive_file")
@handle_http_errors(manage_func_name, service_type="drive")
async def manage_comment(
2026-03-01 17:54:47 +00:00
service,
user_google_email: str,
spreadsheet_id: str,
action: str,
comment_content: Optional[str] = None,
comment_id: Optional[str] = None,
2025-12-13 13:49:28 -08:00
) -> str:
"""Manage comments on a Google Spreadsheet.
Actions:
- create: Create a new comment. Requires comment_content.
- reply: Reply to a comment. Requires comment_id and comment_content.
- resolve: Resolve a comment. Requires comment_id.
"""
return await _manage_comment_dispatch(
service, app_name, spreadsheet_id, action, comment_content, comment_id
2025-12-13 13:49:28 -08:00
)
2025-07-01 18:56:53 -07:00
elif file_id_param == "presentation_id":
2025-12-13 13:49:28 -08:00
2025-07-01 18:56:53 -07:00
@require_google_service("drive", "drive_read")
@handle_http_errors(list_func_name, service_type="drive")
async def list_comments(
2025-12-13 13:49:28 -08:00
service, user_google_email: str, presentation_id: str
) -> str:
"""List all comments from a Google Presentation."""
2025-07-01 18:56:53 -07:00
return await _read_comments_impl(service, app_name, presentation_id)
@require_google_service("drive", "drive_file")
@handle_http_errors(manage_func_name, service_type="drive")
async def manage_comment(
2026-03-01 17:54:47 +00:00
service,
user_google_email: str,
presentation_id: str,
action: str,
comment_content: Optional[str] = None,
comment_id: Optional[str] = None,
2025-12-13 13:49:28 -08:00
) -> str:
"""Manage comments on a Google Presentation.
Actions:
- create: Create a new comment. Requires comment_content.
- reply: Reply to a comment. Requires comment_id and comment_content.
- resolve: Resolve a comment. Requires comment_id.
"""
return await _manage_comment_dispatch(
service, app_name, presentation_id, action, comment_content, comment_id
2025-12-13 13:49:28 -08:00
)
2025-07-01 18:56:53 -07:00
list_comments.__name__ = list_func_name
manage_comment.__name__ = manage_func_name
server.tool()(list_comments)
server.tool()(manage_comment)
2025-07-05 14:58:01 -04:00
2025-07-01 18:56:53 -07:00
return {
"list_comments": list_comments,
"manage_comment": manage_comment,
2025-07-01 18:56:53 -07:00
}
async def _read_comments_impl(service, app_name: str, file_id: str) -> str:
"""Implementation for reading comments from any Google Workspace file."""
logger.info(f"[read_{app_name}_comments] Reading comments for {app_name} {file_id}")
response = await asyncio.to_thread(
2025-12-13 13:49:28 -08:00
service.comments()
.list(
2025-07-01 18:56:53 -07:00
fileId=file_id,
fields="comments(id,content,author,createdTime,modifiedTime,resolved,quotedFileContent,replies(content,author,id,createdTime,modifiedTime))",
2025-12-13 13:49:28 -08:00
)
.execute
2025-07-01 18:56:53 -07:00
)
2025-12-13 13:49:28 -08:00
comments = response.get("comments", [])
2025-07-01 18:56:53 -07:00
if not comments:
return f"No comments found in {app_name} {file_id}"
output = [f"Found {len(comments)} comments in {app_name} {file_id}:\\n"]
for comment in comments:
2025-12-13 13:49:28 -08:00
author = comment.get("author", {}).get("displayName", "Unknown")
content = comment.get("content", "")
created = comment.get("createdTime", "")
resolved = comment.get("resolved", False)
comment_id = comment.get("id", "")
2025-07-01 18:56:53 -07:00
status = " [RESOLVED]" if resolved else ""
2025-07-01 17:32:34 -07:00
quoted_text = comment.get("quotedFileContent", {}).get("value", "")
2025-07-01 18:56:53 -07:00
output.append(f"Comment ID: {comment_id}")
output.append(f"Author: {author}")
output.append(f"Created: {created}{status}")
if quoted_text:
output.append(f"Quoted text: {quoted_text}")
2025-07-01 18:56:53 -07:00
output.append(f"Content: {content}")
# Add replies if any
2025-12-13 13:49:28 -08:00
replies = comment.get("replies", [])
2025-07-01 18:56:53 -07:00
if replies:
output.append(f" Replies ({len(replies)}):")
for reply in replies:
2025-12-13 13:49:28 -08:00
reply_author = reply.get("author", {}).get("displayName", "Unknown")
reply_content = reply.get("content", "")
reply_created = reply.get("createdTime", "")
reply_id = reply.get("id", "")
2025-07-01 18:56:53 -07:00
output.append(f" Reply ID: {reply_id}")
output.append(f" Author: {reply_author}")
output.append(f" Created: {reply_created}")
output.append(f" Content: {reply_content}")
output.append("") # Empty line between comments
return "\\n".join(output)
2025-12-13 13:49:28 -08:00
async def _create_comment_impl(
service, app_name: str, file_id: str, comment_content: str
) -> str:
2025-07-01 18:56:53 -07:00
"""Implementation for creating a comment on any Google Workspace file."""
logger.info(f"[create_{app_name}_comment] Creating comment in {app_name} {file_id}")
body = {"content": comment_content}
comment = await asyncio.to_thread(
2025-12-13 13:49:28 -08:00
service.comments()
.create(
2025-07-01 18:56:53 -07:00
fileId=file_id,
body=body,
2025-12-13 13:49:28 -08:00
fields="id,content,author,createdTime,modifiedTime",
)
.execute
2025-07-01 18:56:53 -07:00
)
2025-12-13 13:49:28 -08:00
comment_id = comment.get("id", "")
author = comment.get("author", {}).get("displayName", "Unknown")
created = comment.get("createdTime", "")
2025-07-01 18:56:53 -07:00
return f"Comment created successfully!\\nComment ID: {comment_id}\\nAuthor: {author}\\nCreated: {created}\\nContent: {comment_content}"
2025-12-13 13:49:28 -08:00
async def _reply_to_comment_impl(
service, app_name: str, file_id: str, comment_id: str, reply_content: str
) -> str:
2025-07-01 18:56:53 -07:00
"""Implementation for replying to a comment on any Google Workspace file."""
2025-12-13 13:49:28 -08:00
logger.info(
f"[reply_to_{app_name}_comment] Replying to comment {comment_id} in {app_name} {file_id}"
)
2025-07-01 18:56:53 -07:00
2025-12-13 13:49:28 -08:00
body = {"content": reply_content}
2025-07-01 18:56:53 -07:00
reply = await asyncio.to_thread(
2025-12-13 13:49:28 -08:00
service.replies()
.create(
2025-07-01 18:56:53 -07:00
fileId=file_id,
commentId=comment_id,
body=body,
2025-12-13 13:49:28 -08:00
fields="id,content,author,createdTime,modifiedTime",
)
.execute
2025-07-01 18:56:53 -07:00
)
2025-12-13 13:49:28 -08:00
reply_id = reply.get("id", "")
author = reply.get("author", {}).get("displayName", "Unknown")
created = reply.get("createdTime", "")
2025-07-01 18:56:53 -07:00
return f"Reply posted successfully!\\nReply ID: {reply_id}\\nAuthor: {author}\\nCreated: {created}\\nContent: {reply_content}"
2025-12-13 13:49:28 -08:00
async def _resolve_comment_impl(
service, app_name: str, file_id: str, comment_id: str
) -> str:
2025-07-01 18:56:53 -07:00
"""Implementation for resolving a comment on any Google Workspace file."""
2025-12-13 13:49:28 -08:00
logger.info(
f"[resolve_{app_name}_comment] Resolving comment {comment_id} in {app_name} {file_id}"
)
2025-07-01 18:56:53 -07:00
2025-12-13 13:49:28 -08:00
body = {"content": "This comment has been resolved.", "action": "resolve"}
2025-07-01 18:56:53 -07:00
2025-07-03 19:37:45 -04:00
reply = await asyncio.to_thread(
2025-12-13 13:49:28 -08:00
service.replies()
.create(
2025-07-01 18:56:53 -07:00
fileId=file_id,
commentId=comment_id,
2025-07-03 19:37:45 -04:00
body=body,
2025-12-13 13:49:28 -08:00
fields="id,content,author,createdTime,modifiedTime",
)
.execute
2025-07-01 18:56:53 -07:00
)
2025-12-13 13:49:28 -08:00
reply_id = reply.get("id", "")
author = reply.get("author", {}).get("displayName", "Unknown")
created = reply.get("createdTime", "")
2025-07-03 19:37:45 -04:00
2025-12-13 13:49:28 -08:00
return f"Comment {comment_id} has been resolved successfully.\\nResolve reply ID: {reply_id}\\nAuthor: {author}\\nCreated: {created}"