merge
This commit is contained in:
257
core/comments.py
Normal file
257
core/comments.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
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 Dict, Any
|
||||
|
||||
from mcp import types
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
from auth.service_decorator import require_google_service
|
||||
from core.server import server
|
||||
from core.utils import handle_http_errors
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_comment_tools(app_name: str, file_id_param: str):
|
||||
"""
|
||||
Factory function to create comment management tools for a specific Google Workspace app.
|
||||
|
||||
Args:
|
||||
app_name: Name of the app (e.g., "document", "spreadsheet", "presentation")
|
||||
file_id_param: Parameter name for the file ID (e.g., "document_id", "spreadsheet_id", "presentation_id")
|
||||
|
||||
Returns:
|
||||
Dict containing the four comment management functions with unique names
|
||||
"""
|
||||
|
||||
# Create unique function names based on the app type
|
||||
read_func_name = f"read_{app_name}_comments"
|
||||
create_func_name = f"create_{app_name}_comment"
|
||||
reply_func_name = f"reply_to_{app_name}_comment"
|
||||
resolve_func_name = f"resolve_{app_name}_comment"
|
||||
|
||||
# Create read comments function
|
||||
if file_id_param == "document_id":
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_read")
|
||||
@handle_http_errors(read_func_name)
|
||||
async def read_comments(service, user_google_email: str, document_id: str) -> str:
|
||||
"""Read all comments from a Google Slide, Sheet or Doc."""
|
||||
return await _read_comments_impl(service, app_name, document_id)
|
||||
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_file")
|
||||
@handle_http_errors(create_func_name)
|
||||
async def create_comment(service, user_google_email: str, document_id: str, comment_content: str) -> str:
|
||||
"""Create a new comment on a Google Slide, Sheet or Doc."""
|
||||
return await _create_comment_impl(service, app_name, document_id, comment_content)
|
||||
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_file")
|
||||
@handle_http_errors(reply_func_name)
|
||||
async def reply_to_comment(service, user_google_email: str, document_id: str, comment_id: str, reply_content: str) -> str:
|
||||
"""Reply to a specific comment in a Google Document."""
|
||||
return await _reply_to_comment_impl(service, app_name, document_id, comment_id, reply_content)
|
||||
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_file")
|
||||
@handle_http_errors(resolve_func_name)
|
||||
async def resolve_comment(service, user_google_email: str, document_id: str, comment_id: str) -> str:
|
||||
"""Resolve a comment in a Google Slide, Sheet or Doc."""
|
||||
return await _resolve_comment_impl(service, app_name, document_id, comment_id)
|
||||
|
||||
elif file_id_param == "spreadsheet_id":
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_read")
|
||||
@handle_http_errors(read_func_name)
|
||||
async def read_comments(service, user_google_email: str, spreadsheet_id: str) -> str:
|
||||
"""Read all comments from a Google Slide, Sheet or Doc."""
|
||||
return await _read_comments_impl(service, app_name, spreadsheet_id)
|
||||
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_file")
|
||||
@handle_http_errors(create_func_name)
|
||||
async def create_comment(service, user_google_email: str, spreadsheet_id: str, comment_content: str) -> str:
|
||||
"""Create a new comment on a Google Slide, Sheet or Doc."""
|
||||
return await _create_comment_impl(service, app_name, spreadsheet_id, comment_content)
|
||||
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_file")
|
||||
@handle_http_errors(reply_func_name)
|
||||
async def reply_to_comment(service, user_google_email: str, spreadsheet_id: str, comment_id: str, reply_content: str) -> str:
|
||||
"""Reply to a specific comment in a Google Slide, Sheet or Doc."""
|
||||
return await _reply_to_comment_impl(service, app_name, spreadsheet_id, comment_id, reply_content)
|
||||
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_file")
|
||||
@handle_http_errors(resolve_func_name)
|
||||
async def resolve_comment(service, user_google_email: str, spreadsheet_id: str, comment_id: str) -> str:
|
||||
"""Resolve a comment in a Google Slide, Sheet or Doc."""
|
||||
return await _resolve_comment_impl(service, app_name, spreadsheet_id, comment_id)
|
||||
|
||||
elif file_id_param == "presentation_id":
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_read")
|
||||
@handle_http_errors(read_func_name)
|
||||
async def read_comments(service, user_google_email: str, presentation_id: str) -> str:
|
||||
"""Read all comments from a Google Slide, Sheet or Doc."""
|
||||
return await _read_comments_impl(service, app_name, presentation_id)
|
||||
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_file")
|
||||
@handle_http_errors(create_func_name)
|
||||
async def create_comment(service, user_google_email: str, presentation_id: str, comment_content: str) -> str:
|
||||
"""Create a new comment on a Google Slide, Sheet or Doc."""
|
||||
return await _create_comment_impl(service, app_name, presentation_id, comment_content)
|
||||
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_file")
|
||||
@handle_http_errors(reply_func_name)
|
||||
async def reply_to_comment(service, user_google_email: str, presentation_id: str, comment_id: str, reply_content: str) -> str:
|
||||
"""Reply to a specific comment in a Google Slide, Sheet or Doc."""
|
||||
return await _reply_to_comment_impl(service, app_name, presentation_id, comment_id, reply_content)
|
||||
|
||||
@server.tool()
|
||||
@require_google_service("drive", "drive_file")
|
||||
@handle_http_errors(resolve_func_name)
|
||||
async def resolve_comment(service, user_google_email: str, presentation_id: str, comment_id: str) -> str:
|
||||
"""Resolve a comment in a Google Slide, Sheet or Doc."""
|
||||
return await _resolve_comment_impl(service, app_name, presentation_id, comment_id)
|
||||
|
||||
# Set the proper function names for MCP registration
|
||||
read_comments.__name__ = read_func_name
|
||||
create_comment.__name__ = create_func_name
|
||||
reply_to_comment.__name__ = reply_func_name
|
||||
resolve_comment.__name__ = resolve_func_name
|
||||
|
||||
return {
|
||||
'read_comments': read_comments,
|
||||
'create_comment': create_comment,
|
||||
'reply_to_comment': reply_to_comment,
|
||||
'resolve_comment': resolve_comment
|
||||
}
|
||||
|
||||
|
||||
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(
|
||||
service.comments().list(
|
||||
fileId=file_id,
|
||||
fields="comments(id,content,author,createdTime,modifiedTime,resolved,replies(content,author,id,createdTime,modifiedTime))"
|
||||
).execute
|
||||
)
|
||||
|
||||
comments = response.get('comments', [])
|
||||
|
||||
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:
|
||||
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', '')
|
||||
status = " [RESOLVED]" if resolved else ""
|
||||
|
||||
output.append(f"Comment ID: {comment_id}")
|
||||
output.append(f"Author: {author}")
|
||||
output.append(f"Created: {created}{status}")
|
||||
output.append(f"Content: {content}")
|
||||
|
||||
# Add replies if any
|
||||
replies = comment.get('replies', [])
|
||||
if replies:
|
||||
output.append(f" Replies ({len(replies)}):")
|
||||
for reply in replies:
|
||||
reply_author = reply.get('author', {}).get('displayName', 'Unknown')
|
||||
reply_content = reply.get('content', '')
|
||||
reply_created = reply.get('createdTime', '')
|
||||
reply_id = reply.get('id', '')
|
||||
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)
|
||||
|
||||
|
||||
async def _create_comment_impl(service, app_name: str, file_id: str, comment_content: str) -> str:
|
||||
"""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(
|
||||
service.comments().create(
|
||||
fileId=file_id,
|
||||
body=body,
|
||||
fields="id,content,author,createdTime,modifiedTime"
|
||||
).execute
|
||||
)
|
||||
|
||||
comment_id = comment.get('id', '')
|
||||
author = comment.get('author', {}).get('displayName', 'Unknown')
|
||||
created = comment.get('createdTime', '')
|
||||
|
||||
return f"Comment created successfully!\\nComment ID: {comment_id}\\nAuthor: {author}\\nCreated: {created}\\nContent: {comment_content}"
|
||||
|
||||
|
||||
async def _reply_to_comment_impl(service, app_name: str, file_id: str, comment_id: str, reply_content: str) -> str:
|
||||
"""Implementation for replying to a comment on any Google Workspace file."""
|
||||
logger.info(f"[reply_to_{app_name}_comment] Replying to comment {comment_id} in {app_name} {file_id}")
|
||||
|
||||
body = {'content': reply_content}
|
||||
|
||||
reply = await asyncio.to_thread(
|
||||
service.replies().create(
|
||||
fileId=file_id,
|
||||
commentId=comment_id,
|
||||
body=body,
|
||||
fields="id,content,author,createdTime,modifiedTime"
|
||||
).execute
|
||||
)
|
||||
|
||||
reply_id = reply.get('id', '')
|
||||
author = reply.get('author', {}).get('displayName', 'Unknown')
|
||||
created = reply.get('createdTime', '')
|
||||
|
||||
return f"Reply posted successfully!\\nReply ID: {reply_id}\\nAuthor: {author}\\nCreated: {created}\\nContent: {reply_content}"
|
||||
|
||||
|
||||
async def _resolve_comment_impl(service, app_name: str, file_id: str, comment_id: str) -> str:
|
||||
"""Implementation for resolving a comment on any Google Workspace file."""
|
||||
logger.info(f"[resolve_{app_name}_comment] Resolving comment {comment_id} in {app_name} {file_id}")
|
||||
|
||||
body = {
|
||||
"content": "This comment has been resolved.",
|
||||
"action": "resolve"
|
||||
}
|
||||
|
||||
reply = await asyncio.to_thread(
|
||||
service.replies().create(
|
||||
fileId=file_id,
|
||||
commentId=comment_id,
|
||||
body=body,
|
||||
fields="id,content,author,createdTime,modifiedTime"
|
||||
).execute
|
||||
)
|
||||
|
||||
reply_id = reply.get('id', '')
|
||||
author = reply.get('author', {}).get('displayName', 'Unknown')
|
||||
created = reply.get('createdTime', '')
|
||||
|
||||
return f"Comment {comment_id} has been resolved successfully.\\nResolve reply ID: {reply_id}\\nAuthor: {author}\\nCreated: {created}"
|
||||
22
core/context.py
Normal file
22
core/context.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# core/context.py
|
||||
import contextvars
|
||||
from typing import Optional
|
||||
|
||||
# Context variable to hold injected credentials for the life of a single request.
|
||||
_injected_oauth_credentials = contextvars.ContextVar(
|
||||
"injected_oauth_credentials", default=None
|
||||
)
|
||||
|
||||
def get_injected_oauth_credentials():
|
||||
"""
|
||||
Retrieve injected OAuth credentials for the current request context.
|
||||
This is called by the authentication layer to check for request-scoped credentials.
|
||||
"""
|
||||
return _injected_oauth_credentials.get()
|
||||
|
||||
def set_injected_oauth_credentials(credentials: Optional[dict]):
|
||||
"""
|
||||
Set or clear the injected OAuth credentials for the current request context.
|
||||
This is called by the service decorator.
|
||||
"""
|
||||
_injected_oauth_credentials.set(credentials)
|
||||
@@ -11,7 +11,7 @@ from mcp import types
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from starlette.requests import Request
|
||||
|
||||
from auth.google_auth import handle_auth_callback, start_auth_flow, CONFIG_CLIENT_SECRETS_PATH
|
||||
from auth.google_auth import handle_auth_callback, start_auth_flow, check_client_secrets
|
||||
from auth.oauth_callback_server import get_oauth_redirect_uri, ensure_oauth_callback_available
|
||||
from auth.oauth_responses import create_error_response, create_success_response, create_server_error_response
|
||||
|
||||
@@ -120,11 +120,10 @@ async def oauth2_callback(request: Request) -> HTMLResponse:
|
||||
return create_error_response(error_message)
|
||||
|
||||
try:
|
||||
client_secrets_path = CONFIG_CLIENT_SECRETS_PATH
|
||||
if not os.path.exists(client_secrets_path):
|
||||
logger.error(f"OAuth client secrets file not found at {client_secrets_path}")
|
||||
# This is a server configuration error, should not happen in a deployed environment.
|
||||
return HTMLResponse(content="Server Configuration Error: Client secrets not found.", status_code=500)
|
||||
# Check if we have credentials available (environment variables or file)
|
||||
error_message = check_client_secrets()
|
||||
if error_message:
|
||||
return create_server_error_response(error_message)
|
||||
|
||||
logger.info(f"OAuth callback: Received code (state: {state}). Attempting to exchange for tokens.")
|
||||
|
||||
@@ -137,7 +136,6 @@ async def oauth2_callback(request: Request) -> HTMLResponse:
|
||||
# Exchange code for credentials. handle_auth_callback will save them.
|
||||
# The user_id returned here is the Google-verified email.
|
||||
verified_user_id, credentials = handle_auth_callback(
|
||||
client_secrets_path=client_secrets_path,
|
||||
scopes=SCOPES, # Ensure all necessary scopes are requested
|
||||
authorization_response=str(request.url),
|
||||
redirect_uri=get_oauth_redirect_uri_for_current_mode(),
|
||||
|
||||
Reference in New Issue
Block a user