Initial commit
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
# Google Calendar OAuth Demo (Read-Only)
|
||||
|
||||
This example demonstrates how to create safe, read-only MCP tools that interact with Google Calendar using the MXCP OAuth authentication system with the Google Calendar API.
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
### 1. MXCP OAuth Authentication
|
||||
- Project-wide Google OAuth configuration
|
||||
- Automatic token management through MXCP authentication system
|
||||
- User authentication via standard OAuth 2.0 flow
|
||||
- Error handling for authentication failures
|
||||
|
||||
### 2. Google Calendar API Integration (Read-Only)
|
||||
- `whoami` - Display information about the current authenticated Google user
|
||||
- `list_calendars` - Retrieve all accessible calendars with filtering options
|
||||
- `get_calendar` - Get detailed information for a specific calendar
|
||||
- `list_events` - List events from a calendar with time filtering and pagination
|
||||
- `get_event` - Retrieve detailed information for a specific event
|
||||
- `search_events` - Search for events matching text queries
|
||||
- `get_freebusy` - Check availability across multiple calendars
|
||||
- Token-based API access using authenticated user context
|
||||
- **Safe Design**: Only read operations - no calendar or event modifications
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Google Account**: You need a Google account with Calendar access
|
||||
2. **Google Cloud Project**: Create a project in Google Cloud Console with Calendar API enabled
|
||||
3. **OAuth Credentials**: Create OAuth 2.0 credentials for your application
|
||||
4. **Python Dependencies**: The `google-api-python-client` and related libraries (automatically managed by MXCP)
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Create Google Cloud Project and Enable APIs
|
||||
|
||||
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project or select an existing one
|
||||
3. Enable the Google Calendar API:
|
||||
- Go to **APIs & Services** → **Library**
|
||||
- Search for "Google Calendar API"
|
||||
- Click on it and press **Enable**
|
||||
|
||||
### 2. Configure OAuth Consent Screen (Required First)
|
||||
|
||||
1. In Google Cloud Console, go to **APIs & Services** → **OAuth consent screen**
|
||||
2. Configure the consent screen:
|
||||
- **User Type**: External (for testing) or Internal (for organization use)
|
||||
- **App Name**: "MXCP Google Calendar Integration" (or your preferred name)
|
||||
- **User Support Email**: Your email
|
||||
- **Developer Contact**: Your email
|
||||
3. **Add Scopes** (under "Data access" section):
|
||||
- Click "Add or Remove Scopes"
|
||||
- In the scope selection dialog, search for "calendar"
|
||||
- Find and select `https://www.googleapis.com/auth/calendar.readonly` (Calendar read-only access)
|
||||
- Click "Update" to save the scopes
|
||||
4. Save the consent screen configuration
|
||||
|
||||
**Note**: The scopes are configured in the OAuth Consent Screen, not when creating the Client ID. This is why you don't see scope options when creating credentials.
|
||||
|
||||
### 3. Create OAuth 2.0 Client ID
|
||||
|
||||
1. Go to **APIs & Services** → **Credentials**
|
||||
2. Click **Create Credentials** → **OAuth 2.0 Client IDs**
|
||||
3. Configure the client:
|
||||
- **Application Type**: Web application
|
||||
- **Name**: "MXCP Calendar Client" (or your preferred name)
|
||||
- **Authorized Redirect URIs**: Add based on your deployment:
|
||||
- **Local Development**: `http://localhost:8000/google/callback`
|
||||
- **Remote/Production**: `https://your-domain.com/google/callback` (replace with your actual server URL)
|
||||
4. Save and note down the **Client ID** and **Client Secret**
|
||||
|
||||
### 4. Configure Environment Variables
|
||||
|
||||
Set your Google OAuth credentials:
|
||||
```bash
|
||||
export GOOGLE_CLIENT_ID="your-client-id-from-google-cloud"
|
||||
export GOOGLE_CLIENT_SECRET="your-client-secret-from-google-cloud"
|
||||
```
|
||||
|
||||
### 5. Configure Callback URL for Your Deployment
|
||||
|
||||
The callback URL configuration depends on where your MXCP server will run:
|
||||
|
||||
#### Local Development
|
||||
For local development, the default configuration in `config.yml` uses `http://localhost:8000/google/callback`. This works when:
|
||||
- You're running MXCP locally on your development machine
|
||||
- Users authenticate from the same machine where MXCP is running
|
||||
|
||||
#### Remote/Production Deployment
|
||||
For remote servers or production deployments, you need to:
|
||||
|
||||
1. **Update config.yml**: Modify the callback URL:
|
||||
```yaml
|
||||
redirect_uris:
|
||||
- "https://your-domain.com/google/callback" # Your actual URL
|
||||
```
|
||||
|
||||
2. **Update base_url**: Set the correct base URL in your config:
|
||||
```yaml
|
||||
transport:
|
||||
http:
|
||||
base_url: https://your-domain.com # Your actual server URL
|
||||
```
|
||||
|
||||
3. **Configure OAuth Credentials**: Add the production callback URL to your Google Cloud OAuth credentials
|
||||
|
||||
**Important**:
|
||||
- The callback URL must be accessible from the user's browser, not just from your server
|
||||
- For production deployments, Google requires HTTPS for callback URLs
|
||||
- You can configure multiple callback URLs in your OAuth credentials to support both local development and production
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
google-calendar/
|
||||
├── mxcp-site.yml # Project metadata
|
||||
├── config.yml # Server and authentication configuration
|
||||
├── python/ # Python modules
|
||||
│ └── google_calendar_client.py # Google Calendar API implementations
|
||||
├── tools/ # Tool definitions (read-only)
|
||||
│ ├── whoami.yml # Current user information
|
||||
│ ├── list_calendars.yml # List accessible calendars
|
||||
│ ├── get_calendar.yml # Get calendar details
|
||||
│ ├── list_events.yml # List calendar events
|
||||
│ ├── get_event.yml # Get event details
|
||||
│ ├── search_events.yml # Search for events
|
||||
│ └── get_freebusy.yml # Check availability
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
1. **MXCP OAuth Integration**: Uses MXCP's built-in Google OAuth provider for secure authentication
|
||||
2. **User Context**: Access tokens are automatically managed and provided through `get_user_context()`
|
||||
3. **Token-based Authentication**: Google API client is initialized with OAuth tokens instead of service account credentials
|
||||
4. **Project-wide Configuration**: Authentication is configured at the project level in `config.yml`
|
||||
5. **Error Handling**: Comprehensive error handling for authentication and API failures
|
||||
6. **Type Safety**: Uses Python type hints and comprehensive error handling for data validation
|
||||
|
||||
## Running the Example
|
||||
|
||||
Once you've completed the setup above:
|
||||
|
||||
1. **Start MXCP**:
|
||||
```bash
|
||||
# From the examples/google-calendar directory:
|
||||
MXCP_CONFIG=config.yml mxcp serve
|
||||
```
|
||||
|
||||
2. **Connect your MCP client** (e.g., Claude Desktop) to the MXCP server
|
||||
|
||||
3. **Authenticate**: When the client first connects, you'll be redirected to Google to authorize the application
|
||||
|
||||
4. **Use the tools**: Once authenticated, you can use all the Google Calendar tools through your MCP client
|
||||
|
||||
## Example Usage
|
||||
|
||||
When you use the tools through an MCP client, you can:
|
||||
|
||||
### Get User Information
|
||||
```
|
||||
Use the whoami tool to see your Google profile information
|
||||
```
|
||||
|
||||
### Manage Calendars
|
||||
```
|
||||
List all your calendars, get details for specific calendars, and check which ones you can modify
|
||||
```
|
||||
|
||||
### View Calendar Events
|
||||
```
|
||||
- List events: "What's on my calendar this week?"
|
||||
- Search events: "Find all meetings with John"
|
||||
- Get event details: "Show me details for my 3 PM meeting"
|
||||
- View event information: "What meetings do I have with the marketing team?"
|
||||
```
|
||||
|
||||
### Check Availability
|
||||
```
|
||||
Use the freebusy tool to find available time slots across multiple calendars
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Errors
|
||||
- **"No user context available"**: User needs to authenticate first by running `mxcp serve` and completing OAuth flow
|
||||
- **"No Google access token found"**: Authentication was incomplete or token expired - re-authenticate
|
||||
- **OAuth Credentials Issues**: Verify your `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` are correct
|
||||
- **Callback URL Mismatch**: Ensure the callback URL in your Google Cloud OAuth credentials matches where your MXCP server is accessible
|
||||
- **API Not Enabled**: Make sure the Google Calendar API is enabled in your Google Cloud project
|
||||
|
||||
### API Errors
|
||||
- **403 Forbidden**: Check that the Calendar API is enabled and your OAuth scopes include calendar access
|
||||
- **404 Not Found**: Verify calendar IDs and event IDs are correct and accessible to the authenticated user
|
||||
- **Rate Limiting**: Google Calendar API has rate limits - implement appropriate retry logic if needed
|
||||
|
||||
### OAuth Setup Issues
|
||||
- **Consent Screen**: Make sure your OAuth consent screen is properly configured with the correct scopes
|
||||
- **Redirect URI**: The redirect URI must exactly match your MXCP server's accessible address
|
||||
- **Scopes**: Ensure your OAuth configuration includes `https://www.googleapis.com/auth/calendar.readonly` scope
|
||||
|
||||
## Next Steps
|
||||
|
||||
This example demonstrates a comprehensive set of read-only Google Calendar integration tools. You could extend it with additional features like:
|
||||
- Advanced calendar filtering and search capabilities
|
||||
- Integration with other Google Workspace services (read-only)
|
||||
- Calendar analytics and reporting
|
||||
- Event pattern analysis and insights
|
||||
- Multi-calendar comparison and availability analysis
|
||||
|
||||
**Note**: This example is intentionally read-only for safety. If you need write operations (create, update, delete), you would need to:
|
||||
- Change the OAuth scope to `https://www.googleapis.com/auth/calendar` (full access)
|
||||
- Add appropriate write functions with proper validation and error handling
|
||||
- Implement additional safety measures and user confirmations
|
||||
@@ -0,0 +1,22 @@
|
||||
mxcp: 1
|
||||
transport:
|
||||
http:
|
||||
port: 8000
|
||||
host: 0.0.0.0
|
||||
# Set base_url to your server's public URL for production
|
||||
base_url: http://localhost:8000
|
||||
|
||||
projects:
|
||||
google-calendar:
|
||||
profiles:
|
||||
default:
|
||||
# OAuth Authentication Configuration
|
||||
auth:
|
||||
provider: google
|
||||
google:
|
||||
client_id: "${GOOGLE_CLIENT_ID}"
|
||||
client_secret: "${GOOGLE_CLIENT_SECRET}"
|
||||
scope: "https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email"
|
||||
callback_path: "/google/callback"
|
||||
auth_url: "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
token_url: "https://oauth2.googleapis.com/token"
|
||||
@@ -0,0 +1,3 @@
|
||||
mxcp: 1
|
||||
project: google-calendar
|
||||
profile: default
|
||||
@@ -0,0 +1,804 @@
|
||||
"""
|
||||
Google Calendar MCP client implementation using mxcp OAuth authentication.
|
||||
|
||||
This module provides Google Calendar API integration with:
|
||||
- OAuth 2.0 authentication via mxcp framework
|
||||
- Thread-safe client caching for performance
|
||||
- Simplified time handling for LLM consumption
|
||||
- Comprehensive error handling and user-friendly messages
|
||||
- Full type safety with Pydantic models
|
||||
"""
|
||||
|
||||
# Required for union syntax (|) in type annotations with runtime objects like threading.Lock
|
||||
# Without this, Python tries to evaluate "threading.Lock | None" at runtime, which fails
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from datetime import date, datetime, timezone
|
||||
from functools import wraps
|
||||
from typing import Any
|
||||
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import Resource, build # type: ignore[import-untyped]
|
||||
from googleapiclient.errors import HttpError # type: ignore[import-untyped]
|
||||
|
||||
from mxcp.runtime import on_init, on_shutdown
|
||||
from mxcp.sdk.auth.context import get_user_context
|
||||
|
||||
# =============================================================================
|
||||
# TIME CONVERSION UTILITIES
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def _datetime_to_google_time(
|
||||
dt: datetime, all_day: bool = False, time_zone: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Convert datetime object to Google Calendar API time format.
|
||||
|
||||
Args:
|
||||
dt: Python datetime object (should be timezone-aware)
|
||||
all_day: Whether this represents an all-day event
|
||||
time_zone: Optional timezone override
|
||||
|
||||
Returns:
|
||||
Google API time object: {"dateTime": "...", "timeZone": "..."}
|
||||
or {"date": "YYYY-MM-DD"} for all-day events
|
||||
"""
|
||||
if all_day:
|
||||
# For all-day events, use date format
|
||||
return {"date": dt.date().isoformat()}
|
||||
else:
|
||||
# For timed events, use dateTime format
|
||||
time_obj = {"dateTime": dt.isoformat()}
|
||||
|
||||
# Add timezone if specified or if datetime has timezone info
|
||||
if time_zone:
|
||||
time_obj["timeZone"] = time_zone
|
||||
elif dt.tzinfo:
|
||||
# Extract timezone name from datetime object if possible
|
||||
tz_name = getattr(dt.tzinfo, "zone", None) or str(dt.tzinfo)
|
||||
if tz_name != "UTC" and "+" not in tz_name and "-" not in tz_name:
|
||||
time_obj["timeZone"] = tz_name
|
||||
else:
|
||||
# If no timezone info is available, try to get user's timezone
|
||||
try:
|
||||
user_timezone = _get_user_timezone()
|
||||
logger.warning(
|
||||
f"Datetime object has no timezone info, using user timezone: {user_timezone}"
|
||||
)
|
||||
time_obj["timeZone"] = user_timezone
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
f"Datetime object has no timezone information and cannot determine timezone from calendar. "
|
||||
f"Please either: 1) Use timezone-aware datetime objects, or 2) Specify the time_zone parameter. "
|
||||
f"Original error: {e}"
|
||||
) from e
|
||||
|
||||
return time_obj
|
||||
|
||||
|
||||
def _get_user_timezone() -> str:
|
||||
"""Get the user's timezone from their primary calendar.
|
||||
|
||||
This function only tries to get the timezone from the user's primary calendar.
|
||||
If that fails, it raises an exception to force explicit timezone specification.
|
||||
|
||||
Results are cached to avoid repeated lookups.
|
||||
|
||||
Returns:
|
||||
IANA timezone identifier from user's primary calendar
|
||||
|
||||
Raises:
|
||||
ValueError: If timezone cannot be determined from primary calendar
|
||||
"""
|
||||
global _user_timezone_cache, _timezone_cache_lock
|
||||
|
||||
# Check cache first
|
||||
if _timezone_cache_lock and _user_timezone_cache:
|
||||
with _timezone_cache_lock:
|
||||
if _user_timezone_cache:
|
||||
return _user_timezone_cache
|
||||
|
||||
try:
|
||||
# Get timezone from user's primary calendar
|
||||
calendar_info = get_calendar("primary")
|
||||
if calendar_info and calendar_info.get("timeZone"):
|
||||
user_timezone: str = calendar_info["timeZone"]
|
||||
logger.debug(f"Using timezone from primary calendar: {user_timezone}")
|
||||
# Cache the result
|
||||
if _timezone_cache_lock:
|
||||
with _timezone_cache_lock:
|
||||
_user_timezone_cache = user_timezone
|
||||
return user_timezone
|
||||
else:
|
||||
raise ValueError("Primary calendar does not have timezone information")
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not get timezone from primary calendar: {e}")
|
||||
raise ValueError(
|
||||
"Cannot determine timezone from primary calendar. "
|
||||
"Please specify the time_zone parameter explicitly in your function call."
|
||||
) from e
|
||||
|
||||
|
||||
def _google_time_to_datetime(google_time: dict[str, Any]) -> tuple[datetime, bool]:
|
||||
"""
|
||||
Convert Google Calendar API time format to datetime object.
|
||||
|
||||
Args:
|
||||
google_time: Google API time object
|
||||
|
||||
Returns:
|
||||
Tuple of (datetime_object, is_all_day)
|
||||
"""
|
||||
if "date" in google_time:
|
||||
# All-day event - convert date to datetime at midnight UTC
|
||||
date_obj = date.fromisoformat(google_time["date"])
|
||||
dt = datetime.combine(date_obj, datetime.min.time(), tzinfo=timezone.utc)
|
||||
return dt, True
|
||||
elif "dateTime" in google_time:
|
||||
# Timed event - parse ISO datetime
|
||||
dt = datetime.fromisoformat(google_time["dateTime"])
|
||||
return dt, False
|
||||
else:
|
||||
raise ValueError(f"Invalid Google time format: {google_time}")
|
||||
|
||||
|
||||
def _convert_event_to_simplified(google_event: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Convert Google Calendar API event to our simplified EventInfo format.
|
||||
|
||||
Args:
|
||||
google_event: Raw event from Google Calendar API
|
||||
|
||||
Returns:
|
||||
Event in simplified format matching EventInfo model
|
||||
"""
|
||||
# Convert start/end times
|
||||
start_dt, start_all_day = _google_time_to_datetime(google_event["start"])
|
||||
end_dt, end_all_day = _google_time_to_datetime(google_event["end"])
|
||||
|
||||
# Ensure both times have same all-day status
|
||||
all_day = start_all_day and end_all_day
|
||||
|
||||
simplified_event = {
|
||||
"id": google_event["id"],
|
||||
"summary": google_event.get("summary", ""),
|
||||
"description": google_event.get("description"),
|
||||
"location": google_event.get("location"),
|
||||
"start_time": start_dt,
|
||||
"end_time": end_dt,
|
||||
"all_day": all_day,
|
||||
"time_zone": google_event.get("start", {}).get("timeZone"),
|
||||
"status": google_event.get("status", "confirmed"),
|
||||
"htmlLink": google_event.get("htmlLink", ""),
|
||||
"created": datetime.fromisoformat(google_event["created"].replace("Z", "+00:00")),
|
||||
"updated": datetime.fromisoformat(google_event["updated"].replace("Z", "+00:00")),
|
||||
"calendar_id": google_event.get("calendarId", "unknown"), # Added by our code
|
||||
"etag": google_event.get("etag"),
|
||||
}
|
||||
|
||||
# Convert attendees if present
|
||||
if "attendees" in google_event:
|
||||
simplified_event["attendees"] = [
|
||||
{
|
||||
"email": att["email"],
|
||||
"displayName": att.get("displayName"),
|
||||
"responseStatus": att.get("responseStatus", "needsAction"),
|
||||
"optional": att.get("optional", False),
|
||||
"resource": att.get("resource", False),
|
||||
"comment": att.get("comment"),
|
||||
"additionalGuests": att.get("additionalGuests", 0),
|
||||
}
|
||||
for att in google_event["attendees"]
|
||||
]
|
||||
|
||||
# Convert creator/organizer if present
|
||||
for role in ["creator", "organizer"]:
|
||||
if role in google_event:
|
||||
simplified_event[role] = {
|
||||
"email": google_event[role].get("email"),
|
||||
"displayName": google_event[role].get("displayName"),
|
||||
"self": google_event[role].get("self", False),
|
||||
}
|
||||
|
||||
# Handle recurrence rules
|
||||
if "recurrence" in google_event:
|
||||
simplified_event["recurrence"] = google_event["recurrence"]
|
||||
|
||||
# Handle reminders
|
||||
if "reminders" in google_event:
|
||||
reminders = google_event["reminders"]
|
||||
simplified_event["reminders"] = {
|
||||
"useDefault": reminders.get("useDefault", True),
|
||||
"overrides": reminders.get("overrides"),
|
||||
}
|
||||
|
||||
# Handle transparency and visibility
|
||||
simplified_event["transparency"] = google_event.get("transparency", "opaque")
|
||||
simplified_event["visibility"] = google_event.get("visibility", "default")
|
||||
|
||||
return simplified_event
|
||||
|
||||
|
||||
def _convert_simplified_to_google_event(simplified_event: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Convert simplified event format to Google Calendar API format.
|
||||
|
||||
Args:
|
||||
simplified_event: Event in our simplified format
|
||||
|
||||
Returns:
|
||||
Event in Google Calendar API format
|
||||
"""
|
||||
google_event = {
|
||||
"summary": simplified_event["summary"],
|
||||
"start": _datetime_to_google_time(
|
||||
simplified_event["start_time"],
|
||||
simplified_event.get("all_day", False),
|
||||
simplified_event.get("time_zone"),
|
||||
),
|
||||
"end": _datetime_to_google_time(
|
||||
simplified_event["end_time"],
|
||||
simplified_event.get("all_day", False),
|
||||
simplified_event.get("time_zone"),
|
||||
),
|
||||
}
|
||||
|
||||
# Add optional fields
|
||||
optional_fields = ["description", "location"]
|
||||
for field in optional_fields:
|
||||
if field in simplified_event and simplified_event[field] is not None:
|
||||
google_event[field] = simplified_event[field]
|
||||
|
||||
# Convert attendees
|
||||
if "attendees" in simplified_event and simplified_event["attendees"]:
|
||||
google_event["attendees"] = [
|
||||
(
|
||||
{"email": str(email)}
|
||||
if isinstance(email, str)
|
||||
else {
|
||||
"email": (
|
||||
str(email) if isinstance(email, str) else str(attendee.get("email", ""))
|
||||
),
|
||||
"displayName": attendee.get("displayName"),
|
||||
"optional": attendee.get("optional", False),
|
||||
"resource": attendee.get("resource", False),
|
||||
"comment": attendee.get("comment"),
|
||||
"additionalGuests": attendee.get("additionalGuests", 0),
|
||||
}
|
||||
)
|
||||
for email in simplified_event["attendees"]
|
||||
for attendee in [email if isinstance(email, dict) else {"email": email}]
|
||||
]
|
||||
|
||||
# Add transparency and visibility if specified
|
||||
if "transparency" in simplified_event:
|
||||
google_event["transparency"] = simplified_event["transparency"]
|
||||
if "visibility" in simplified_event:
|
||||
google_event["visibility"] = simplified_event["visibility"]
|
||||
if "recurrence" in simplified_event:
|
||||
google_event["recurrence"] = simplified_event["recurrence"]
|
||||
|
||||
# Add reminders if specified
|
||||
if "reminders" in simplified_event and simplified_event["reminders"]:
|
||||
google_event["reminders"] = simplified_event["reminders"]
|
||||
|
||||
return google_event
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# THREAD-SAFE CLIENT CACHING
|
||||
# =============================================================================
|
||||
|
||||
# Thread-safe cache for Google Calendar service clients
|
||||
_client_cache: dict[str, Resource] | None = None
|
||||
_cache_lock: threading.Lock | None = None
|
||||
|
||||
# Global timezone cache to avoid repeated lookups
|
||||
_user_timezone_cache: str | None = None
|
||||
_timezone_cache_lock: threading.Lock | None = None
|
||||
|
||||
|
||||
@on_init
|
||||
def init_client_cache() -> None:
|
||||
"""Initialize the Google Calendar client cache and timezone cache."""
|
||||
global _client_cache, _cache_lock, _timezone_cache_lock
|
||||
_client_cache = {}
|
||||
_cache_lock = threading.Lock()
|
||||
_timezone_cache_lock = threading.Lock()
|
||||
|
||||
|
||||
@on_shutdown
|
||||
def clear_client_cache() -> None:
|
||||
"""Clear the Google Calendar client cache and timezone cache."""
|
||||
global _client_cache, _cache_lock, _user_timezone_cache, _timezone_cache_lock
|
||||
_client_cache = None
|
||||
_cache_lock = None
|
||||
_user_timezone_cache = None
|
||||
_timezone_cache_lock = None
|
||||
|
||||
|
||||
def _get_cache_key(context: Any) -> str | None:
|
||||
"""Generate cache key based on user context."""
|
||||
if not context:
|
||||
return None
|
||||
|
||||
# Use user ID as cache key for per-user client isolation
|
||||
user_id = getattr(context, "user_id", None) or getattr(context, "id", None)
|
||||
if user_id:
|
||||
return f"gcal:{user_id}"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_google_credentials() -> Credentials:
|
||||
"""Get Google OAuth credentials from mxcp user context."""
|
||||
context = get_user_context()
|
||||
if not context or not context.external_token:
|
||||
raise ValueError("No user context available. User must be authenticated.")
|
||||
|
||||
# Create Google credentials object with OAuth token
|
||||
credentials = Credentials(token=context.external_token) # type: ignore[no-untyped-call]
|
||||
return credentials
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# LOGGING & ERROR HANDLING
|
||||
# =============================================================================
|
||||
|
||||
# Set up comprehensive logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def with_session_retry(func: Any) -> Any:
|
||||
"""
|
||||
Decorator for handling OAuth token refresh and API errors with user-friendly messages.
|
||||
|
||||
Wraps functions to automatically handle:
|
||||
- OAuth token refresh failures (RefreshError)
|
||||
- Google API HTTP errors with specific status codes
|
||||
- Client cache invalidation on auth failures
|
||||
- Comprehensive error logging for debugging
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
try:
|
||||
logger.info(f"Executing {func.__name__} with args={args}, kwargs={kwargs}")
|
||||
result = func(*args, **kwargs)
|
||||
logger.info(f"Successfully completed {func.__name__}")
|
||||
return result
|
||||
|
||||
except RefreshError as e:
|
||||
# OAuth token has expired and cannot be refreshed
|
||||
logger.warning(f"OAuth token refresh failed in {func.__name__}: {e}")
|
||||
clear_client_cache() # Clear cache to force re-authentication
|
||||
error_msg = (
|
||||
"Your Google Calendar access has expired. Please re-authenticate to continue."
|
||||
)
|
||||
logger.error(f"Authentication error: {error_msg}")
|
||||
raise ValueError(error_msg) from e
|
||||
|
||||
except HttpError as e:
|
||||
# Handle specific Google API errors with detailed logging
|
||||
status = e.resp.status
|
||||
error_details = str(e)
|
||||
logger.error(
|
||||
f"Google API HttpError in {func.__name__}: status={status}, details={error_details}"
|
||||
)
|
||||
|
||||
# Clear cache on authentication errors
|
||||
if status in [401, 403]:
|
||||
context = get_user_context()
|
||||
cache_key = _get_cache_key(context)
|
||||
if cache_key and _cache_lock and _client_cache:
|
||||
with _cache_lock:
|
||||
_client_cache.pop(cache_key, None)
|
||||
|
||||
# Just forward the original Google API error - it's already clear and actionable
|
||||
raise ValueError(f"Google Calendar API error: {error_details}") from e
|
||||
|
||||
# ValidationError removed since we no longer use Pydantic models
|
||||
|
||||
except ValueError as e:
|
||||
# Re-raise ValueError (these are user-friendly messages)
|
||||
logger.warning(f"User error in {func.__name__}: {e}")
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
# Catch-all for unexpected errors
|
||||
logger.error(
|
||||
f"Unexpected error in {func.__name__}: {type(e).__name__}: {e}", exc_info=True
|
||||
)
|
||||
raise ValueError(f"An unexpected error occurred: {str(e)}") from e
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _get_google_calendar_client() -> Resource:
|
||||
"""
|
||||
Get cached Google Calendar API client or create new one with OAuth authentication.
|
||||
|
||||
Uses per-user caching for performance and proper multi-user isolation.
|
||||
"""
|
||||
try:
|
||||
# Get authenticated user context
|
||||
context = get_user_context()
|
||||
if not context:
|
||||
raise ValueError("No user context available. User must be authenticated.")
|
||||
|
||||
# Check cache first
|
||||
cache_key = _get_cache_key(context)
|
||||
if cache_key and _cache_lock and _client_cache:
|
||||
with _cache_lock:
|
||||
if cache_key in _client_cache:
|
||||
logging.info("Using cached Google Calendar client")
|
||||
return _client_cache[cache_key]
|
||||
|
||||
logging.info("Creating new Google Calendar client")
|
||||
# Create new authenticated client
|
||||
credentials = _get_google_credentials()
|
||||
service = build(
|
||||
serviceName="calendar",
|
||||
version="v3",
|
||||
credentials=credentials,
|
||||
cache_discovery=True, # Cache API discovery documents
|
||||
num_retries=3, # Retry transient failures
|
||||
)
|
||||
|
||||
# Cache the client for this user
|
||||
if cache_key and _cache_lock and _client_cache:
|
||||
with _cache_lock:
|
||||
_client_cache[cache_key] = service
|
||||
|
||||
return service
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to initialize Google Calendar client: {str(e)}") from e
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MCP TOOL FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def whoami() -> dict[str, Any]:
|
||||
"""
|
||||
Get information about the currently authenticated user.
|
||||
|
||||
Returns:
|
||||
UserInfo object with user profile data from Google OAuth
|
||||
|
||||
Note:
|
||||
Uses OAuth profile information, no additional API calls required
|
||||
"""
|
||||
context = get_user_context()
|
||||
if not context:
|
||||
raise ValueError("No user context available. User must be authenticated.")
|
||||
|
||||
# Extract user information from OAuth profile
|
||||
raw_profile = context.raw_profile or {}
|
||||
|
||||
# Return plain dictionary following standard MXCP pattern
|
||||
return {
|
||||
"id": raw_profile.get("sub") or raw_profile.get("id", "unknown"),
|
||||
"email": raw_profile.get("email", "unknown@example.com"),
|
||||
"name": raw_profile.get("name", "Unknown User"),
|
||||
"given_name": raw_profile.get("given_name"),
|
||||
"family_name": raw_profile.get("family_name"),
|
||||
"picture": raw_profile.get("picture"),
|
||||
"locale": raw_profile.get("locale"),
|
||||
"verified_email": raw_profile.get("email_verified"),
|
||||
}
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def list_calendars(
|
||||
show_hidden: bool = False,
|
||||
show_deleted: bool = False,
|
||||
max_results: int = 100,
|
||||
min_access_role: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
List all calendars accessible to the authenticated user.
|
||||
|
||||
Args:
|
||||
show_hidden: Include hidden calendars in results
|
||||
show_deleted: Include deleted calendars in results
|
||||
max_results: Maximum number of calendars to return (1-250)
|
||||
min_access_role: Filter by minimum access level
|
||||
|
||||
Returns:
|
||||
List of CalendarInfo objects with user's accessible calendars
|
||||
|
||||
Raises:
|
||||
ValueError: If user is not authenticated or parameters are invalid
|
||||
"""
|
||||
service = _get_google_calendar_client()
|
||||
|
||||
# Build parameters
|
||||
params: dict[str, Any] = {
|
||||
"maxResults": min(max_results, 250),
|
||||
"showHidden": show_hidden,
|
||||
"showDeleted": show_deleted,
|
||||
}
|
||||
|
||||
if min_access_role:
|
||||
params["minAccessRole"] = min_access_role
|
||||
|
||||
# Execute API call
|
||||
result = service.calendarList().list(**params).execute()
|
||||
|
||||
# Return plain dictionaries following standard MXCP pattern
|
||||
calendars = []
|
||||
for cal in result.get("items", []):
|
||||
calendar_dict = {
|
||||
"id": cal["id"],
|
||||
"summary": cal.get("summary", ""),
|
||||
"description": cal.get("description"),
|
||||
"timeZone": cal.get("timeZone", "UTC"),
|
||||
"accessRole": cal.get("accessRole", "reader"),
|
||||
"primary": cal.get("primary", False),
|
||||
"backgroundColor": cal.get("backgroundColor"),
|
||||
"foregroundColor": cal.get("foregroundColor"),
|
||||
"selected": cal.get("selected", False),
|
||||
"hidden": cal.get("hidden", False),
|
||||
"defaultReminders": cal.get("defaultReminders"),
|
||||
}
|
||||
calendars.append(calendar_dict)
|
||||
|
||||
return calendars
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def get_calendar(calendar_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
Get detailed information for a specific calendar.
|
||||
|
||||
Args:
|
||||
calendar_id: Calendar identifier or 'primary' for main calendar
|
||||
|
||||
Returns:
|
||||
CalendarInfo object with calendar details
|
||||
|
||||
Raises:
|
||||
ValueError: If calendar_id is invalid or user lacks access
|
||||
"""
|
||||
service = _get_google_calendar_client()
|
||||
|
||||
try:
|
||||
result = service.calendarList().get(calendarId=calendar_id).execute()
|
||||
except HttpError as e:
|
||||
if e.resp.status == 404:
|
||||
raise ValueError(
|
||||
f"Calendar '{calendar_id}' not found or you don't have access to it"
|
||||
) from e
|
||||
raise
|
||||
|
||||
# Return plain dictionary following standard MXCP pattern
|
||||
return {
|
||||
"id": result["id"],
|
||||
"summary": result.get("summary", ""),
|
||||
"description": result.get("description"),
|
||||
"timeZone": result.get("timeZone", "UTC"),
|
||||
"accessRole": result.get("accessRole", "reader"),
|
||||
"primary": result.get("primary", False),
|
||||
"backgroundColor": result.get("backgroundColor"),
|
||||
"foregroundColor": result.get("foregroundColor"),
|
||||
"selected": result.get("selected", False),
|
||||
"hidden": result.get("hidden", False),
|
||||
"defaultReminders": result.get("defaultReminders"),
|
||||
}
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def list_events(
|
||||
calendar_id: str = "primary",
|
||||
time_min: datetime | None = None,
|
||||
time_max: datetime | None = None,
|
||||
max_results: int = 250,
|
||||
single_events: bool = True,
|
||||
order_by: str = "startTime",
|
||||
page_token: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
List events from a specific calendar with optional time filtering.
|
||||
|
||||
Args:
|
||||
calendar_id: Calendar to query ('primary' or specific calendar ID)
|
||||
time_min: Lower bound for event start times (inclusive)
|
||||
time_max: Upper bound for event start times (exclusive)
|
||||
max_results: Maximum number of events to return (1-2500)
|
||||
single_events: Whether to expand recurring events into instances
|
||||
order_by: Sort order - 'startTime' or 'updated'
|
||||
page_token: Token for pagination
|
||||
|
||||
Returns:
|
||||
EventSearchResult with events and pagination info
|
||||
"""
|
||||
service = _get_google_calendar_client()
|
||||
|
||||
# Build parameters
|
||||
params = {
|
||||
"calendarId": calendar_id,
|
||||
"maxResults": min(max_results, 2500),
|
||||
"singleEvents": single_events,
|
||||
"orderBy": order_by,
|
||||
}
|
||||
|
||||
if time_min:
|
||||
params["timeMin"] = time_min.isoformat()
|
||||
if time_max:
|
||||
params["timeMax"] = time_max.isoformat()
|
||||
if page_token:
|
||||
params["pageToken"] = page_token
|
||||
|
||||
# Execute API call
|
||||
result = service.events().list(**params).execute()
|
||||
|
||||
# Convert events to simplified format
|
||||
events = []
|
||||
for event in result.get("items", []):
|
||||
event["calendarId"] = calendar_id # Add calendar_id to event
|
||||
simplified_event = _convert_event_to_simplified(event)
|
||||
events.append(simplified_event)
|
||||
|
||||
# Return plain dictionary following standard MXCP pattern
|
||||
return {
|
||||
"events": events,
|
||||
"next_page_token": result.get("nextPageToken"),
|
||||
"total_results": len(events),
|
||||
}
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def get_event(calendar_id: str, event_id: str) -> dict[str, Any]:
|
||||
"""
|
||||
Retrieve detailed information for a specific event.
|
||||
|
||||
Args:
|
||||
calendar_id: Calendar containing the event
|
||||
event_id: Event identifier
|
||||
|
||||
Returns:
|
||||
EventInfo object with complete event details
|
||||
|
||||
Raises:
|
||||
ValueError: If event not found or user lacks access
|
||||
"""
|
||||
service = _get_google_calendar_client()
|
||||
|
||||
try:
|
||||
result = service.events().get(calendarId=calendar_id, eventId=event_id).execute()
|
||||
except HttpError as e:
|
||||
if e.resp.status == 404:
|
||||
raise ValueError(f"Event '{event_id}' not found in calendar '{calendar_id}'") from e
|
||||
raise
|
||||
|
||||
# Add calendar_id to event
|
||||
result["calendarId"] = calendar_id
|
||||
simplified_event = _convert_event_to_simplified(result)
|
||||
|
||||
return simplified_event
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def search_events(
|
||||
q: str,
|
||||
calendar_id: str = "primary",
|
||||
time_min: datetime | None = None,
|
||||
time_max: datetime | None = None,
|
||||
max_results: int = 250,
|
||||
page_token: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Search for events matching a text query.
|
||||
|
||||
Args:
|
||||
q: Free text search query (searches title, description, location, attendees)
|
||||
calendar_id: Calendar to search ('primary' or specific calendar ID)
|
||||
time_min: Earliest event start time to include
|
||||
time_max: Latest event start time to include
|
||||
max_results: Maximum number of events to return
|
||||
page_token: Token for pagination
|
||||
|
||||
Returns:
|
||||
EventSearchResult with matching events and pagination info
|
||||
"""
|
||||
service = _get_google_calendar_client()
|
||||
|
||||
# Build parameters
|
||||
params = {
|
||||
"calendarId": calendar_id,
|
||||
"q": q,
|
||||
"maxResults": min(max_results, 2500),
|
||||
"singleEvents": True,
|
||||
"orderBy": "startTime",
|
||||
}
|
||||
|
||||
if time_min:
|
||||
params["timeMin"] = time_min.isoformat()
|
||||
if time_max:
|
||||
params["timeMax"] = time_max.isoformat()
|
||||
if page_token:
|
||||
params["pageToken"] = page_token
|
||||
|
||||
# Execute API call
|
||||
result = service.events().list(**params).execute()
|
||||
|
||||
# Convert events to simplified format
|
||||
events = []
|
||||
for event in result.get("items", []):
|
||||
event["calendarId"] = calendar_id # Add calendar_id to event
|
||||
simplified_event = _convert_event_to_simplified(event)
|
||||
events.append(simplified_event)
|
||||
|
||||
# Return plain dictionary following standard MXCP pattern
|
||||
return {
|
||||
"events": events,
|
||||
"next_page_token": result.get("nextPageToken"),
|
||||
"total_results": len(events),
|
||||
}
|
||||
|
||||
|
||||
@with_session_retry
|
||||
def get_freebusy(calendar_ids: list[str], time_min: datetime, time_max: datetime) -> dict[str, Any]:
|
||||
"""
|
||||
Check free/busy status across multiple calendars.
|
||||
|
||||
Args:
|
||||
calendar_ids: List of calendar IDs to check (use 'primary' for main calendar)
|
||||
time_min: Start time for availability check
|
||||
time_max: End time for availability check
|
||||
|
||||
Returns:
|
||||
FreeBusyResponse with busy periods for each calendar
|
||||
|
||||
Note:
|
||||
Useful for finding meeting slots and checking availability before scheduling
|
||||
"""
|
||||
if time_min >= time_max:
|
||||
raise ValueError("time_max must be after time_min")
|
||||
|
||||
service = _get_google_calendar_client()
|
||||
|
||||
# Build request
|
||||
request_body = {
|
||||
"timeMin": time_min.isoformat(),
|
||||
"timeMax": time_max.isoformat(),
|
||||
"items": [{"id": cal_id} for cal_id in calendar_ids],
|
||||
}
|
||||
|
||||
# Execute API call
|
||||
result = service.freebusy().query(body=request_body).execute()
|
||||
|
||||
# Convert to plain dictionary format following standard MXCP pattern
|
||||
calendars = []
|
||||
for calendar_id in calendar_ids:
|
||||
calendar_data = result.get("calendars", {}).get(calendar_id, {})
|
||||
|
||||
# Convert busy times to plain dictionaries
|
||||
busy_times = []
|
||||
for busy_period in calendar_data.get("busy", []):
|
||||
busy_times.append(
|
||||
{
|
||||
"start": busy_period["start"],
|
||||
"end": busy_period["end"],
|
||||
}
|
||||
)
|
||||
|
||||
calendars.append(
|
||||
{"calendar_id": calendar_id, "busy": busy_times, "errors": calendar_data.get("errors")}
|
||||
)
|
||||
|
||||
return {
|
||||
"time_min": time_min.isoformat(),
|
||||
"time_max": time_max.isoformat(),
|
||||
"calendars": calendars,
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: get_calendar
|
||||
title: Get Calendar Details
|
||||
description: |
|
||||
Get detailed information for a specific calendar by ID.
|
||||
Returns calendar metadata including timezone, access role, and display properties.
|
||||
|
||||
Example usage:
|
||||
- "Get details for my primary calendar"
|
||||
- "Show me information about the work@company.com calendar"
|
||||
- "What timezone is my calendar set to?"
|
||||
tags:
|
||||
- google-calendar
|
||||
- calendars
|
||||
- get
|
||||
annotations:
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters:
|
||||
- name: calendar_id
|
||||
type: string
|
||||
description: "Calendar identifier or 'primary' for main calendar"
|
||||
default: "primary"
|
||||
examples: ["primary", "work@company.com", "team@company.com"]
|
||||
return:
|
||||
type: object
|
||||
description: "Calendar details and metadata"
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: "Calendar identifier"
|
||||
summary:
|
||||
type: string
|
||||
description: "Calendar name/title"
|
||||
description:
|
||||
type: string
|
||||
description: "Calendar description"
|
||||
timeZone:
|
||||
type: string
|
||||
description: "IANA timezone identifier"
|
||||
accessRole:
|
||||
type: string
|
||||
description: "User's access level"
|
||||
primary:
|
||||
type: boolean
|
||||
description: "Whether this is user's primary calendar"
|
||||
backgroundColor:
|
||||
type: string
|
||||
description: "Background color hex code"
|
||||
foregroundColor:
|
||||
type: string
|
||||
description: "Foreground color hex code"
|
||||
selected:
|
||||
type: boolean
|
||||
description: "Whether calendar is selected in UI"
|
||||
hidden:
|
||||
type: boolean
|
||||
description: "Whether calendar is hidden from list"
|
||||
required: ["id", "summary", "timeZone", "accessRole"]
|
||||
language: python
|
||||
source:
|
||||
file: ../python/google_calendar_client.py
|
||||
@@ -0,0 +1,96 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: get_event
|
||||
title: Get Event Details
|
||||
description: |
|
||||
Retrieve detailed information for a specific event by ID.
|
||||
Returns complete event data in simplified format optimized for LLM use.
|
||||
|
||||
Example usage:
|
||||
- "Show me details for event abc123 in my primary calendar"
|
||||
- "Get full information about that meeting I mentioned"
|
||||
- "What are the attendees for event xyz789?"
|
||||
tags:
|
||||
- google-calendar
|
||||
- events
|
||||
- get
|
||||
annotations:
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters:
|
||||
- name: calendar_id
|
||||
type: string
|
||||
description: "Calendar containing the event"
|
||||
examples: ["primary", "work@company.com", "team-calendar@company.com"]
|
||||
- name: event_id
|
||||
type: string
|
||||
description: "Event identifier"
|
||||
examples: ["abc123def456", "event_id_example", "recurring_event_20240115T090000Z"]
|
||||
return:
|
||||
type: object
|
||||
description: "Complete event details"
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: "Event identifier"
|
||||
summary:
|
||||
type: string
|
||||
description: "Event title"
|
||||
description:
|
||||
type: string
|
||||
description: "Event description"
|
||||
location:
|
||||
type: string
|
||||
description: "Event location"
|
||||
start_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Event start time"
|
||||
end_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Event end time"
|
||||
all_day:
|
||||
type: boolean
|
||||
description: "Whether this is an all-day event"
|
||||
time_zone:
|
||||
type: string
|
||||
description: "Event timezone"
|
||||
attendees:
|
||||
type: array
|
||||
description: "Event attendees"
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
description: "Attendee email"
|
||||
displayName:
|
||||
type: string
|
||||
description: "Attendee name"
|
||||
responseStatus:
|
||||
type: string
|
||||
description: "Response status"
|
||||
status:
|
||||
type: string
|
||||
description: "Event status"
|
||||
htmlLink:
|
||||
type: string
|
||||
description: "Google Calendar web URL"
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Creation timestamp"
|
||||
updated:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Last update timestamp"
|
||||
recurrence:
|
||||
type: array
|
||||
description: "Recurrence rules"
|
||||
items:
|
||||
type: string
|
||||
required: ["id", "summary", "start_time", "end_time", "htmlLink", "status"]
|
||||
language: python
|
||||
source:
|
||||
file: ../python/google_calendar_client.py
|
||||
@@ -0,0 +1,83 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: get_freebusy
|
||||
title: Check Calendar Availability
|
||||
description: |
|
||||
Check free/busy status across multiple calendars for a specified time range.
|
||||
Useful for finding meeting slots and checking availability before scheduling.
|
||||
|
||||
Example usage:
|
||||
- "Check my availability tomorrow from 9 AM to 5 PM"
|
||||
- "Find free time slots across my work and personal calendars"
|
||||
- "When am I free for a meeting this week?"
|
||||
- "Check availability for multiple team members' calendars"
|
||||
tags:
|
||||
- google-calendar
|
||||
- freebusy
|
||||
- availability
|
||||
annotations:
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters:
|
||||
- name: calendar_ids
|
||||
type: array
|
||||
description: "List of calendar IDs to check (use 'primary' for main calendar)"
|
||||
items:
|
||||
type: string
|
||||
description: "Calendar identifier"
|
||||
minItems: 1
|
||||
examples: [["primary"], ["primary", "work@company.com"], ["team@company.com", "resources@company.com"]]
|
||||
- name: time_min
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Start time for availability check (RFC3339 format)"
|
||||
examples: ["2024-01-15T09:00:00Z", "2024-01-15T09:00:00-08:00"]
|
||||
- name: time_max
|
||||
type: string
|
||||
format: date-time
|
||||
description: "End time for availability check (RFC3339 format)"
|
||||
examples: ["2024-01-15T17:00:00Z", "2024-01-15T17:00:00-08:00"]
|
||||
return:
|
||||
type: object
|
||||
description: "Free/busy information for requested calendars"
|
||||
properties:
|
||||
time_min:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Query start time"
|
||||
time_max:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Query end time"
|
||||
calendars:
|
||||
type: array
|
||||
description: "Per-calendar availability information"
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
calendar_id:
|
||||
type: string
|
||||
description: "Calendar identifier"
|
||||
busy:
|
||||
type: array
|
||||
description: "Busy time periods"
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
start:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Busy period start"
|
||||
end:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Busy period end"
|
||||
errors:
|
||||
type: array
|
||||
description: "API errors for this calendar"
|
||||
items:
|
||||
type: object
|
||||
required: ["time_min", "time_max", "calendars"]
|
||||
language: python
|
||||
source:
|
||||
file: ../python/google_calendar_client.py
|
||||
@@ -0,0 +1,84 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: list_calendars
|
||||
title: List Google Calendars
|
||||
description: |
|
||||
List all calendars accessible to the authenticated user.
|
||||
Returns calendars with their access permissions, timezones, and display properties.
|
||||
|
||||
Example usage:
|
||||
- "Show me all my calendars"
|
||||
- "List calendars I can write to" (with min_access_role: "writer")
|
||||
- "What calendars do I have access to?"
|
||||
- "Show me my work calendars" (then filter by name)
|
||||
tags:
|
||||
- google-calendar
|
||||
- calendars
|
||||
- list
|
||||
annotations:
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters:
|
||||
- name: show_hidden
|
||||
type: boolean
|
||||
description: "Include hidden calendars in results"
|
||||
default: false
|
||||
examples: [false, true]
|
||||
- name: show_deleted
|
||||
type: boolean
|
||||
description: "Include deleted calendars in results"
|
||||
default: false
|
||||
examples: [false, true]
|
||||
- name: max_results
|
||||
type: integer
|
||||
description: "Maximum number of calendars to return"
|
||||
default: 100
|
||||
minimum: 1
|
||||
maximum: 250
|
||||
examples: [10, 50, 100]
|
||||
- name: min_access_role
|
||||
type: string
|
||||
description: "Filter by minimum access role"
|
||||
enum: ["freeBusyReader", "owner", "reader", "writer"]
|
||||
default: null
|
||||
examples: ["reader", "writer", "owner"]
|
||||
return:
|
||||
type: array
|
||||
description: "List of accessible calendars"
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: "Calendar identifier (email address or calendar ID)"
|
||||
summary:
|
||||
type: string
|
||||
description: "Calendar name/title"
|
||||
description:
|
||||
type: string
|
||||
description: "Calendar description"
|
||||
timeZone:
|
||||
type: string
|
||||
description: "IANA timezone identifier"
|
||||
accessRole:
|
||||
type: string
|
||||
description: "User's access level"
|
||||
primary:
|
||||
type: boolean
|
||||
description: "Whether this is user's primary calendar"
|
||||
backgroundColor:
|
||||
type: string
|
||||
description: "Background color hex code"
|
||||
foregroundColor:
|
||||
type: string
|
||||
description: "Foreground color hex code"
|
||||
selected:
|
||||
type: boolean
|
||||
description: "Whether calendar is selected in UI"
|
||||
hidden:
|
||||
type: boolean
|
||||
description: "Whether calendar is hidden from list"
|
||||
required: ["id", "summary", "timeZone", "accessRole"]
|
||||
language: python
|
||||
source:
|
||||
file: ../python/google_calendar_client.py
|
||||
@@ -0,0 +1,153 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: list_events
|
||||
title: List Calendar Events
|
||||
description: |
|
||||
List events from a specific calendar with optional time filtering and pagination.
|
||||
Returns events in simplified format optimized for LLM consumption.
|
||||
|
||||
Example usage:
|
||||
- "What's on my calendar today?" (with time_min/time_max for today)
|
||||
- "Show me this week's meetings" (with time range for current week)
|
||||
- "List all events in my work calendar" (with specific calendar_id)
|
||||
- "What meetings do I have next month?" (with future time range)
|
||||
tags:
|
||||
- google-calendar
|
||||
- events
|
||||
- list
|
||||
annotations:
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters:
|
||||
- name: calendar_id
|
||||
type: string
|
||||
description: "Calendar to query ('primary' or specific calendar ID)"
|
||||
default: "primary"
|
||||
examples: ["primary", "work@company.com", "team-calendar@company.com"]
|
||||
- name: time_min
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Lower bound for event start times (RFC3339 format)"
|
||||
default: null
|
||||
examples: ["2024-01-15T00:00:00Z", "2024-01-15T09:00:00-08:00"]
|
||||
- name: time_max
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Upper bound for event start times (RFC3339 format)"
|
||||
default: null
|
||||
examples: ["2024-01-15T23:59:59Z", "2024-01-15T17:00:00-08:00"]
|
||||
- name: max_results
|
||||
type: integer
|
||||
description: "Maximum number of events to return"
|
||||
default: 250
|
||||
minimum: 1
|
||||
maximum: 2500
|
||||
examples: [10, 50, 250]
|
||||
- name: single_events
|
||||
type: boolean
|
||||
description: "Whether to expand recurring events into instances"
|
||||
default: true
|
||||
examples: [true, false]
|
||||
- name: order_by
|
||||
type: string
|
||||
description: "Sort order for events"
|
||||
enum: ["startTime", "updated"]
|
||||
default: "startTime"
|
||||
examples: ["startTime", "updated"]
|
||||
- name: page_token
|
||||
type: string
|
||||
description: "Token for pagination"
|
||||
default: null
|
||||
examples: ["CAESGjBpNDd2Nmp2Zml2cXRwYjBpOXA", "next_page_token_example"]
|
||||
return:
|
||||
type: object
|
||||
description: "Event search results with pagination"
|
||||
properties:
|
||||
events:
|
||||
type: array
|
||||
description: "Matching events"
|
||||
items:
|
||||
type: object
|
||||
description: "Complete event information with simplified time handling"
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: "Event identifier"
|
||||
summary:
|
||||
type: string
|
||||
description: "Event title"
|
||||
description:
|
||||
type: string
|
||||
description: "Event description"
|
||||
location:
|
||||
type: string
|
||||
description: "Event location"
|
||||
start_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Event start time (timezone-aware)"
|
||||
end_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Event end time (timezone-aware)"
|
||||
all_day:
|
||||
type: boolean
|
||||
description: "Whether this is an all-day event"
|
||||
time_zone:
|
||||
type: string
|
||||
description: "Event timezone (if different from calendar)"
|
||||
attendees:
|
||||
type: array
|
||||
description: "Event attendees"
|
||||
items:
|
||||
type: object
|
||||
creator:
|
||||
type: object
|
||||
description: "Event creator"
|
||||
organizer:
|
||||
type: object
|
||||
description: "Event organizer"
|
||||
status:
|
||||
type: string
|
||||
description: "Event status"
|
||||
htmlLink:
|
||||
type: string
|
||||
description: "Google Calendar web URL for this event"
|
||||
created:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Event creation timestamp (timezone-aware)"
|
||||
updated:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Last update timestamp (timezone-aware)"
|
||||
recurrence:
|
||||
type: array
|
||||
description: "Recurrence rules in RRULE format"
|
||||
items:
|
||||
type: string
|
||||
reminders:
|
||||
type: object
|
||||
description: "Reminder settings"
|
||||
transparency:
|
||||
type: string
|
||||
description: "Event transparency"
|
||||
visibility:
|
||||
type: string
|
||||
description: "Event visibility"
|
||||
calendar_id:
|
||||
type: string
|
||||
description: "Calendar containing this event"
|
||||
etag:
|
||||
type: string
|
||||
description: "Event ETag for change detection"
|
||||
next_page_token:
|
||||
type: string
|
||||
description: "Token for pagination"
|
||||
total_results:
|
||||
type: integer
|
||||
description: "Total number of matching events"
|
||||
required: ["events"]
|
||||
language: python
|
||||
source:
|
||||
file: ../python/google_calendar_client.py
|
||||
@@ -0,0 +1,97 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: search_events
|
||||
title: Search Calendar Events
|
||||
description: |
|
||||
Search for events matching a text query. Searches across event titles, descriptions,
|
||||
locations, and attendee information with optional time filtering.
|
||||
|
||||
Example usage:
|
||||
- "Find all meetings with John" (q: "John")
|
||||
- "Search for events about project Alpha" (q: "project Alpha")
|
||||
- "Find meetings in the conference room" (q: "conference room")
|
||||
- "Show me all standup meetings this month" (q: "standup" with time range)
|
||||
tags:
|
||||
- google-calendar
|
||||
- events
|
||||
- search
|
||||
annotations:
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters:
|
||||
- name: q
|
||||
type: string
|
||||
description: "Free text search query (searches title, description, location, attendees)"
|
||||
examples: ["John Smith", "project Alpha", "conference room", "standup meeting"]
|
||||
- name: calendar_id
|
||||
type: string
|
||||
description: "Calendar to search ('primary' or specific calendar ID)"
|
||||
default: "primary"
|
||||
examples: ["primary", "work@company.com", "team-calendar@company.com"]
|
||||
- name: time_min
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Earliest event start time to include (RFC3339 format)"
|
||||
default: null
|
||||
examples: ["2024-01-15T00:00:00Z", "2024-01-15T09:00:00-08:00"]
|
||||
- name: time_max
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Latest event start time to include (RFC3339 format)"
|
||||
default: null
|
||||
examples: ["2024-01-15T23:59:59Z", "2024-01-15T17:00:00-08:00"]
|
||||
- name: max_results
|
||||
type: integer
|
||||
description: "Maximum number of events to return"
|
||||
default: 250
|
||||
minimum: 1
|
||||
maximum: 2500
|
||||
examples: [10, 50, 250]
|
||||
- name: page_token
|
||||
type: string
|
||||
description: "Token for pagination"
|
||||
default: null
|
||||
examples: ["CAESGjBpNDd2Nmp2Zml2cXRwYjBpOXA", "next_page_token_example"]
|
||||
return:
|
||||
type: object
|
||||
description: "Search results with matching events"
|
||||
properties:
|
||||
events:
|
||||
type: array
|
||||
description: "Matching events"
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: "Event identifier"
|
||||
summary:
|
||||
type: string
|
||||
description: "Event title"
|
||||
start_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Event start time"
|
||||
end_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: "Event end time"
|
||||
location:
|
||||
type: string
|
||||
description: "Event location"
|
||||
description:
|
||||
type: string
|
||||
description: "Event description"
|
||||
htmlLink:
|
||||
type: string
|
||||
description: "Google Calendar web URL"
|
||||
next_page_token:
|
||||
type: string
|
||||
description: "Token for next page of results"
|
||||
total_results:
|
||||
type: integer
|
||||
description: "Number of results in current page"
|
||||
required: ["events"]
|
||||
language: python
|
||||
source:
|
||||
file: ../python/google_calendar_client.py
|
||||
@@ -0,0 +1,53 @@
|
||||
mxcp: 1
|
||||
tool:
|
||||
name: whoami
|
||||
title: Current User Information
|
||||
description: |
|
||||
Get the current authenticated user's information (id, email, name) from Google OAuth context.
|
||||
Use this tool to verify authentication status and get user profile data.
|
||||
|
||||
Example usage:
|
||||
- "Who am I logged in as?"
|
||||
- "What's my Google account information?"
|
||||
- "Show me my user profile"
|
||||
tags:
|
||||
- google-calendar
|
||||
- user
|
||||
- auth
|
||||
annotations:
|
||||
readOnlyHint: true
|
||||
idempotentHint: true
|
||||
parameters: []
|
||||
return:
|
||||
type: object
|
||||
description: Current user information from Google OAuth profile
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Google user ID (subject)
|
||||
email:
|
||||
type: string
|
||||
description: User's email address
|
||||
name:
|
||||
type: string
|
||||
description: User's full display name
|
||||
given_name:
|
||||
type: string
|
||||
description: User's first name
|
||||
family_name:
|
||||
type: string
|
||||
description: User's last name
|
||||
picture:
|
||||
type: string
|
||||
description: User's profile picture URL
|
||||
locale:
|
||||
type: string
|
||||
description: User's locale (e.g., 'en-US')
|
||||
verified_email:
|
||||
type: boolean
|
||||
description: Whether email address is verified
|
||||
required: ["id", "email", "name"]
|
||||
language: python
|
||||
source:
|
||||
file: ../python/google_calendar_client.py
|
||||
# NOTE: tests section omitted - OAuth tools cannot be tested via mxcp CLI
|
||||
Reference in New Issue
Block a user