working build for all functionality on streamablehttp with auth_session_id
This commit is contained in:
@@ -19,6 +19,7 @@ from googleapiclient.errors import HttpError
|
||||
|
||||
# Use functions directly from google_auth
|
||||
from auth.google_auth import get_credentials, handle_auth_callback
|
||||
from auth.auth_session_manager import auth_session_manager
|
||||
|
||||
# Configure module logger
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -26,11 +27,12 @@ logger = logging.getLogger(__name__)
|
||||
# Import the server directly (will be initialized before this module is imported)
|
||||
from core.server import server, OAUTH_REDIRECT_URI
|
||||
|
||||
# Define Google Calendar API Scopes
|
||||
CALENDAR_READONLY_SCOPE = "https://www.googleapis.com/auth/calendar.readonly"
|
||||
CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events"
|
||||
USERINFO_EMAIL_SCOPE = "https://www.googleapis.com/auth/userinfo.email"
|
||||
OPENID_SCOPE = "openid"
|
||||
# Import scopes from server
|
||||
from core.server import (
|
||||
SCOPES, BASE_SCOPES, CALENDAR_SCOPES,
|
||||
USERINFO_EMAIL_SCOPE, OPENID_SCOPE,
|
||||
CALENDAR_READONLY_SCOPE, CALENDAR_EVENTS_SCOPE
|
||||
)
|
||||
|
||||
# --- Configuration Placeholders (should ideally be loaded from a central app config) ---
|
||||
_client_secrets_env = os.getenv("GOOGLE_CLIENT_SECRETS")
|
||||
@@ -44,17 +46,20 @@ else:
|
||||
CONFIG_REDIRECT_URI = OAUTH_REDIRECT_URI
|
||||
# ---
|
||||
|
||||
async def _initiate_auth_and_get_message(user_id: str, scopes: List[str]) -> types.CallToolResult:
|
||||
async def _initiate_auth_and_get_message(scopes: List[str]) -> types.CallToolResult:
|
||||
"""
|
||||
Initiates the Google OAuth flow and returns a message for the user.
|
||||
Uses the MCP server's OAuth resource endpoint for callback handling.
|
||||
Returns a CallToolResult with appropriate content types.
|
||||
Initiates the Google OAuth flow using AuthSessionManager and returns a message for the user.
|
||||
The MCP server's OAuth resource endpoint handles callback.
|
||||
Returns a CallToolResult with auth URL and session ID.
|
||||
"""
|
||||
logger.info(f"Initiating auth for user '{user_id}' with scopes: {scopes}")
|
||||
logger.info(f"[_initiate_auth_and_get_message] Called with scopes: {scopes}")
|
||||
|
||||
try:
|
||||
logger.info(f"[_initiate_auth_and_get_message] For user '{user_id}', initiating auth flow with scopes: {scopes}")
|
||||
|
||||
# Create a new authentication session
|
||||
auth_session = auth_session_manager.create_session()
|
||||
session_id_for_state = auth_session.session_id
|
||||
logger.info(f"[_initiate_auth_and_get_message] Created auth session ID (for state): {session_id_for_state}")
|
||||
|
||||
# Ensure OAUTHLIB_INSECURE_TRANSPORT is set
|
||||
if 'OAUTHLIB_INSECURE_TRANSPORT' not in os.environ:
|
||||
logger.warning("OAUTHLIB_INSECURE_TRANSPORT not set. Setting it for localhost development.")
|
||||
@@ -65,43 +70,56 @@ async def _initiate_auth_and_get_message(user_id: str, scopes: List[str]) -> typ
|
||||
flow = Flow.from_client_secrets_file(
|
||||
CONFIG_CLIENT_SECRETS_PATH,
|
||||
scopes=scopes,
|
||||
redirect_uri=CONFIG_REDIRECT_URI
|
||||
redirect_uri=CONFIG_REDIRECT_URI,
|
||||
state=session_id_for_state # Pass the session_id as state
|
||||
)
|
||||
|
||||
# Generate the authorization URL with offline access and consent prompt
|
||||
auth_url, state = flow.authorization_url(
|
||||
# Generate the authorization URL
|
||||
auth_url, state_from_flow = flow.authorization_url(
|
||||
access_type='offline',
|
||||
prompt='consent'
|
||||
)
|
||||
logger.info(f"Auth flow started for user '{user_id}'. State: {state}. Advise user to visit: {auth_url}")
|
||||
# Verify state consistency (state_from_flow should match session_id_for_state)
|
||||
if state_from_flow != session_id_for_state:
|
||||
error_message = "OAuth state mismatch during flow generation. This is an internal server error."
|
||||
logger.error(f"OAuth state mismatch! Expected {session_id_for_state}, got {state_from_flow}. Aborting auth initiation.")
|
||||
auth_session_manager.fail_session(session_id_for_state, "OAuth state mismatch during flow generation.")
|
||||
return types.CallToolResult(
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=error_message)]
|
||||
)
|
||||
|
||||
logger.info(f"Auth flow started. State: {state_from_flow}. Advise user to visit: {auth_url}")
|
||||
|
||||
# Return MCP-formatted response with auth URL
|
||||
# Return MCP-formatted response with auth URL and session ID
|
||||
instructional_message = (
|
||||
f"**ACTION REQUIRED: Authentication Needed**\n\n"
|
||||
f"1. To proceed, please [click here to authorize Google Calendar access]({auth_url}).\n\n"
|
||||
f"2. After successful authorization in your browser, you will receive an `auth_session_id` (it is: `{session_id_for_state}`).\n\n"
|
||||
f"3. Call the `get_auth_result` tool, providing this `auth_session_id`, to obtain your verified `user_id`.\n\n"
|
||||
f"4. Once you have your `user_id`, you can retry your original command."
|
||||
)
|
||||
return types.CallToolResult(
|
||||
isError=True, # Indicates an action is required from the user/LLM before proceeding
|
||||
content=[
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=f"ACTION REQUIRED for user '{user_id}':"
|
||||
),
|
||||
types.LinkContent(
|
||||
type="link",
|
||||
url=auth_url,
|
||||
display_text="Click here to authorize Google Calendar access"
|
||||
),
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text="After successful authorization, please RETRY your original command."
|
||||
text=instructional_message
|
||||
)
|
||||
]
|
||||
)
|
||||
except Exception as e:
|
||||
error_message = f"Could not initiate authentication for user '{user_id}'. {str(e)}"
|
||||
error_message = f"Could not initiate authentication due to an unexpected error: {str(e)}"
|
||||
logger.error(f"Failed to start the OAuth flow: {e}", exc_info=True)
|
||||
# If session was created, mark it as failed
|
||||
if 'auth_session' in locals() and auth_session: # Check if auth_session was defined
|
||||
auth_session_manager.fail_session(auth_session.session_id, f"OAuth flow initiation error: {e}")
|
||||
return types.CallToolResult(
|
||||
isError=True,
|
||||
content=[
|
||||
types.ErrorContent(
|
||||
type="error",
|
||||
error_type="auth_initialization_error",
|
||||
message=error_message
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=error_message
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -109,29 +127,81 @@ async def _initiate_auth_and_get_message(user_id: str, scopes: List[str]) -> typ
|
||||
# --- Tool Implementations ---
|
||||
|
||||
@server.tool()
|
||||
async def start_auth(user_id: str) -> types.CallToolResult:
|
||||
async def start_auth() -> types.CallToolResult:
|
||||
"""
|
||||
Starts the Google OAuth authentication process.
|
||||
The user will be prompted to visit a URL and then retry their command.
|
||||
This tool is useful for pre-authentication or if other tools fail due to auth.
|
||||
Starts the Google OAuth authentication process using a session-based flow.
|
||||
The user will be prompted to visit an authorization URL.
|
||||
After authorization, they must call 'get_auth_result' with the provided
|
||||
'auth_session_id' to obtain their verified user_id for subsequent tool calls.
|
||||
|
||||
Args:
|
||||
user_id (str): The user identifier to authenticate
|
||||
|
||||
Returns:
|
||||
A CallToolResult with LinkContent for authentication URL and TextContent for instructions
|
||||
A CallToolResult with the authentication URL, an auth_session_id, and instructions.
|
||||
"""
|
||||
logger.info(f"Tool 'start_auth' invoked for user: {user_id}")
|
||||
logger.info(f"Tool 'start_auth' invoked. This will initiate a new session-based OAuth flow.")
|
||||
# Define desired scopes for general authentication, including userinfo
|
||||
auth_scopes = list(set([
|
||||
CALENDAR_READONLY_SCOPE, # Default for viewing
|
||||
USERINFO_EMAIL_SCOPE,
|
||||
OPENID_SCOPE
|
||||
]))
|
||||
return await _initiate_auth_and_get_message(user_id, auth_scopes)
|
||||
# These are the broadest scopes needed for any calendar operation.
|
||||
auth_scopes = SCOPES # Use the comprehensive SCOPES from core.server
|
||||
|
||||
logger.info(f"[start_auth] Using scopes: {auth_scopes}")
|
||||
# The user_id is not known at this point; it will be determined after OAuth.
|
||||
return await _initiate_auth_and_get_message(auth_scopes)
|
||||
|
||||
|
||||
@server.tool()
|
||||
async def get_auth_result(auth_session_id: str) -> types.CallToolResult:
|
||||
"""
|
||||
Retrieves the result of an authentication attempt using the auth_session_id.
|
||||
This tool should be called after the user completes the OAuth flow initiated by 'start_auth'.
|
||||
|
||||
Args:
|
||||
auth_session_id (str): The session ID provided by the 'start_auth' tool.
|
||||
|
||||
Returns:
|
||||
A CallToolResult containing the verified user_id if authentication was successful,
|
||||
or an error message if it failed or is still pending.
|
||||
"""
|
||||
logger.info(f"[get_auth_result] Tool invoked with auth_session_id: '{auth_session_id}'")
|
||||
session = auth_session_manager.get_session(auth_session_id)
|
||||
|
||||
if not session:
|
||||
message = f"Authentication session ID '{auth_session_id}' not found. Please ensure you are using the correct ID provided by 'start_auth' or restart the authentication process with 'start_auth'."
|
||||
logger.warning(f"[get_auth_result] Auth session not found for ID: '{auth_session_id}'")
|
||||
return types.CallToolResult(
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
|
||||
if session.status == "pending":
|
||||
message = "Authentication is still pending. Please ensure you have completed the authorization steps in your browser. Then, call this tool again."
|
||||
logger.info(f"[get_auth_result] Auth session '{auth_session_id}' is still pending.")
|
||||
return types.CallToolResult(
|
||||
isError=True, # Still an error in the sense that the original goal isn't met
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
elif session.status == "completed" and session.user_id:
|
||||
message = f"Authentication successful. Your verified user_id is: {session.user_id}. You can now use this user_id to retry your original command."
|
||||
logger.info(f"[get_auth_result] Auth session '{auth_session_id}' completed. Verified user_id: '{session.user_id}'.")
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.TextContent(type="text", text=message)
|
||||
]
|
||||
)
|
||||
elif session.status == "failed":
|
||||
message = f"Authentication failed for session '{auth_session_id}'. Error: {session.error_message or 'Unknown reason.'}. Please try running 'start_auth' again."
|
||||
logger.warning(f"[get_auth_result] Auth session '{auth_session_id}' failed. Error: {session.error_message}")
|
||||
return types.CallToolResult(
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
else: # Should not happen
|
||||
message = f"Authentication session '{auth_session_id}' is in an unknown state: {session.status}. This is an internal server error."
|
||||
logger.error(f"[get_auth_result] Auth session '{auth_session_id}' is in an unknown state: {session.status}")
|
||||
return types.CallToolResult(
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
|
||||
|
||||
async def list_calendars(user_id: str) -> types.CallToolResult:
|
||||
"""
|
||||
Lists the Google Calendars the user has access to.
|
||||
@@ -141,10 +211,11 @@ async def list_calendars(user_id: str) -> types.CallToolResult:
|
||||
user_id (str): The user identifier to list calendars for
|
||||
|
||||
Returns:
|
||||
A CallToolResult with either JsonContent containing calendars or ErrorContent
|
||||
A CallToolResult with TextContent describing the list of calendars or an error message.
|
||||
"""
|
||||
logger.info(f"[list_calendars] Tool invoked for user: {user_id}") # ADDED LOG
|
||||
required_scopes = [CALENDAR_READONLY_SCOPE]
|
||||
logger.info(f"[list_calendars] Tool invoked for user: {user_id}")
|
||||
# Always use full scopes to ensure future operations work
|
||||
required_scopes = SCOPES
|
||||
|
||||
# If user_id is 'default', try to find existing credentials
|
||||
if user_id == 'default':
|
||||
@@ -159,7 +230,7 @@ async def list_calendars(user_id: str) -> types.CallToolResult:
|
||||
break
|
||||
|
||||
try:
|
||||
logger.info(f"[list_calendars] Attempting to get_credentials for user_id: '{user_id}' with scopes: {required_scopes}") # ADDED LOG
|
||||
logger.info(f"[list_calendars] Attempting to get_credentials for user_id: '{user_id}'")
|
||||
credentials = await asyncio.to_thread(
|
||||
get_credentials,
|
||||
user_id,
|
||||
@@ -168,70 +239,71 @@ async def list_calendars(user_id: str) -> types.CallToolResult:
|
||||
)
|
||||
logger.debug(f"get_credentials returned: {credentials}")
|
||||
except Exception as e:
|
||||
message = f"Failed to get credentials for user '{user_id}': {e}. This might be an internal issue or the stored credentials might be corrupted. You can try to re-authenticate using the 'start_auth' tool."
|
||||
logger.error(f"Error getting credentials for {user_id}: {e}", exc_info=True)
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.ErrorContent(
|
||||
type="error",
|
||||
error_type="credential_error",
|
||||
message=f"Failed to get credentials: {e}. You might need to authenticate using the 'start_auth' tool."
|
||||
)
|
||||
]
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
|
||||
if not credentials or not credentials.valid:
|
||||
logger.warning(f"[list_calendars] Missing or invalid credentials for user '{user_id}'. Initiating auth with scopes: {required_scopes}") # MODIFIED LOG
|
||||
return await _initiate_auth_and_get_message(user_id, required_scopes)
|
||||
tool_name = "list_calendars"
|
||||
message = (
|
||||
f"**Authentication Required for '{tool_name}'**\n\n"
|
||||
f"Valid credentials for user '{user_id}' are missing or invalid.\n\n"
|
||||
f"Please follow these steps:\n"
|
||||
f"1. Call the `start_auth` tool (it takes no arguments). This will provide an authorization URL and an `auth_session_id`.\n"
|
||||
f"2. Complete the authorization flow in your browser.\n"
|
||||
f"3. Call the `get_auth_result` tool with the `auth_session_id` obtained in step 1. This will return your verified `user_id`.\n"
|
||||
f"4. Retry the `{tool_name}` command using the verified `user_id`."
|
||||
)
|
||||
logger.warning(f"[{tool_name}] Missing or invalid credentials for user '{user_id}'. Instructing LLM to use 'start_auth' and 'get_auth_result'.")
|
||||
return types.CallToolResult(
|
||||
isError=True, # Action required from user/LLM
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
|
||||
try:
|
||||
service = build('calendar', 'v3', credentials=credentials)
|
||||
logger.info(f"Successfully built calendar service for user: {user_id}")
|
||||
|
||||
calendar_list = await asyncio.to_thread(service.calendarList().list().execute)
|
||||
items = calendar_list.get('items', [])
|
||||
calendar_list_response = await asyncio.to_thread(service.calendarList().list().execute)
|
||||
items = calendar_list_response.get('items', [])
|
||||
|
||||
calendars = []
|
||||
if not items:
|
||||
return types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text=f"No calendars found for user '{user_id}'.")]
|
||||
)
|
||||
|
||||
calendars_summary_list = []
|
||||
for calendar in items:
|
||||
calendars.append({
|
||||
"id": calendar['id'],
|
||||
"summary": calendar.get('summary', 'No Summary'),
|
||||
"description": calendar.get('description', ''),
|
||||
"primary": calendar.get('primary', False),
|
||||
"accessRole": calendar.get('accessRole', '')
|
||||
})
|
||||
|
||||
summary = calendar.get('summary', 'No Summary')
|
||||
cal_id = calendar['id']
|
||||
primary_indicator = " (Primary)" if calendar.get('primary') else ""
|
||||
calendars_summary_list.append(f"- \"{summary}\"{primary_indicator} (ID: {cal_id})")
|
||||
|
||||
calendars_text_output = f"Successfully listed {len(items)} calendars for user '{user_id}':\n" + "\n".join(calendars_summary_list)
|
||||
logger.info(f"Successfully listed {len(items)} calendars for user: {user_id}")
|
||||
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.JsonContent(
|
||||
type="json",
|
||||
json={"calendars": calendars}
|
||||
)
|
||||
types.TextContent(type="text", text=calendars_text_output)
|
||||
]
|
||||
)
|
||||
|
||||
except HttpError as error:
|
||||
message = f"An API error occurred while listing calendars for user '{user_id}': {error}. This might be due to insufficient permissions or an issue with the Google Calendar API. You might need to re-authenticate using 'start_auth'."
|
||||
logger.error(f"An API error occurred for user {user_id} listing calendars: {error}", exc_info=True)
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.ErrorContent(
|
||||
type="error",
|
||||
error_type="api_error",
|
||||
message=f"An API error occurred: {error}. You might need to re-authenticate using 'start_auth'."
|
||||
)
|
||||
]
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
except Exception as e:
|
||||
message = f"An unexpected error occurred while listing calendars for user '{user_id}': {e}."
|
||||
logger.exception(f"An unexpected error occurred while listing calendars for {user_id}: {e}")
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.ErrorContent(
|
||||
type="error",
|
||||
error_type="unexpected_error",
|
||||
message=f"An unexpected error occurred: {e}"
|
||||
)
|
||||
]
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
|
||||
@server.tool()
|
||||
@@ -254,9 +326,9 @@ async def get_events(
|
||||
max_results (int): Maximum number of events to return (default: 25)
|
||||
|
||||
Returns:
|
||||
A CallToolResult with either JsonContent containing events or ErrorContent
|
||||
A CallToolResult with TextContent describing the list of events or an error message.
|
||||
"""
|
||||
logger.info(f"[get_events] Tool invoked for user: {user_id}, calendar: {calendar_id}") # ADDED LOG
|
||||
logger.info(f"[get_events] Tool invoked for user: {user_id}, calendar: {calendar_id}")
|
||||
required_scopes = [CALENDAR_READONLY_SCOPE]
|
||||
|
||||
try:
|
||||
@@ -269,90 +341,94 @@ async def get_events(
|
||||
)
|
||||
logger.debug(f"get_credentials returned: {credentials}")
|
||||
except Exception as e:
|
||||
message = f"Failed to get credentials for user '{user_id}': {e}. This might be an internal issue or the stored credentials might be corrupted. You can try to re-authenticate using the 'start_auth' tool."
|
||||
logger.error(f"Error getting credentials for {user_id}: {e}", exc_info=True)
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.ErrorContent(
|
||||
type="error",
|
||||
error_type="credential_error",
|
||||
message=f"Failed to get credentials: {e}. You might need to authenticate using the 'start_auth' tool."
|
||||
)
|
||||
]
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
|
||||
if not credentials or not credentials.valid:
|
||||
logger.warning(f"[get_events] Missing or invalid credentials for user '{user_id}'. Initiating auth with scopes: {required_scopes}") # MODIFIED LOG
|
||||
return await _initiate_auth_and_get_message(user_id, required_scopes)
|
||||
tool_name = "get_events"
|
||||
message = (
|
||||
f"**Authentication Required for '{tool_name}'**\n\n"
|
||||
f"Valid credentials for user '{user_id}' are missing or invalid.\n\n"
|
||||
f"Please follow these steps:\n"
|
||||
f"1. Call the `start_auth` tool (it takes no arguments). This will provide an authorization URL and an `auth_session_id`.\n"
|
||||
f"2. Complete the authorization flow in your browser.\n"
|
||||
f"3. Call the `get_auth_result` tool with the `auth_session_id` obtained in step 1. This will return your verified `user_id`.\n"
|
||||
f"4. Retry the `{tool_name}` command using the verified `user_id`."
|
||||
)
|
||||
logger.warning(f"[{tool_name}] Missing or invalid credentials for user '{user_id}'. Instructing LLM to use 'start_auth' and 'get_auth_result'.")
|
||||
return types.CallToolResult(
|
||||
isError=True, # Action required from user/LLM
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
|
||||
try:
|
||||
service = build('calendar', 'v3', credentials=credentials)
|
||||
logger.info(f"Successfully built calendar service for user: {user_id}")
|
||||
|
||||
if time_min is None:
|
||||
now = datetime.datetime.utcnow().isoformat() + 'Z'
|
||||
time_min = now
|
||||
logger.info(f"Defaulting time_min to current time: {time_min}")
|
||||
effective_time_min = time_min
|
||||
if effective_time_min is None:
|
||||
effective_time_min = datetime.datetime.utcnow().isoformat() + 'Z' # Default to now
|
||||
logger.info(f"Defaulting time_min to current time: {effective_time_min}")
|
||||
|
||||
time_max_log = f"and {time_max}" if time_max else "indefinitely (no end time specified)"
|
||||
logger.info(f"Fetching events for {user_id} from calendar '{calendar_id}' starting {effective_time_min} {time_max_log}, max results: {max_results}")
|
||||
|
||||
events_result = await asyncio.to_thread(
|
||||
service.events().list(
|
||||
calendarId=calendar_id,
|
||||
timeMin=time_min,
|
||||
timeMax=time_max,
|
||||
timeMin=effective_time_min,
|
||||
timeMax=time_max, # Can be None
|
||||
maxResults=max_results,
|
||||
singleEvents=True,
|
||||
orderBy='startTime'
|
||||
).execute
|
||||
)
|
||||
events = events_result.get('items', [])
|
||||
items = events_result.get('items', [])
|
||||
|
||||
parsed_events = []
|
||||
for event in events:
|
||||
parsed_events.append({
|
||||
"id": event['id'],
|
||||
"summary": event.get('summary', 'No Title'),
|
||||
"start": event['start'].get('dateTime', event['start'].get('date')),
|
||||
"end": event['end'].get('dateTime', event['end'].get('date')),
|
||||
"location": event.get('location', ''),
|
||||
"description": event.get('description', ''),
|
||||
"htmlLink": event.get('htmlLink', '')
|
||||
})
|
||||
if not items:
|
||||
return types.CallToolResult(
|
||||
content=[types.TextContent(type="text", text=f"No events found for user '{user_id}' in calendar '{calendar_id}' for the specified time range.")]
|
||||
)
|
||||
|
||||
event_summary_list = []
|
||||
for event_item in items:
|
||||
summary = event_item.get('summary', 'No Title')
|
||||
start_obj = event_item['start']
|
||||
start_time_str = start_obj.get('dateTime', start_obj.get('date')) # Handles all-day events
|
||||
event_id = event_item['id']
|
||||
event_link = event_item.get('htmlLink', '')
|
||||
|
||||
logger.info(f"Successfully retrieved {len(events)} events for user: {user_id}, calendar: {calendar_id}")
|
||||
event_desc = f"- \"{summary}\" starting at {start_time_str} (ID: {event_id})"
|
||||
if event_link:
|
||||
event_desc += f" [Link: {event_link}]"
|
||||
event_summary_list.append(event_desc)
|
||||
|
||||
events_text_output = f"Successfully fetched {len(items)} events for user '{user_id}' from calendar '{calendar_id}':\n" + "\n".join(event_summary_list)
|
||||
logger.info(f"Successfully retrieved {len(items)} events for user: {user_id}, calendar: {calendar_id}")
|
||||
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.JsonContent(
|
||||
type="json",
|
||||
json={
|
||||
"calendar_id": calendar_id,
|
||||
"events": parsed_events,
|
||||
"event_count": len(parsed_events)
|
||||
}
|
||||
)
|
||||
types.TextContent(type="text", text=events_text_output)
|
||||
]
|
||||
)
|
||||
|
||||
except HttpError as error:
|
||||
message = f"An API error occurred while fetching events for user '{user_id}': {error}. This might be due to insufficient permissions or an issue with the Google Calendar API. You might need to re-authenticate using 'start_auth'."
|
||||
logger.error(f"An API error occurred for user {user_id} getting events: {error}", exc_info=True)
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.ErrorContent(
|
||||
type="error",
|
||||
error_type="api_error",
|
||||
message=f"An API error occurred while fetching events: {error}. You might need to re-authenticate using 'start_auth'."
|
||||
)
|
||||
]
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
except Exception as e:
|
||||
message = f"An unexpected error occurred while fetching events for user '{user_id}': {e}."
|
||||
logger.exception(f"An unexpected error occurred while getting events for {user_id}: {e}")
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.ErrorContent(
|
||||
type="error",
|
||||
error_type="unexpected_error",
|
||||
message=f"An unexpected error occurred: {e}"
|
||||
)
|
||||
]
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
|
||||
|
||||
@@ -384,10 +460,11 @@ async def create_event(
|
||||
timezone (Optional[str]): Timezone for the event
|
||||
|
||||
Returns:
|
||||
A CallToolResult with either JsonContent containing created event details or ErrorContent
|
||||
A CallToolResult with TextContent confirming event creation or an error message.
|
||||
"""
|
||||
logger.info(f"Attempting to create event for user: {user_id}, calendar: {calendar_id}")
|
||||
required_scopes = [CALENDAR_EVENTS_SCOPE] # Write scope needed
|
||||
logger.info(f"[create_event] Requesting credentials with scopes: {required_scopes}")
|
||||
|
||||
try:
|
||||
credentials = await asyncio.to_thread(
|
||||
@@ -398,29 +475,38 @@ async def create_event(
|
||||
)
|
||||
logger.debug(f"get_credentials returned: {credentials}")
|
||||
except Exception as e:
|
||||
message = f"Failed to get credentials for user '{user_id}': {e}. This might be an internal issue or the stored credentials might be corrupted. You can try to re-authenticate using the 'start_auth' tool."
|
||||
logger.error(f"Error getting credentials for {user_id}: {e}", exc_info=True)
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.ErrorContent(
|
||||
type="error",
|
||||
error_type="credential_error",
|
||||
message=f"Failed to get credentials: {e}. You might need to authenticate using the 'start_auth' tool."
|
||||
)
|
||||
]
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
|
||||
if not credentials or not credentials.valid:
|
||||
logger.warning(f"Missing or invalid credentials for user '{user_id}' for create_event. Initiating auth.")
|
||||
return await _initiate_auth_and_get_message(user_id, required_scopes)
|
||||
tool_name = "create_event"
|
||||
message = (
|
||||
f"**Authentication Required for '{tool_name}'**\n\n"
|
||||
f"Valid credentials for user '{user_id}' are missing or invalid.\n\n"
|
||||
f"Please follow these steps:\n"
|
||||
f"1. Call the `start_auth` tool (it takes no arguments). This will provide an authorization URL and an `auth_session_id`.\n"
|
||||
f"2. Complete the authorization flow in your browser.\n"
|
||||
f"3. Call the `get_auth_result` tool with the `auth_session_id` obtained in step 1. This will return your verified `user_id`.\n"
|
||||
f"4. Retry the `{tool_name}` command using the verified `user_id`."
|
||||
)
|
||||
logger.warning(f"[{tool_name}] Missing or invalid credentials for user '{user_id}'. Instructing LLM to use 'start_auth' and 'get_auth_result'.")
|
||||
return types.CallToolResult(
|
||||
isError=True, # Action required from user/LLM
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
|
||||
try:
|
||||
service = build('calendar', 'v3', credentials=credentials)
|
||||
logger.info(f"Successfully built calendar service for user: {user_id}")
|
||||
|
||||
event_body = {
|
||||
event_body: Dict[str, Any] = {
|
||||
'summary': summary,
|
||||
'start': {'dateTime': start_time},
|
||||
'end': {'dateTime': end_time},
|
||||
'start': {'dateTime': start_time}, # Timezone will be added if provided
|
||||
'end': {'dateTime': end_time}, # Timezone will be added if provided
|
||||
}
|
||||
if location:
|
||||
event_body['location'] = location
|
||||
@@ -430,57 +516,48 @@ async def create_event(
|
||||
event_body['start']['timeZone'] = timezone
|
||||
event_body['end']['timeZone'] = timezone
|
||||
if attendees:
|
||||
event_body['attendees'] = [{'email': email} for email in attendees]
|
||||
event_body['attendees'] = [{'email': email_address} for email_address in attendees]
|
||||
|
||||
logger.debug(f"Creating event with body: {event_body}")
|
||||
logger.debug(f"Creating event with body: {event_body} for calendar: {calendar_id}")
|
||||
|
||||
created_event = await asyncio.to_thread(
|
||||
created_event_details = await asyncio.to_thread(
|
||||
service.events().insert(
|
||||
calendarId=calendar_id,
|
||||
body=event_body
|
||||
).execute
|
||||
)
|
||||
|
||||
logger.info(f"Successfully created event for user: {user_id}, event ID: {created_event['id']}")
|
||||
|
||||
event_summary_text = created_event_details.get('summary', 'No Title')
|
||||
event_id_text = created_event_details.get('id')
|
||||
event_link_text = created_event_details.get('htmlLink', 'N/A')
|
||||
created_time_text = created_event_details.get('created', 'N/A')
|
||||
|
||||
logger.info(f"Successfully created event for user: {user_id}, event ID: {event_id_text}, Link: {event_link_text}")
|
||||
|
||||
success_message = (
|
||||
f"Successfully created event: \"{event_summary_text}\".\n"
|
||||
f"- Event ID: {event_id_text}\n"
|
||||
f"- Calendar ID: {calendar_id}\n"
|
||||
f"- Link to event: {event_link_text}\n"
|
||||
f"- Created at: {created_time_text}"
|
||||
)
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=f"Successfully created event '{created_event.get('summary', '')}'"
|
||||
),
|
||||
types.JsonContent(
|
||||
type="json",
|
||||
json={
|
||||
"event_id": created_event['id'],
|
||||
"html_link": created_event.get('htmlLink', ''),
|
||||
"summary": created_event.get('summary', ''),
|
||||
"calendar_id": calendar_id,
|
||||
"created": created_event.get('created', '')
|
||||
}
|
||||
)
|
||||
types.TextContent(type="text", text=success_message)
|
||||
]
|
||||
)
|
||||
|
||||
except HttpError as error:
|
||||
message = f"An API error occurred while creating event for user '{user_id}': {error}. This could be due to invalid event details (e.g., time format), insufficient permissions, or an issue with the Google Calendar API. You might need to re-authenticate using 'start_auth'."
|
||||
logger.error(f"An API error occurred for user {user_id} creating event: {error}", exc_info=True)
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.ErrorContent(
|
||||
type="error",
|
||||
error_type="api_error",
|
||||
message=f"An API error occurred while creating the event: {error}. You might need to re-authenticate using 'start_auth'."
|
||||
)
|
||||
]
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
except Exception as e:
|
||||
message = f"An unexpected error occurred while creating event for user '{user_id}': {e}."
|
||||
logger.exception(f"An unexpected error occurred while creating event for {user_id}: {e}")
|
||||
return types.CallToolResult(
|
||||
content=[
|
||||
types.ErrorContent(
|
||||
type="error",
|
||||
error_type="unexpected_error",
|
||||
message=f"An unexpected error occurred: {e}"
|
||||
)
|
||||
]
|
||||
isError=True,
|
||||
content=[types.TextContent(type="text", text=message)]
|
||||
)
|
||||
Reference in New Issue
Block a user