refactor tools to consolidate all modify actions
This commit is contained in:
111
README.md
111
README.md
@@ -846,9 +846,7 @@ cp .env.oauth21 .env
|
|||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
| `list_calendars` | **Core** | List accessible calendars |
|
| `list_calendars` | **Core** | List accessible calendars |
|
||||||
| `get_events` | **Core** | Retrieve events with time range filtering |
|
| `get_events` | **Core** | Retrieve events with time range filtering |
|
||||||
| `create_event` | **Core** | Create events with attachments & reminders |
|
| `manage_event` | **Core** | Create, update, or delete calendar events |
|
||||||
| `modify_event` | **Core** | Update existing events |
|
|
||||||
| `delete_event` | Extended | Remove events |
|
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
@@ -863,15 +861,11 @@ cp .env.oauth21 .env
|
|||||||
| `create_drive_file` | **Core** | Create files or fetch from URLs |
|
| `create_drive_file` | **Core** | Create files or fetch from URLs |
|
||||||
| `create_drive_folder` | **Core** | Create empty folders in Drive or shared drives |
|
| `create_drive_folder` | **Core** | Create empty folders in Drive or shared drives |
|
||||||
| `import_to_google_doc` | **Core** | Import files (MD, DOCX, HTML, etc.) as Google Docs |
|
| `import_to_google_doc` | **Core** | Import files (MD, DOCX, HTML, etc.) as Google Docs |
|
||||||
| `share_drive_file` | **Core** | Share file with users/groups/domains/anyone |
|
|
||||||
| `get_drive_shareable_link` | **Core** | Get shareable links for a file |
|
| `get_drive_shareable_link` | **Core** | Get shareable links for a file |
|
||||||
| `list_drive_items` | Extended | List folder contents |
|
| `list_drive_items` | Extended | List folder contents |
|
||||||
| `copy_drive_file` | Extended | Copy existing files (templates) with optional renaming |
|
| `copy_drive_file` | Extended | Copy existing files (templates) with optional renaming |
|
||||||
| `update_drive_file` | Extended | Update file metadata, move between folders |
|
| `update_drive_file` | Extended | Update file metadata, move between folders |
|
||||||
| `batch_share_drive_file` | Extended | Share file with multiple recipients |
|
| `manage_drive_access` | Extended | Grant, update, revoke permissions, and transfer ownership |
|
||||||
| `update_drive_permission` | Extended | Modify permission role |
|
|
||||||
| `remove_drive_permission` | Extended | Revoke file access |
|
|
||||||
| `transfer_drive_ownership` | Extended | Transfer file ownership to another user |
|
|
||||||
| `set_drive_file_permissions` | Extended | Set link sharing and file-level sharing settings |
|
| `set_drive_file_permissions` | Extended | Set link sharing and file-level sharing settings |
|
||||||
| `get_drive_file_permissions` | Complete | Get detailed file permissions |
|
| `get_drive_file_permissions` | Complete | Get detailed file permissions |
|
||||||
| `check_drive_file_public_access` | Complete | Check public sharing status |
|
| `check_drive_file_public_access` | Complete | Check public sharing status |
|
||||||
@@ -894,7 +888,9 @@ cp .env.oauth21 .env
|
|||||||
| `get_gmail_thread_content` | Extended | Get full thread content |
|
| `get_gmail_thread_content` | Extended | Get full thread content |
|
||||||
| `modify_gmail_message_labels` | Extended | Modify message labels |
|
| `modify_gmail_message_labels` | Extended | Modify message labels |
|
||||||
| `list_gmail_labels` | Extended | List available labels |
|
| `list_gmail_labels` | Extended | List available labels |
|
||||||
|
| `list_gmail_filters` | Extended | List Gmail filters |
|
||||||
| `manage_gmail_label` | Extended | Create/update/delete labels |
|
| `manage_gmail_label` | Extended | Create/update/delete labels |
|
||||||
|
| `manage_gmail_filter` | Extended | Create or delete Gmail filters |
|
||||||
| `draft_gmail_message` | Extended | Create drafts |
|
| `draft_gmail_message` | Extended | Create drafts |
|
||||||
| `get_gmail_threads_content_batch` | Complete | Batch retrieve thread content |
|
| `get_gmail_threads_content_batch` | Complete | Batch retrieve thread content |
|
||||||
| `batch_modify_gmail_message_labels` | Complete | Batch modify labels |
|
| `batch_modify_gmail_message_labels` | Complete | Batch modify labels |
|
||||||
@@ -965,7 +961,8 @@ Saved files expire after 1 hour and are cleaned up automatically.
|
|||||||
| `export_doc_to_pdf` | Extended | Export document to PDF |
|
| `export_doc_to_pdf` | Extended | Export document to PDF |
|
||||||
| `create_table_with_data` | Complete | Create data tables |
|
| `create_table_with_data` | Complete | Create data tables |
|
||||||
| `debug_table_structure` | Complete | Debug table issues |
|
| `debug_table_structure` | Complete | Debug table issues |
|
||||||
| `*_document_comments` | Complete | Read, Reply, Create, Resolve |
|
| `list_document_comments` | Complete | List all document comments |
|
||||||
|
| `manage_document_comment` | Complete | Create, reply to, or resolve comments |
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -984,7 +981,9 @@ Saved files expire after 1 hour and are cleaned up automatically.
|
|||||||
| `get_spreadsheet_info` | Extended | Get spreadsheet metadata |
|
| `get_spreadsheet_info` | Extended | Get spreadsheet metadata |
|
||||||
| `format_sheet_range` | Extended | Apply colors, number formats, text wrapping, alignment, bold/italic, font size |
|
| `format_sheet_range` | Extended | Apply colors, number formats, text wrapping, alignment, bold/italic, font size |
|
||||||
| `create_sheet` | Complete | Add sheets to existing files |
|
| `create_sheet` | Complete | Add sheets to existing files |
|
||||||
| `*_sheet_comment` | Complete | Read/create/reply/resolve comments |
|
| `list_spreadsheet_comments` | Complete | List all spreadsheet comments |
|
||||||
|
| `manage_spreadsheet_comment` | Complete | Create, reply to, or resolve comments |
|
||||||
|
| `manage_conditional_formatting` | Complete | Add, update, or delete conditional formatting rules |
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td width="50%" valign="top">
|
<td width="50%" valign="top">
|
||||||
@@ -998,7 +997,8 @@ Saved files expire after 1 hour and are cleaned up automatically.
|
|||||||
| `batch_update_presentation` | Extended | Apply multiple updates |
|
| `batch_update_presentation` | Extended | Apply multiple updates |
|
||||||
| `get_page` | Extended | Get specific slide information |
|
| `get_page` | Extended | Get specific slide information |
|
||||||
| `get_page_thumbnail` | Extended | Generate slide thumbnails |
|
| `get_page_thumbnail` | Extended | Generate slide thumbnails |
|
||||||
| `*_presentation_comment` | Complete | Read/create/reply/resolve comments |
|
| `list_presentation_comments` | Complete | List all presentation comments |
|
||||||
|
| `manage_presentation_comment` | Complete | Create, reply to, or resolve comments |
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1025,12 +1025,10 @@ Saved files expire after 1 hour and are cleaned up automatically.
|
|||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
| `list_tasks` | **Core** | List tasks with filtering |
|
| `list_tasks` | **Core** | List tasks with filtering |
|
||||||
| `get_task` | **Core** | Retrieve task details |
|
| `get_task` | **Core** | Retrieve task details |
|
||||||
| `create_task` | **Core** | Create tasks with hierarchy |
|
| `manage_task` | **Core** | Create, update, delete, or move tasks |
|
||||||
| `update_task` | **Core** | Modify task properties |
|
| `list_task_lists` | Complete | List task lists |
|
||||||
| `delete_task` | Extended | Remove tasks |
|
| `get_task_list` | Complete | Get task list details |
|
||||||
| `move_task` | Complete | Reposition tasks |
|
| `manage_task_list` | Complete | Create, update, delete task lists, or clear completed tasks |
|
||||||
| `clear_completed_tasks` | Complete | Hide completed tasks |
|
|
||||||
| `*_task_list` | Complete | List/get/create/update/delete task lists |
|
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1044,14 +1042,11 @@ Saved files expire after 1 hour and are cleaned up automatically.
|
|||||||
| `search_contacts` | **Core** | Search contacts by name, email, phone |
|
| `search_contacts` | **Core** | Search contacts by name, email, phone |
|
||||||
| `get_contact` | **Core** | Retrieve detailed contact info |
|
| `get_contact` | **Core** | Retrieve detailed contact info |
|
||||||
| `list_contacts` | **Core** | List contacts with pagination |
|
| `list_contacts` | **Core** | List contacts with pagination |
|
||||||
| `create_contact` | **Core** | Create new contacts |
|
| `manage_contact` | **Core** | Create, update, or delete contacts |
|
||||||
| `update_contact` | Extended | Update existing contacts |
|
|
||||||
| `delete_contact` | Extended | Delete contacts |
|
|
||||||
| `list_contact_groups` | Extended | List contact groups/labels |
|
| `list_contact_groups` | Extended | List contact groups/labels |
|
||||||
| `get_contact_group` | Extended | Get group details with members |
|
| `get_contact_group` | Extended | Get group details with members |
|
||||||
| `batch_*_contacts` | Complete | Batch create/update/delete contacts |
|
| `manage_contacts_batch` | Complete | Batch create, update, or delete contacts |
|
||||||
| `*_contact_group` | Complete | Create/update/delete contact groups |
|
| `manage_contact_group` | Complete | Create, update, delete groups, or modify membership |
|
||||||
| `modify_contact_group_members` | Complete | Add/remove contacts from groups |
|
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1076,9 +1071,8 @@ Saved files expire after 1 hour and are cleaned up automatically.
|
|||||||
|
|
||||||
| Tool | Tier | Description |
|
| Tool | Tier | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
| `search_custom` | **Core** | Perform web searches |
|
| `search_custom` | **Core** | Perform web searches (supports site restrictions via sites parameter) |
|
||||||
| `get_search_engine_info` | Complete | Retrieve search engine metadata |
|
| `get_search_engine_info` | Complete | Retrieve search engine metadata |
|
||||||
| `search_custom_siterestrict` | Extended | Search within specific domains |
|
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1095,10 +1089,8 @@ Saved files expire after 1 hour and are cleaned up automatically.
|
|||||||
| `create_script_project` | **Core** | Create new standalone or bound project |
|
| `create_script_project` | **Core** | Create new standalone or bound project |
|
||||||
| `update_script_content` | **Core** | Update or create script files |
|
| `update_script_content` | **Core** | Update or create script files |
|
||||||
| `run_script_function` | **Core** | Execute function with parameters |
|
| `run_script_function` | **Core** | Execute function with parameters |
|
||||||
| `create_deployment` | Extended | Create new script deployment |
|
|
||||||
| `list_deployments` | Extended | List all project deployments |
|
| `list_deployments` | Extended | List all project deployments |
|
||||||
| `update_deployment` | Extended | Update deployment configuration |
|
| `manage_deployment` | Extended | Create, update, or delete script deployments |
|
||||||
| `delete_deployment` | Extended | Remove deployment |
|
|
||||||
| `list_script_processes` | Extended | View recent executions and status |
|
| `list_script_processes` | Extended | View recent executions and status |
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
@@ -1113,6 +1105,69 @@ Saved files expire after 1 hour and are cleaned up automatically.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 🔄 Tool Consolidation & Migration
|
||||||
|
|
||||||
|
**Tool Count Reduction**: This release consolidates 139 tools down to 111 tools (20% reduction) by combining CRUD operations into action-based `manage_*` tools.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Migration Mapping</b> <sub><sup>← Old tool → New tool mapping</sup></sub></summary>
|
||||||
|
|
||||||
|
| Old Tool | New Tool | Action Parameter |
|
||||||
|
|----------|----------|------------------|
|
||||||
|
| `create_event` | `manage_event` | `action="create"` |
|
||||||
|
| `modify_event` | `manage_event` | `action="update"` |
|
||||||
|
| `delete_event` | `manage_event` | `action="delete"` |
|
||||||
|
| `share_drive_file` | `manage_drive_access` | `action="grant"` |
|
||||||
|
| `batch_share_drive_file` | `manage_drive_access` | `action="grant_batch"` |
|
||||||
|
| `update_drive_permission` | `manage_drive_access` | `action="update"` |
|
||||||
|
| `remove_drive_permission` | `manage_drive_access` | `action="revoke"` |
|
||||||
|
| `transfer_drive_ownership` | `manage_drive_access` | `action="transfer_owner"` |
|
||||||
|
| `create_gmail_filter` | `manage_gmail_filter` | `action="create"` |
|
||||||
|
| `delete_gmail_filter` | `manage_gmail_filter` | `action="delete"` |
|
||||||
|
| `create_task` | `manage_task` | `action="create"` |
|
||||||
|
| `update_task` | `manage_task` | `action="update"` |
|
||||||
|
| `delete_task` | `manage_task` | `action="delete"` |
|
||||||
|
| `move_task` | `manage_task` | `action="move"` |
|
||||||
|
| `create_task_list` | `manage_task_list` | `action="create"` |
|
||||||
|
| `update_task_list` | `manage_task_list` | `action="update"` |
|
||||||
|
| `delete_task_list` | `manage_task_list` | `action="delete"` |
|
||||||
|
| `clear_completed_tasks` | `manage_task_list` | `action="clear_completed"` |
|
||||||
|
| `create_contact` | `manage_contact` | `action="create"` |
|
||||||
|
| `update_contact` | `manage_contact` | `action="update"` |
|
||||||
|
| `delete_contact` | `manage_contact` | `action="delete"` |
|
||||||
|
| `batch_create_contacts` | `manage_contacts_batch` | `action="create"` |
|
||||||
|
| `batch_update_contacts` | `manage_contacts_batch` | `action="update"` |
|
||||||
|
| `batch_delete_contacts` | `manage_contacts_batch` | `action="delete"` |
|
||||||
|
| `create_contact_group` | `manage_contact_group` | `action="create"` |
|
||||||
|
| `update_contact_group` | `manage_contact_group` | `action="update"` |
|
||||||
|
| `delete_contact_group` | `manage_contact_group` | `action="delete"` |
|
||||||
|
| `modify_contact_group_members` | `manage_contact_group` | `action="modify_members"` |
|
||||||
|
| `create_deployment` | `manage_deployment` | `action="create"` |
|
||||||
|
| `update_deployment` | `manage_deployment` | `action="update"` |
|
||||||
|
| `delete_deployment` | `manage_deployment` | `action="delete"` |
|
||||||
|
| `add_conditional_formatting` | `manage_conditional_formatting` | `action="add"` |
|
||||||
|
| `update_conditional_formatting` | `manage_conditional_formatting` | `action="update"` |
|
||||||
|
| `delete_conditional_formatting` | `manage_conditional_formatting` | `action="delete"` |
|
||||||
|
| `read_document_comments` | `list_document_comments` | N/A (renamed) |
|
||||||
|
| `create_document_comment` | `manage_document_comment` | `action="create"` |
|
||||||
|
| `reply_to_document_comment` | `manage_document_comment` | `action="reply"` |
|
||||||
|
| `resolve_document_comment` | `manage_document_comment` | `action="resolve"` |
|
||||||
|
| `read_spreadsheet_comments` | `list_spreadsheet_comments` | N/A (renamed) |
|
||||||
|
| `create_spreadsheet_comment` | `manage_spreadsheet_comment` | `action="create"` |
|
||||||
|
| `reply_to_spreadsheet_comment` | `manage_spreadsheet_comment` | `action="reply"` |
|
||||||
|
| `resolve_spreadsheet_comment` | `manage_spreadsheet_comment` | `action="resolve"` |
|
||||||
|
| `read_presentation_comments` | `list_presentation_comments` | N/A (renamed) |
|
||||||
|
| `create_presentation_comment` | `manage_presentation_comment` | `action="create"` |
|
||||||
|
| `reply_to_presentation_comment` | `manage_presentation_comment` | `action="reply"` |
|
||||||
|
| `resolve_presentation_comment` | `manage_presentation_comment` | `action="resolve"` |
|
||||||
|
| `search_custom_siterestrict` | `search_custom` | Use `sites` parameter |
|
||||||
|
|
||||||
|
**Breaking Change**: Legacy tools have been removed. Use the new consolidated tools with appropriate action parameters.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Connect to Claude Desktop
|
### Connect to Claude Desktop
|
||||||
|
|
||||||
The server supports two transport modes:
|
The server supports two transport modes:
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export OAUTHLIB_INSECURE_TRANSPORT=1 # Development only
|
|||||||
|
|
||||||
## 🛠 Tools Reference
|
## 🛠 Tools Reference
|
||||||
|
|
||||||
### Gmail (11 tools)
|
### Gmail (10 tools)
|
||||||
|
|
||||||
| Tool | Tier | Description |
|
| Tool | Tier | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
@@ -60,39 +60,42 @@ export OAUTHLIB_INSECURE_TRANSPORT=1 # Development only
|
|||||||
| `list_gmail_labels` | Extended | List all system and user labels |
|
| `list_gmail_labels` | Extended | List all system and user labels |
|
||||||
| `manage_gmail_label` | Extended | Create, update, delete labels |
|
| `manage_gmail_label` | Extended | Create, update, delete labels |
|
||||||
| `modify_gmail_message_labels` | Extended | Add/remove labels (archive, trash, etc.) |
|
| `modify_gmail_message_labels` | Extended | Add/remove labels (archive, trash, etc.) |
|
||||||
|
| `manage_gmail_filter` | Extended | Create or delete Gmail filters |
|
||||||
| `get_gmail_threads_content_batch` | Complete | Batch retrieve threads |
|
| `get_gmail_threads_content_batch` | Complete | Batch retrieve threads |
|
||||||
| `batch_modify_gmail_message_labels` | Complete | Bulk label operations |
|
| `batch_modify_gmail_message_labels` | Complete | Bulk label operations |
|
||||||
|
|
||||||
**Also includes:** `get_gmail_attachment_content`, `list_gmail_filters`, `create_gmail_filter`, `delete_gmail_filter`
|
**Also includes:** `get_gmail_attachment_content`, `list_gmail_filters`
|
||||||
|
|
||||||
### Google Drive (7 tools)
|
### Google Drive (10 tools)
|
||||||
|
|
||||||
| Tool | Tier | Description |
|
| Tool | Tier | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
| `search_drive_files` | Core | Search files with Drive query syntax or free text |
|
| `search_drive_files` | Core | Search files with Drive query syntax or free text |
|
||||||
| `get_drive_file_content` | Core | Read content from Docs, Sheets, Office files (.docx, .xlsx, .pptx) |
|
| `get_drive_file_content` | Core | Read content from Docs, Sheets, Office files (.docx, .xlsx, .pptx) |
|
||||||
|
| `get_drive_file_download_url` | Core | Download Drive files to local disk |
|
||||||
| `create_drive_file` | Core | Create files from content or URL (supports file://, http://, https://) |
|
| `create_drive_file` | Core | Create files from content or URL (supports file://, http://, https://) |
|
||||||
| `create_drive_folder` | Core | Create empty folders in Drive or shared drives |
|
| `create_drive_folder` | Core | Create empty folders in Drive or shared drives |
|
||||||
|
| `import_to_google_doc` | Core | Import files (MD, DOCX, HTML, etc.) as Google Docs |
|
||||||
|
| `get_drive_shareable_link` | Core | Get shareable links for a file |
|
||||||
| `list_drive_items` | Extended | List folder contents with shared drive support |
|
| `list_drive_items` | Extended | List folder contents with shared drive support |
|
||||||
|
| `copy_drive_file` | Extended | Copy existing files (templates) with optional renaming |
|
||||||
| `update_drive_file` | Extended | Update metadata, move between folders, star, trash |
|
| `update_drive_file` | Extended | Update metadata, move between folders, star, trash |
|
||||||
| `get_drive_file_permissions` | Complete | Check sharing status and permissions |
|
| `manage_drive_access` | Extended | Grant, update, revoke permissions, and transfer ownership |
|
||||||
|
| `set_drive_file_permissions` | Extended | Set link sharing and file-level sharing settings |
|
||||||
|
| `get_drive_file_permissions` | Complete | Get detailed file permissions |
|
||||||
| `check_drive_file_public_access` | Complete | Verify public link sharing for Docs image insertion |
|
| `check_drive_file_public_access` | Complete | Verify public link sharing for Docs image insertion |
|
||||||
|
|
||||||
**Also includes:** `get_drive_file_download_url` for generating download URLs
|
### Google Calendar (3 tools)
|
||||||
|
|
||||||
### Google Calendar (5 tools)
|
|
||||||
|
|
||||||
| Tool | Tier | Description |
|
| Tool | Tier | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
| `list_calendars` | Core | List all accessible calendars |
|
| `list_calendars` | Core | List all accessible calendars |
|
||||||
| `get_events` | Core | Query events by time range, search, or specific ID |
|
| `get_events` | Core | Query events by time range, search, or specific ID |
|
||||||
| `create_event` | Core | Create events with attendees, reminders, Google Meet, attachments |
|
| `manage_event` | Core | Create, update, or delete calendar events |
|
||||||
| `modify_event` | Core | Update any event property including conferencing |
|
|
||||||
| `delete_event` | Extended | Remove events |
|
|
||||||
|
|
||||||
**Event features:** Timezone support, transparency (busy/free), visibility settings, up to 5 custom reminders
|
**Event features:** Timezone support, transparency (busy/free), visibility settings, up to 5 custom reminders, Google Meet integration, attendees, attachments
|
||||||
|
|
||||||
### Google Docs (16 tools)
|
### Google Docs (14 tools)
|
||||||
|
|
||||||
| Tool | Tier | Description |
|
| Tool | Tier | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
@@ -103,6 +106,8 @@ export OAUTHLIB_INSECURE_TRANSPORT=1 # Development only
|
|||||||
| `find_and_replace_doc` | Extended | Global find/replace with case matching |
|
| `find_and_replace_doc` | Extended | Global find/replace with case matching |
|
||||||
| `list_docs_in_folder` | Extended | List Docs in a specific folder |
|
| `list_docs_in_folder` | Extended | List Docs in a specific folder |
|
||||||
| `insert_doc_elements` | Extended | Add tables, lists, page breaks |
|
| `insert_doc_elements` | Extended | Add tables, lists, page breaks |
|
||||||
|
| `update_paragraph_style` | Extended | Apply heading styles, lists (bulleted/numbered with nesting), and paragraph formatting |
|
||||||
|
| `get_doc_as_markdown` | Extended | Export document as formatted Markdown with optional comments |
|
||||||
| `export_doc_to_pdf` | Extended | Export to PDF and save to Drive |
|
| `export_doc_to_pdf` | Extended | Export to PDF and save to Drive |
|
||||||
| `insert_doc_image` | Complete | Insert images from Drive or URLs |
|
| `insert_doc_image` | Complete | Insert images from Drive or URLs |
|
||||||
| `update_doc_headers_footers` | Complete | Modify headers/footers |
|
| `update_doc_headers_footers` | Complete | Modify headers/footers |
|
||||||
@@ -110,10 +115,10 @@ export OAUTHLIB_INSECURE_TRANSPORT=1 # Development only
|
|||||||
| `inspect_doc_structure` | Complete | Analyze document structure for safe insertion points |
|
| `inspect_doc_structure` | Complete | Analyze document structure for safe insertion points |
|
||||||
| `create_table_with_data` | Complete | Create and populate tables in one operation |
|
| `create_table_with_data` | Complete | Create and populate tables in one operation |
|
||||||
| `debug_table_structure` | Complete | Debug table cell positions and content |
|
| `debug_table_structure` | Complete | Debug table cell positions and content |
|
||||||
|
| `list_document_comments` | Complete | List all document comments |
|
||||||
|
| `manage_document_comment` | Complete | Create, reply to, or resolve comments |
|
||||||
|
|
||||||
**Comments:** `read_document_comments`, `create_document_comment`, `reply_to_document_comment`, `resolve_document_comment`
|
### Google Sheets (9 tools)
|
||||||
|
|
||||||
### Google Sheets (13 tools)
|
|
||||||
|
|
||||||
| Tool | Tier | Description |
|
| Tool | Tier | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
@@ -124,13 +129,11 @@ export OAUTHLIB_INSECURE_TRANSPORT=1 # Development only
|
|||||||
| `get_spreadsheet_info` | Extended | Get metadata, sheets, conditional formats |
|
| `get_spreadsheet_info` | Extended | Get metadata, sheets, conditional formats |
|
||||||
| `format_sheet_range` | Extended | Apply colors, number formats, text wrapping, alignment, bold/italic, font size |
|
| `format_sheet_range` | Extended | Apply colors, number formats, text wrapping, alignment, bold/italic, font size |
|
||||||
| `create_sheet` | Complete | Add sheets to existing spreadsheets |
|
| `create_sheet` | Complete | Add sheets to existing spreadsheets |
|
||||||
| `add_conditional_formatting` | Complete | Add boolean or gradient rules |
|
| `list_spreadsheet_comments` | Complete | List all spreadsheet comments |
|
||||||
| `update_conditional_formatting` | Complete | Modify existing rules |
|
| `manage_spreadsheet_comment` | Complete | Create, reply to, or resolve comments |
|
||||||
| `delete_conditional_formatting` | Complete | Remove formatting rules |
|
| `manage_conditional_formatting` | Complete | Add, update, or delete conditional formatting rules |
|
||||||
|
|
||||||
**Comments:** `read_spreadsheet_comments`, `create_spreadsheet_comment`, `reply_to_spreadsheet_comment`, `resolve_spreadsheet_comment`
|
### Google Slides (7 tools)
|
||||||
|
|
||||||
### Google Slides (9 tools)
|
|
||||||
|
|
||||||
| Tool | Tier | Description |
|
| Tool | Tier | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
@@ -139,8 +142,8 @@ export OAUTHLIB_INSECURE_TRANSPORT=1 # Development only
|
|||||||
| `batch_update_presentation` | Extended | Apply multiple updates (create slides, shapes, etc.) |
|
| `batch_update_presentation` | Extended | Apply multiple updates (create slides, shapes, etc.) |
|
||||||
| `get_page` | Extended | Get specific slide details and elements |
|
| `get_page` | Extended | Get specific slide details and elements |
|
||||||
| `get_page_thumbnail` | Extended | Generate PNG thumbnails |
|
| `get_page_thumbnail` | Extended | Generate PNG thumbnails |
|
||||||
|
| `list_presentation_comments` | Complete | List all presentation comments |
|
||||||
**Comments:** `read_presentation_comments`, `create_presentation_comment`, `reply_to_presentation_comment`, `resolve_presentation_comment`
|
| `manage_presentation_comment` | Complete | Create, reply to, or resolve comments |
|
||||||
|
|
||||||
### Google Forms (6 tools)
|
### Google Forms (6 tools)
|
||||||
|
|
||||||
@@ -153,24 +156,18 @@ export OAUTHLIB_INSECURE_TRANSPORT=1 # Development only
|
|||||||
| `get_form_response` | Complete | Get individual response details |
|
| `get_form_response` | Complete | Get individual response details |
|
||||||
| `batch_update_form` | Complete | Execute batch updates to forms (questions, items, settings) |
|
| `batch_update_form` | Complete | Execute batch updates to forms (questions, items, settings) |
|
||||||
|
|
||||||
### Google Tasks (12 tools)
|
### Google Tasks (5 tools)
|
||||||
|
|
||||||
| Tool | Tier | Description |
|
| Tool | Tier | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
| `list_tasks` | Core | List tasks with filtering, subtask hierarchy preserved |
|
| `list_tasks` | Core | List tasks with filtering, subtask hierarchy preserved |
|
||||||
| `get_task` | Core | Get task details |
|
| `get_task` | Core | Get task details |
|
||||||
| `create_task` | Core | Create tasks with notes, due dates, parent/sibling positioning |
|
| `manage_task` | Core | Create, update, delete, or move tasks |
|
||||||
| `update_task` | Core | Update task properties |
|
|
||||||
| `delete_task` | Extended | Remove tasks |
|
|
||||||
| `list_task_lists` | Complete | List all task lists |
|
| `list_task_lists` | Complete | List all task lists |
|
||||||
| `get_task_list` | Complete | Get task list details |
|
| `get_task_list` | Complete | Get task list details |
|
||||||
| `create_task_list` | Complete | Create new task lists |
|
| `manage_task_list` | Complete | Create, update, delete task lists, or clear completed tasks |
|
||||||
| `update_task_list` | Complete | Rename task lists |
|
|
||||||
| `delete_task_list` | Complete | Delete task lists (and all tasks) |
|
|
||||||
| `move_task` | Complete | Reposition or move between lists |
|
|
||||||
| `clear_completed_tasks` | Complete | Hide completed tasks |
|
|
||||||
|
|
||||||
### Google Apps Script (11 tools)
|
### Google Apps Script (9 tools)
|
||||||
|
|
||||||
| Tool | Tier | Description |
|
| Tool | Tier | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
@@ -180,16 +177,27 @@ export OAUTHLIB_INSECURE_TRANSPORT=1 # Development only
|
|||||||
| `create_script_project` | Core | Create new standalone or bound project |
|
| `create_script_project` | Core | Create new standalone or bound project |
|
||||||
| `update_script_content` | Core | Update or create script files |
|
| `update_script_content` | Core | Update or create script files |
|
||||||
| `run_script_function` | Core | Execute function with parameters |
|
| `run_script_function` | Core | Execute function with parameters |
|
||||||
| `create_deployment` | Extended | Create new script deployment |
|
|
||||||
| `list_deployments` | Extended | List all project deployments |
|
| `list_deployments` | Extended | List all project deployments |
|
||||||
| `update_deployment` | Extended | Update deployment configuration |
|
| `manage_deployment` | Extended | Create, update, or delete script deployments |
|
||||||
| `delete_deployment` | Extended | Remove deployment |
|
|
||||||
| `list_script_processes` | Extended | View recent executions and status |
|
| `list_script_processes` | Extended | View recent executions and status |
|
||||||
|
|
||||||
**Enables:** Cross-app automation, persistent workflows, custom business logic execution, script development and debugging
|
**Enables:** Cross-app automation, persistent workflows, custom business logic execution, script development and debugging
|
||||||
|
|
||||||
**Note:** Trigger management is not currently supported via MCP tools.
|
**Note:** Trigger management is not currently supported via MCP tools.
|
||||||
|
|
||||||
|
### Google Contacts (7 tools)
|
||||||
|
|
||||||
|
| Tool | Tier | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| `search_contacts` | Core | Search contacts by name, email, phone |
|
||||||
|
| `get_contact` | Core | Retrieve detailed contact info |
|
||||||
|
| `list_contacts` | Core | List contacts with pagination |
|
||||||
|
| `manage_contact` | Core | Create, update, or delete contacts |
|
||||||
|
| `list_contact_groups` | Extended | List contact groups/labels |
|
||||||
|
| `get_contact_group` | Extended | Get group details with members |
|
||||||
|
| `manage_contacts_batch` | Complete | Batch create, update, or delete contacts |
|
||||||
|
| `manage_contact_group` | Complete | Create, update, delete groups, or modify membership |
|
||||||
|
|
||||||
### Google Chat (4 tools)
|
### Google Chat (4 tools)
|
||||||
|
|
||||||
| Tool | Tier | Description |
|
| Tool | Tier | Description |
|
||||||
@@ -199,12 +207,11 @@ export OAUTHLIB_INSECURE_TRANSPORT=1 # Development only
|
|||||||
| `search_messages` | Core | Search across chat history |
|
| `search_messages` | Core | Search across chat history |
|
||||||
| `list_spaces` | Extended | List rooms and DMs |
|
| `list_spaces` | Extended | List rooms and DMs |
|
||||||
|
|
||||||
### Google Custom Search (3 tools)
|
### Google Custom Search (2 tools)
|
||||||
|
|
||||||
| Tool | Tier | Description |
|
| Tool | Tier | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
| `search_custom` | Core | Web search with filters (date, file type, language, safe search) |
|
| `search_custom` | Core | Web search with filters (date, file type, language, safe search, site restrictions via sites parameter) |
|
||||||
| `search_custom_siterestrict` | Extended | Search within specific domains |
|
|
||||||
| `get_search_engine_info` | Complete | Get search engine metadata |
|
| `get_search_engine_info` | Complete | Get search engine metadata |
|
||||||
|
|
||||||
**Requires:** `GOOGLE_PSE_API_KEY` and `GOOGLE_PSE_ENGINE_ID` environment variables
|
**Requires:** `GOOGLE_PSE_API_KEY` and `GOOGLE_PSE_ENGINE_ID` environment variables
|
||||||
@@ -219,7 +226,7 @@ Choose a tier based on your needs:
|
|||||||
|------|-------|----------|
|
|------|-------|----------|
|
||||||
| **Core** | ~30 | Essential operations: search, read, create, send |
|
| **Core** | ~30 | Essential operations: search, read, create, send |
|
||||||
| **Extended** | ~50 | Core + management: labels, folders, batch ops |
|
| **Extended** | ~50 | Core + management: labels, folders, batch ops |
|
||||||
| **Complete** | ~80 | Full API: comments, headers, admin functions |
|
| **Complete** | 111 | Full API: comments, headers, admin functions |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx workspace-mcp --tool-tier core # Start minimal
|
uvx workspace-mcp --tool-tier core # Start minimal
|
||||||
|
|||||||
199
core/comments.py
199
core/comments.py
@@ -7,7 +7,7 @@ All Google Workspace apps (Docs, Sheets, Slides) use the Drive API for comment o
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from auth.service_decorator import require_google_service
|
from auth.service_decorator import require_google_service
|
||||||
from core.server import server
|
from core.server import server
|
||||||
@@ -16,6 +16,28 @@ from core.utils import handle_http_errors
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def _manage_comment_dispatch(
|
||||||
|
service, app_name: str, file_id: str, action: str,
|
||||||
|
comment_content: Optional[str] = None, comment_id: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Route comment management actions to the appropriate implementation."""
|
||||||
|
action_lower = action.lower().strip()
|
||||||
|
if action_lower == "create":
|
||||||
|
if not comment_content:
|
||||||
|
raise ValueError("comment_content is required for create action")
|
||||||
|
return await _create_comment_impl(service, app_name, file_id, comment_content)
|
||||||
|
elif action_lower == "reply":
|
||||||
|
if not comment_id or not comment_content:
|
||||||
|
raise ValueError("comment_id and comment_content are required for reply action")
|
||||||
|
return await _reply_to_comment_impl(service, app_name, file_id, comment_id, comment_content)
|
||||||
|
elif action_lower == "resolve":
|
||||||
|
if not comment_id:
|
||||||
|
raise ValueError("comment_id is required for resolve action")
|
||||||
|
return await _resolve_comment_impl(service, app_name, file_id, comment_id)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid action '{action_lower}'. Must be 'create', 'reply', or 'resolve'.")
|
||||||
|
|
||||||
|
|
||||||
def create_comment_tools(app_name: str, file_id_param: str):
|
def create_comment_tools(app_name: str, file_id_param: str):
|
||||||
"""
|
"""
|
||||||
Factory function to create comment management tools for a specific Google Workspace app.
|
Factory function to create comment management tools for a specific Google Workspace app.
|
||||||
@@ -25,165 +47,102 @@ def create_comment_tools(app_name: str, file_id_param: str):
|
|||||||
file_id_param: Parameter name for the file ID (e.g., "document_id", "spreadsheet_id", "presentation_id")
|
file_id_param: Parameter name for the file ID (e.g., "document_id", "spreadsheet_id", "presentation_id")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict containing the four comment management functions with unique names
|
Dict containing the comment management functions with unique names
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Create unique function names based on the app type
|
# --- Consolidated tools ---
|
||||||
read_func_name = f"read_{app_name}_comments"
|
list_func_name = f"list_{app_name}_comments"
|
||||||
create_func_name = f"create_{app_name}_comment"
|
manage_func_name = f"manage_{app_name}_comment"
|
||||||
reply_func_name = f"reply_to_{app_name}_comment"
|
|
||||||
resolve_func_name = f"resolve_{app_name}_comment"
|
|
||||||
|
|
||||||
# Create functions without decorators first, then apply decorators with proper names
|
|
||||||
if file_id_param == "document_id":
|
if file_id_param == "document_id":
|
||||||
|
|
||||||
@require_google_service("drive", "drive_read")
|
@require_google_service("drive", "drive_read")
|
||||||
@handle_http_errors(read_func_name, service_type="drive")
|
@handle_http_errors(list_func_name, service_type="drive")
|
||||||
async def read_comments(
|
async def list_comments(
|
||||||
service, user_google_email: str, document_id: str
|
service, user_google_email: str, document_id: str
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Read all comments from a Google Document."""
|
"""List all comments from a Google Document."""
|
||||||
return await _read_comments_impl(service, app_name, document_id)
|
return await _read_comments_impl(service, app_name, document_id)
|
||||||
|
|
||||||
@require_google_service("drive", "drive_file")
|
@require_google_service("drive", "drive_file")
|
||||||
@handle_http_errors(create_func_name, service_type="drive")
|
@handle_http_errors(manage_func_name, service_type="drive")
|
||||||
async def create_comment(
|
async def manage_comment(
|
||||||
service, user_google_email: str, document_id: str, comment_content: str
|
service, user_google_email: str, document_id: str, action: str,
|
||||||
|
comment_content: Optional[str] = None, comment_id: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a new comment on a Google Document."""
|
"""Manage comments on a Google Document.
|
||||||
return await _create_comment_impl(
|
|
||||||
service, app_name, document_id, comment_content
|
|
||||||
)
|
|
||||||
|
|
||||||
@require_google_service("drive", "drive_file")
|
Actions:
|
||||||
@handle_http_errors(reply_func_name, service_type="drive")
|
- create: Create a new comment. Requires comment_content.
|
||||||
async def reply_to_comment(
|
- reply: Reply to a comment. Requires comment_id and comment_content.
|
||||||
service,
|
- resolve: Resolve a comment. Requires comment_id.
|
||||||
user_google_email: str,
|
"""
|
||||||
document_id: str,
|
return await _manage_comment_dispatch(
|
||||||
comment_id: str,
|
service, app_name, document_id, action, comment_content, comment_id
|
||||||
reply_content: str,
|
|
||||||
) -> str:
|
|
||||||
"""Reply to a specific comment in a Google Document."""
|
|
||||||
return await _reply_to_comment_impl(
|
|
||||||
service, app_name, document_id, comment_id, reply_content
|
|
||||||
)
|
|
||||||
|
|
||||||
@require_google_service("drive", "drive_file")
|
|
||||||
@handle_http_errors(resolve_func_name, service_type="drive")
|
|
||||||
async def resolve_comment(
|
|
||||||
service, user_google_email: str, document_id: str, comment_id: str
|
|
||||||
) -> str:
|
|
||||||
"""Resolve a comment in a Google Document."""
|
|
||||||
return await _resolve_comment_impl(
|
|
||||||
service, app_name, document_id, comment_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif file_id_param == "spreadsheet_id":
|
elif file_id_param == "spreadsheet_id":
|
||||||
|
|
||||||
@require_google_service("drive", "drive_read")
|
@require_google_service("drive", "drive_read")
|
||||||
@handle_http_errors(read_func_name, service_type="drive")
|
@handle_http_errors(list_func_name, service_type="drive")
|
||||||
async def read_comments(
|
async def list_comments(
|
||||||
service, user_google_email: str, spreadsheet_id: str
|
service, user_google_email: str, spreadsheet_id: str
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Read all comments from a Google Spreadsheet."""
|
"""List all comments from a Google Spreadsheet."""
|
||||||
return await _read_comments_impl(service, app_name, spreadsheet_id)
|
return await _read_comments_impl(service, app_name, spreadsheet_id)
|
||||||
|
|
||||||
@require_google_service("drive", "drive_file")
|
@require_google_service("drive", "drive_file")
|
||||||
@handle_http_errors(create_func_name, service_type="drive")
|
@handle_http_errors(manage_func_name, service_type="drive")
|
||||||
async def create_comment(
|
async def manage_comment(
|
||||||
service, user_google_email: str, spreadsheet_id: str, comment_content: str
|
service, user_google_email: str, spreadsheet_id: str, action: str,
|
||||||
|
comment_content: Optional[str] = None, comment_id: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a new comment on a Google Spreadsheet."""
|
"""Manage comments on a Google Spreadsheet.
|
||||||
return await _create_comment_impl(
|
|
||||||
service, app_name, spreadsheet_id, comment_content
|
|
||||||
)
|
|
||||||
|
|
||||||
@require_google_service("drive", "drive_file")
|
Actions:
|
||||||
@handle_http_errors(reply_func_name, service_type="drive")
|
- create: Create a new comment. Requires comment_content.
|
||||||
async def reply_to_comment(
|
- reply: Reply to a comment. Requires comment_id and comment_content.
|
||||||
service,
|
- resolve: Resolve a comment. Requires comment_id.
|
||||||
user_google_email: str,
|
"""
|
||||||
spreadsheet_id: str,
|
return await _manage_comment_dispatch(
|
||||||
comment_id: str,
|
service, app_name, spreadsheet_id, action, comment_content, comment_id
|
||||||
reply_content: str,
|
|
||||||
) -> str:
|
|
||||||
"""Reply to a specific comment in a Google Spreadsheet."""
|
|
||||||
return await _reply_to_comment_impl(
|
|
||||||
service, app_name, spreadsheet_id, comment_id, reply_content
|
|
||||||
)
|
|
||||||
|
|
||||||
@require_google_service("drive", "drive_file")
|
|
||||||
@handle_http_errors(resolve_func_name, service_type="drive")
|
|
||||||
async def resolve_comment(
|
|
||||||
service, user_google_email: str, spreadsheet_id: str, comment_id: str
|
|
||||||
) -> str:
|
|
||||||
"""Resolve a comment in a Google Spreadsheet."""
|
|
||||||
return await _resolve_comment_impl(
|
|
||||||
service, app_name, spreadsheet_id, comment_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif file_id_param == "presentation_id":
|
elif file_id_param == "presentation_id":
|
||||||
|
|
||||||
@require_google_service("drive", "drive_read")
|
@require_google_service("drive", "drive_read")
|
||||||
@handle_http_errors(read_func_name, service_type="drive")
|
@handle_http_errors(list_func_name, service_type="drive")
|
||||||
async def read_comments(
|
async def list_comments(
|
||||||
service, user_google_email: str, presentation_id: str
|
service, user_google_email: str, presentation_id: str
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Read all comments from a Google Presentation."""
|
"""List all comments from a Google Presentation."""
|
||||||
return await _read_comments_impl(service, app_name, presentation_id)
|
return await _read_comments_impl(service, app_name, presentation_id)
|
||||||
|
|
||||||
@require_google_service("drive", "drive_file")
|
@require_google_service("drive", "drive_file")
|
||||||
@handle_http_errors(create_func_name, service_type="drive")
|
@handle_http_errors(manage_func_name, service_type="drive")
|
||||||
async def create_comment(
|
async def manage_comment(
|
||||||
service, user_google_email: str, presentation_id: str, comment_content: str
|
service, user_google_email: str, presentation_id: str, action: str,
|
||||||
|
comment_content: Optional[str] = None, comment_id: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a new comment on a Google Presentation."""
|
"""Manage comments on a Google Presentation.
|
||||||
return await _create_comment_impl(
|
|
||||||
service, app_name, presentation_id, comment_content
|
Actions:
|
||||||
|
- create: Create a new comment. Requires comment_content.
|
||||||
|
- reply: Reply to a comment. Requires comment_id and comment_content.
|
||||||
|
- resolve: Resolve a comment. Requires comment_id.
|
||||||
|
"""
|
||||||
|
return await _manage_comment_dispatch(
|
||||||
|
service, app_name, presentation_id, action, comment_content, comment_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@require_google_service("drive", "drive_file")
|
list_comments.__name__ = list_func_name
|
||||||
@handle_http_errors(reply_func_name, service_type="drive")
|
manage_comment.__name__ = manage_func_name
|
||||||
async def reply_to_comment(
|
server.tool()(list_comments)
|
||||||
service,
|
server.tool()(manage_comment)
|
||||||
user_google_email: str,
|
|
||||||
presentation_id: str,
|
|
||||||
comment_id: str,
|
|
||||||
reply_content: str,
|
|
||||||
) -> str:
|
|
||||||
"""Reply to a specific comment in a Google Presentation."""
|
|
||||||
return await _reply_to_comment_impl(
|
|
||||||
service, app_name, presentation_id, comment_id, reply_content
|
|
||||||
)
|
|
||||||
|
|
||||||
@require_google_service("drive", "drive_file")
|
|
||||||
@handle_http_errors(resolve_func_name, service_type="drive")
|
|
||||||
async def resolve_comment(
|
|
||||||
service, user_google_email: str, presentation_id: str, comment_id: str
|
|
||||||
) -> str:
|
|
||||||
"""Resolve a comment in a Google Presentation."""
|
|
||||||
return await _resolve_comment_impl(
|
|
||||||
service, app_name, presentation_id, comment_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set the proper function names and register with server
|
|
||||||
read_comments.__name__ = read_func_name
|
|
||||||
create_comment.__name__ = create_func_name
|
|
||||||
reply_to_comment.__name__ = reply_func_name
|
|
||||||
resolve_comment.__name__ = resolve_func_name
|
|
||||||
|
|
||||||
# Register tools with the server using the proper names
|
|
||||||
server.tool()(read_comments)
|
|
||||||
server.tool()(create_comment)
|
|
||||||
server.tool()(reply_to_comment)
|
|
||||||
server.tool()(resolve_comment)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"read_comments": read_comments,
|
"list_comments": list_comments,
|
||||||
"create_comment": create_comment,
|
"manage_comment": manage_comment,
|
||||||
"reply_to_comment": reply_to_comment,
|
|
||||||
"resolve_comment": resolve_comment,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ gmail:
|
|||||||
- manage_gmail_label
|
- manage_gmail_label
|
||||||
- draft_gmail_message
|
- draft_gmail_message
|
||||||
- list_gmail_filters
|
- list_gmail_filters
|
||||||
- create_gmail_filter
|
- manage_gmail_filter
|
||||||
- delete_gmail_filter
|
|
||||||
|
|
||||||
complete:
|
complete:
|
||||||
- get_gmail_threads_content_batch
|
- get_gmail_threads_content_batch
|
||||||
@@ -29,16 +28,12 @@ drive:
|
|||||||
- create_drive_file
|
- create_drive_file
|
||||||
- create_drive_folder
|
- create_drive_folder
|
||||||
- import_to_google_doc
|
- import_to_google_doc
|
||||||
- share_drive_file
|
|
||||||
- get_drive_shareable_link
|
- get_drive_shareable_link
|
||||||
extended:
|
extended:
|
||||||
- list_drive_items
|
- list_drive_items
|
||||||
- copy_drive_file
|
- copy_drive_file
|
||||||
- update_drive_file
|
- update_drive_file
|
||||||
- update_drive_permission
|
- manage_drive_access
|
||||||
- remove_drive_permission
|
|
||||||
- transfer_drive_ownership
|
|
||||||
- batch_share_drive_file
|
|
||||||
- set_drive_file_permissions
|
- set_drive_file_permissions
|
||||||
complete:
|
complete:
|
||||||
- get_drive_file_permissions
|
- get_drive_file_permissions
|
||||||
@@ -48,10 +43,8 @@ calendar:
|
|||||||
core:
|
core:
|
||||||
- list_calendars
|
- list_calendars
|
||||||
- get_events
|
- get_events
|
||||||
- create_event
|
- manage_event
|
||||||
- modify_event
|
|
||||||
extended:
|
extended:
|
||||||
- delete_event
|
|
||||||
- query_freebusy
|
- query_freebusy
|
||||||
complete: []
|
complete: []
|
||||||
|
|
||||||
@@ -75,10 +68,8 @@ docs:
|
|||||||
- inspect_doc_structure
|
- inspect_doc_structure
|
||||||
- create_table_with_data
|
- create_table_with_data
|
||||||
- debug_table_structure
|
- debug_table_structure
|
||||||
- read_document_comments
|
- list_document_comments
|
||||||
- create_document_comment
|
- manage_document_comment
|
||||||
- reply_to_document_comment
|
|
||||||
- resolve_document_comment
|
|
||||||
|
|
||||||
sheets:
|
sheets:
|
||||||
core:
|
core:
|
||||||
@@ -91,10 +82,9 @@ sheets:
|
|||||||
- format_sheet_range
|
- format_sheet_range
|
||||||
complete:
|
complete:
|
||||||
- create_sheet
|
- create_sheet
|
||||||
- read_spreadsheet_comments
|
- list_spreadsheet_comments
|
||||||
- create_spreadsheet_comment
|
- manage_spreadsheet_comment
|
||||||
- reply_to_spreadsheet_comment
|
- manage_conditional_formatting
|
||||||
- resolve_spreadsheet_comment
|
|
||||||
|
|
||||||
chat:
|
chat:
|
||||||
core:
|
core:
|
||||||
@@ -127,53 +117,37 @@ slides:
|
|||||||
- get_page
|
- get_page
|
||||||
- get_page_thumbnail
|
- get_page_thumbnail
|
||||||
complete:
|
complete:
|
||||||
- read_presentation_comments
|
- list_presentation_comments
|
||||||
- create_presentation_comment
|
- manage_presentation_comment
|
||||||
- reply_to_presentation_comment
|
|
||||||
- resolve_presentation_comment
|
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
core:
|
core:
|
||||||
- get_task
|
- get_task
|
||||||
- list_tasks
|
- list_tasks
|
||||||
- create_task
|
- manage_task
|
||||||
- update_task
|
extended: []
|
||||||
extended:
|
|
||||||
- delete_task
|
|
||||||
complete:
|
complete:
|
||||||
- list_task_lists
|
- list_task_lists
|
||||||
- get_task_list
|
- get_task_list
|
||||||
- create_task_list
|
- manage_task_list
|
||||||
- update_task_list
|
|
||||||
- delete_task_list
|
|
||||||
- move_task
|
|
||||||
- clear_completed_tasks
|
|
||||||
|
|
||||||
contacts:
|
contacts:
|
||||||
core:
|
core:
|
||||||
- search_contacts
|
- search_contacts
|
||||||
- get_contact
|
- get_contact
|
||||||
- list_contacts
|
- list_contacts
|
||||||
- create_contact
|
- manage_contact
|
||||||
extended:
|
extended:
|
||||||
- update_contact
|
|
||||||
- delete_contact
|
|
||||||
- list_contact_groups
|
- list_contact_groups
|
||||||
- get_contact_group
|
- get_contact_group
|
||||||
complete:
|
complete:
|
||||||
- batch_create_contacts
|
- manage_contacts_batch
|
||||||
- batch_update_contacts
|
- manage_contact_group
|
||||||
- batch_delete_contacts
|
|
||||||
- create_contact_group
|
|
||||||
- update_contact_group
|
|
||||||
- delete_contact_group
|
|
||||||
- modify_contact_group_members
|
|
||||||
|
|
||||||
search:
|
search:
|
||||||
core:
|
core:
|
||||||
- search_custom
|
- search_custom
|
||||||
extended:
|
extended: []
|
||||||
- search_custom_siterestrict
|
|
||||||
complete:
|
complete:
|
||||||
- get_search_engine_info
|
- get_search_engine_info
|
||||||
|
|
||||||
@@ -187,10 +161,8 @@ appscript:
|
|||||||
- run_script_function
|
- run_script_function
|
||||||
- generate_trigger_code
|
- generate_trigger_code
|
||||||
extended:
|
extended:
|
||||||
- create_deployment
|
- manage_deployment
|
||||||
- list_deployments
|
- list_deployments
|
||||||
- update_deployment
|
|
||||||
- delete_deployment
|
|
||||||
- delete_script_project
|
- delete_script_project
|
||||||
- list_versions
|
- list_versions
|
||||||
- create_version
|
- create_version
|
||||||
|
|||||||
@@ -464,31 +464,55 @@ async def _create_deployment_impl(
|
|||||||
|
|
||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
@handle_http_errors("create_deployment", service_type="script")
|
@handle_http_errors("manage_deployment", service_type="script")
|
||||||
@require_google_service("script", "script_deployments")
|
@require_google_service("script", "script_deployments")
|
||||||
async def create_deployment(
|
async def manage_deployment(
|
||||||
service: Any,
|
service: Any,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
|
action: str,
|
||||||
script_id: str,
|
script_id: str,
|
||||||
description: str,
|
deployment_id: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
version_description: Optional[str] = None,
|
version_description: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Creates a new deployment of the script.
|
Manages Apps Script deployments. Supports creating, updating, and deleting deployments.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
service: Injected Google API service client
|
service: Injected Google API service client
|
||||||
user_google_email: User's email address
|
user_google_email: User's email address
|
||||||
|
action: Action to perform - "create", "update", or "delete"
|
||||||
script_id: The script project ID
|
script_id: The script project ID
|
||||||
description: Deployment description
|
deployment_id: The deployment ID (required for update and delete)
|
||||||
version_description: Optional version description
|
description: Deployment description (required for create, optional for update)
|
||||||
|
version_description: Optional version description (for create only)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Formatted string with deployment details
|
str: Formatted string with deployment details or confirmation
|
||||||
"""
|
"""
|
||||||
|
action = action.lower().strip()
|
||||||
|
if action == "create":
|
||||||
|
if not description:
|
||||||
|
raise ValueError("description is required for create action")
|
||||||
return await _create_deployment_impl(
|
return await _create_deployment_impl(
|
||||||
service, user_google_email, script_id, description, version_description
|
service, user_google_email, script_id, description, version_description
|
||||||
)
|
)
|
||||||
|
elif action == "update":
|
||||||
|
if not deployment_id:
|
||||||
|
raise ValueError("deployment_id is required for update action")
|
||||||
|
return await _update_deployment_impl(
|
||||||
|
service, user_google_email, script_id, deployment_id, description
|
||||||
|
)
|
||||||
|
elif action == "delete":
|
||||||
|
if not deployment_id:
|
||||||
|
raise ValueError("deployment_id is required for delete action")
|
||||||
|
return await _delete_deployment_impl(
|
||||||
|
service, user_google_email, script_id, deployment_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid action '{action}'. Must be 'create', 'update', or 'delete'.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def _list_deployments_impl(
|
async def _list_deployments_impl(
|
||||||
@@ -578,32 +602,6 @@ async def _update_deployment_impl(
|
|||||||
return "\n".join(output)
|
return "\n".join(output)
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@handle_http_errors("update_deployment", service_type="script")
|
|
||||||
@require_google_service("script", "script_deployments")
|
|
||||||
async def update_deployment(
|
|
||||||
service: Any,
|
|
||||||
user_google_email: str,
|
|
||||||
script_id: str,
|
|
||||||
deployment_id: str,
|
|
||||||
description: Optional[str] = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Updates an existing deployment configuration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
service: Injected Google API service client
|
|
||||||
user_google_email: User's email address
|
|
||||||
script_id: The script project ID
|
|
||||||
deployment_id: The deployment ID to update
|
|
||||||
description: Optional new description
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Formatted string confirming update
|
|
||||||
"""
|
|
||||||
return await _update_deployment_impl(
|
|
||||||
service, user_google_email, script_id, deployment_id, description
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _delete_deployment_impl(
|
async def _delete_deployment_impl(
|
||||||
@@ -630,30 +628,6 @@ async def _delete_deployment_impl(
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@handle_http_errors("delete_deployment", service_type="script")
|
|
||||||
@require_google_service("script", "script_deployments")
|
|
||||||
async def delete_deployment(
|
|
||||||
service: Any,
|
|
||||||
user_google_email: str,
|
|
||||||
script_id: str,
|
|
||||||
deployment_id: str,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Deletes a deployment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
service: Injected Google API service client
|
|
||||||
user_google_email: User's email address
|
|
||||||
script_id: The script project ID
|
|
||||||
deployment_id: The deployment ID to delete
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message
|
|
||||||
"""
|
|
||||||
return await _delete_deployment_impl(
|
|
||||||
service, user_google_email, script_id, deployment_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _list_script_processes_impl(
|
async def _list_script_processes_impl(
|
||||||
|
|||||||
@@ -534,10 +534,15 @@ async def get_events(
|
|||||||
return text_output
|
return text_output
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@handle_http_errors("create_event", service_type="calendar")
|
# ---------------------------------------------------------------------------
|
||||||
@require_google_service("calendar", "calendar_events")
|
# Internal implementation functions for event create/modify/delete.
|
||||||
async def create_event(
|
# These are called by both the consolidated ``manage_event`` tool and the
|
||||||
|
# legacy single-action tools.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_event_impl(
|
||||||
service,
|
service,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
summary: str,
|
summary: str,
|
||||||
@@ -558,32 +563,7 @@ async def create_event(
|
|||||||
guests_can_invite_others: Optional[bool] = None,
|
guests_can_invite_others: Optional[bool] = None,
|
||||||
guests_can_see_other_guests: Optional[bool] = None,
|
guests_can_see_other_guests: Optional[bool] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Internal implementation for creating a calendar event."""
|
||||||
Creates a new event.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
summary (str): Event title.
|
|
||||||
start_time (str): Start time (RFC3339, e.g., "2023-10-27T10:00:00-07:00" or "2023-10-27" for all-day).
|
|
||||||
end_time (str): End time (RFC3339, e.g., "2023-10-27T11:00:00-07:00" or "2023-10-28" for all-day).
|
|
||||||
calendar_id (str): Calendar ID (default: 'primary').
|
|
||||||
description (Optional[str]): Event description.
|
|
||||||
location (Optional[str]): Event location.
|
|
||||||
attendees (Optional[List[str]]): Attendee email addresses.
|
|
||||||
timezone (Optional[str]): Timezone (e.g., "America/New_York").
|
|
||||||
attachments (Optional[List[str]]): List of Google Drive file URLs or IDs to attach to the event.
|
|
||||||
add_google_meet (bool): Whether to add a Google Meet video conference to the event. Defaults to False.
|
|
||||||
reminders (Optional[Union[str, List[Dict[str, Any]]]]): JSON string or list of reminder objects. Each should have 'method' ("popup" or "email") and 'minutes' (0-40320). Max 5 reminders. Example: '[{"method": "popup", "minutes": 15}]' or [{"method": "popup", "minutes": 15}]
|
|
||||||
use_default_reminders (bool): Whether to use calendar's default reminders. If False, uses custom reminders. Defaults to True.
|
|
||||||
transparency (Optional[str]): Event transparency for busy/free status. "opaque" shows as Busy (default), "transparent" shows as Available/Free. Defaults to None (uses Google Calendar default).
|
|
||||||
visibility (Optional[str]): Event visibility. "default" uses calendar default, "public" is visible to all, "private" is visible only to attendees, "confidential" is same as private (legacy). Defaults to None (uses Google Calendar default).
|
|
||||||
guests_can_modify (Optional[bool]): Whether attendees other than the organizer can modify the event. Defaults to None (uses Google Calendar default of False).
|
|
||||||
guests_can_invite_others (Optional[bool]): Whether attendees other than the organizer can invite others to the event. Defaults to None (uses Google Calendar default of True).
|
|
||||||
guests_can_see_other_guests (Optional[bool]): Whether attendees other than the organizer can see who the event's attendees are. Defaults to None (uses Google Calendar default of True).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message of the successful event creation with event link.
|
|
||||||
"""
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[create_event] Invoked. Email: '{user_google_email}', Summary: {summary}"
|
f"[create_event] Invoked. Email: '{user_google_email}', Summary: {summary}"
|
||||||
)
|
)
|
||||||
@@ -809,10 +789,8 @@ def _normalize_attendees(
|
|||||||
return normalized if normalized else None
|
return normalized if normalized else None
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@handle_http_errors("modify_event", service_type="calendar")
|
async def _modify_event_impl(
|
||||||
@require_google_service("calendar", "calendar_events")
|
|
||||||
async def modify_event(
|
|
||||||
service,
|
service,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
event_id: str,
|
event_id: str,
|
||||||
@@ -834,33 +812,7 @@ async def modify_event(
|
|||||||
guests_can_invite_others: Optional[bool] = None,
|
guests_can_invite_others: Optional[bool] = None,
|
||||||
guests_can_see_other_guests: Optional[bool] = None,
|
guests_can_see_other_guests: Optional[bool] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Internal implementation for modifying a calendar event."""
|
||||||
Modifies an existing event.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
event_id (str): The ID of the event to modify.
|
|
||||||
calendar_id (str): Calendar ID (default: 'primary').
|
|
||||||
summary (Optional[str]): New event title.
|
|
||||||
start_time (Optional[str]): New start time (RFC3339, e.g., "2023-10-27T10:00:00-07:00" or "2023-10-27" for all-day).
|
|
||||||
end_time (Optional[str]): New end time (RFC3339, e.g., "2023-10-27T11:00:00-07:00" or "2023-10-28" for all-day).
|
|
||||||
description (Optional[str]): New event description.
|
|
||||||
location (Optional[str]): New event location.
|
|
||||||
attendees (Optional[Union[List[str], List[Dict[str, Any]]]]): Attendees as email strings or objects with metadata. Supports: ["email@example.com"] or [{"email": "email@example.com", "responseStatus": "accepted", "organizer": true, "optional": true}]. When using objects, existing metadata (responseStatus, organizer, optional) is preserved. New attendees default to responseStatus="needsAction".
|
|
||||||
timezone (Optional[str]): New timezone (e.g., "America/New_York").
|
|
||||||
add_google_meet (Optional[bool]): Whether to add or remove Google Meet video conference. If True, adds Google Meet; if False, removes it; if None, leaves unchanged.
|
|
||||||
reminders (Optional[Union[str, List[Dict[str, Any]]]]): JSON string or list of reminder objects to replace existing reminders. Each should have 'method' ("popup" or "email") and 'minutes' (0-40320). Max 5 reminders. Example: '[{"method": "popup", "minutes": 15}]' or [{"method": "popup", "minutes": 15}]
|
|
||||||
use_default_reminders (Optional[bool]): Whether to use calendar's default reminders. If specified, overrides current reminder settings.
|
|
||||||
transparency (Optional[str]): Event transparency for busy/free status. "opaque" shows as Busy, "transparent" shows as Available/Free. If None, preserves existing transparency setting.
|
|
||||||
visibility (Optional[str]): Event visibility. "default" uses calendar default, "public" is visible to all, "private" is visible only to attendees, "confidential" is same as private (legacy). If None, preserves existing visibility setting.
|
|
||||||
color_id (Optional[str]): Event color ID (1-11). If None, preserves existing color.
|
|
||||||
guests_can_modify (Optional[bool]): Whether attendees other than the organizer can modify the event. If None, preserves existing setting.
|
|
||||||
guests_can_invite_others (Optional[bool]): Whether attendees other than the organizer can invite others to the event. If None, preserves existing setting.
|
|
||||||
guests_can_see_other_guests (Optional[bool]): Whether attendees other than the organizer can see who the event's attendees are. If None, preserves existing setting.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message of the successful event modification with event link.
|
|
||||||
"""
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[modify_event] Invoked. Email: '{user_google_email}', Event ID: {event_id}"
|
f"[modify_event] Invoked. Email: '{user_google_email}', Event ID: {event_id}"
|
||||||
)
|
)
|
||||||
@@ -1075,23 +1027,13 @@ async def modify_event(
|
|||||||
return confirmation_message
|
return confirmation_message
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
async def _delete_event_impl(
|
||||||
@handle_http_errors("delete_event", service_type="calendar")
|
service,
|
||||||
@require_google_service("calendar", "calendar_events")
|
user_google_email: str,
|
||||||
async def delete_event(
|
event_id: str,
|
||||||
service, user_google_email: str, event_id: str, calendar_id: str = "primary"
|
calendar_id: str = "primary",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Internal implementation for deleting a calendar event."""
|
||||||
Deletes an existing event.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
event_id (str): The ID of the event to delete.
|
|
||||||
calendar_id (str): Calendar ID (default: 'primary').
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message of the successful event deletion.
|
|
||||||
"""
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[delete_event] Invoked. Email: '{user_google_email}', Event ID: {event_id}"
|
f"[delete_event] Invoked. Email: '{user_google_email}', Event ID: {event_id}"
|
||||||
)
|
)
|
||||||
@@ -1133,6 +1075,141 @@ async def delete_event(
|
|||||||
return confirmation_message
|
return confirmation_message
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Consolidated event management tool
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@server.tool()
|
||||||
|
@handle_http_errors("manage_event", service_type="calendar")
|
||||||
|
@require_google_service("calendar", "calendar_events")
|
||||||
|
async def manage_event(
|
||||||
|
service,
|
||||||
|
user_google_email: str,
|
||||||
|
action: str,
|
||||||
|
summary: Optional[str] = None,
|
||||||
|
start_time: Optional[str] = None,
|
||||||
|
end_time: Optional[str] = None,
|
||||||
|
event_id: Optional[str] = None,
|
||||||
|
calendar_id: str = "primary",
|
||||||
|
description: Optional[str] = None,
|
||||||
|
location: Optional[str] = None,
|
||||||
|
attendees: Optional[Union[List[str], List[Dict[str, Any]]]] = None,
|
||||||
|
timezone: Optional[str] = None,
|
||||||
|
attachments: Optional[List[str]] = None,
|
||||||
|
add_google_meet: Optional[bool] = None,
|
||||||
|
reminders: Optional[Union[str, List[Dict[str, Any]]]] = None,
|
||||||
|
use_default_reminders: Optional[bool] = None,
|
||||||
|
transparency: Optional[str] = None,
|
||||||
|
visibility: Optional[str] = None,
|
||||||
|
color_id: Optional[str] = None,
|
||||||
|
guests_can_modify: Optional[bool] = None,
|
||||||
|
guests_can_invite_others: Optional[bool] = None,
|
||||||
|
guests_can_see_other_guests: Optional[bool] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Manages calendar events. Supports creating, updating, and deleting events.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_google_email (str): The user's Google email address. Required.
|
||||||
|
action (str): Action to perform - "create", "update", or "delete".
|
||||||
|
summary (Optional[str]): Event title (required for create).
|
||||||
|
start_time (Optional[str]): Start time in RFC3339 format (required for create).
|
||||||
|
end_time (Optional[str]): End time in RFC3339 format (required for create).
|
||||||
|
event_id (Optional[str]): Event ID (required for update and delete).
|
||||||
|
calendar_id (str): Calendar ID (default: 'primary').
|
||||||
|
description (Optional[str]): Event description.
|
||||||
|
location (Optional[str]): Event location.
|
||||||
|
attendees (Optional[Union[List[str], List[Dict[str, Any]]]]): Attendee email addresses or objects.
|
||||||
|
timezone (Optional[str]): Timezone (e.g., "America/New_York").
|
||||||
|
attachments (Optional[List[str]]): List of Google Drive file URLs or IDs to attach.
|
||||||
|
add_google_meet (Optional[bool]): Whether to add/remove Google Meet.
|
||||||
|
reminders (Optional[Union[str, List[Dict[str, Any]]]]): Custom reminder objects.
|
||||||
|
use_default_reminders (Optional[bool]): Whether to use default reminders.
|
||||||
|
transparency (Optional[str]): "opaque" (busy) or "transparent" (free).
|
||||||
|
visibility (Optional[str]): "default", "public", "private", or "confidential".
|
||||||
|
color_id (Optional[str]): Event color ID (1-11, update only).
|
||||||
|
guests_can_modify (Optional[bool]): Whether attendees can modify.
|
||||||
|
guests_can_invite_others (Optional[bool]): Whether attendees can invite others.
|
||||||
|
guests_can_see_other_guests (Optional[bool]): Whether attendees can see other guests.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Confirmation message with event details.
|
||||||
|
"""
|
||||||
|
action_lower = action.lower().strip()
|
||||||
|
if action_lower == "create":
|
||||||
|
if not summary or not start_time or not end_time:
|
||||||
|
raise ValueError("summary, start_time, and end_time are required for create action")
|
||||||
|
return await _create_event_impl(
|
||||||
|
service=service,
|
||||||
|
user_google_email=user_google_email,
|
||||||
|
summary=summary,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
calendar_id=calendar_id,
|
||||||
|
description=description,
|
||||||
|
location=location,
|
||||||
|
attendees=attendees,
|
||||||
|
timezone=timezone,
|
||||||
|
attachments=attachments,
|
||||||
|
add_google_meet=add_google_meet or False,
|
||||||
|
reminders=reminders,
|
||||||
|
use_default_reminders=use_default_reminders if use_default_reminders is not None else True,
|
||||||
|
transparency=transparency,
|
||||||
|
visibility=visibility,
|
||||||
|
guests_can_modify=guests_can_modify,
|
||||||
|
guests_can_invite_others=guests_can_invite_others,
|
||||||
|
guests_can_see_other_guests=guests_can_see_other_guests,
|
||||||
|
)
|
||||||
|
elif action_lower == "update":
|
||||||
|
if not event_id:
|
||||||
|
raise ValueError("event_id is required for update action")
|
||||||
|
return await _modify_event_impl(
|
||||||
|
service=service,
|
||||||
|
user_google_email=user_google_email,
|
||||||
|
event_id=event_id,
|
||||||
|
calendar_id=calendar_id,
|
||||||
|
summary=summary,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
description=description,
|
||||||
|
location=location,
|
||||||
|
attendees=attendees,
|
||||||
|
timezone=timezone,
|
||||||
|
add_google_meet=add_google_meet,
|
||||||
|
reminders=reminders,
|
||||||
|
use_default_reminders=use_default_reminders,
|
||||||
|
transparency=transparency,
|
||||||
|
visibility=visibility,
|
||||||
|
color_id=color_id,
|
||||||
|
guests_can_modify=guests_can_modify,
|
||||||
|
guests_can_invite_others=guests_can_invite_others,
|
||||||
|
guests_can_see_other_guests=guests_can_see_other_guests,
|
||||||
|
)
|
||||||
|
elif action_lower == "delete":
|
||||||
|
if not event_id:
|
||||||
|
raise ValueError("event_id is required for delete action")
|
||||||
|
return await _delete_event_impl(
|
||||||
|
service=service,
|
||||||
|
user_google_email=user_google_email,
|
||||||
|
event_id=event_id,
|
||||||
|
calendar_id=calendar_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid action '{action_lower}'. Must be 'create', 'update', or 'delete'.")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Legacy single-action tools (deprecated -- prefer ``manage_event``)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
@handle_http_errors("query_freebusy", is_read_only=True, service_type="calendar")
|
@handle_http_errors("query_freebusy", is_read_only=True, service_type="calendar")
|
||||||
@require_google_service("calendar", "calendar_read")
|
@require_google_service("calendar", "calendar_read")
|
||||||
|
|||||||
@@ -414,10 +414,12 @@ async def search_contacts(
|
|||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
@require_google_service("people", "contacts")
|
@require_google_service("people", "contacts")
|
||||||
@handle_http_errors("create_contact", service_type="people")
|
@handle_http_errors("manage_contact", service_type="people")
|
||||||
async def create_contact(
|
async def manage_contact(
|
||||||
service: Resource,
|
service: Resource,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
|
action: str,
|
||||||
|
contact_id: Optional[str] = None,
|
||||||
given_name: Optional[str] = None,
|
given_name: Optional[str] = None,
|
||||||
family_name: Optional[str] = None,
|
family_name: Optional[str] = None,
|
||||||
email: Optional[str] = None,
|
email: Optional[str] = None,
|
||||||
@@ -427,26 +429,36 @@ async def create_contact(
|
|||||||
notes: Optional[str] = None,
|
notes: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Create a new contact.
|
Create, update, or delete a contact. Consolidated tool replacing create_contact,
|
||||||
|
update_contact, and delete_contact.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_google_email (str): The user's Google email address. Required.
|
user_google_email (str): The user's Google email address. Required.
|
||||||
given_name (Optional[str]): First name.
|
action (str): The action to perform: "create", "update", or "delete".
|
||||||
family_name (Optional[str]): Last name.
|
contact_id (Optional[str]): The contact ID. Required for "update" and "delete" actions.
|
||||||
email (Optional[str]): Email address.
|
given_name (Optional[str]): First name (for create/update).
|
||||||
phone (Optional[str]): Phone number.
|
family_name (Optional[str]): Last name (for create/update).
|
||||||
organization (Optional[str]): Company/organization name.
|
email (Optional[str]): Email address (for create/update).
|
||||||
job_title (Optional[str]): Job title.
|
phone (Optional[str]): Phone number (for create/update).
|
||||||
notes (Optional[str]): Additional notes.
|
organization (Optional[str]): Company/organization name (for create/update).
|
||||||
|
job_title (Optional[str]): Job title (for create/update).
|
||||||
|
notes (Optional[str]): Additional notes (for create/update).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Confirmation with the new contact's details.
|
str: Result of the action performed.
|
||||||
"""
|
"""
|
||||||
|
action = action.lower().strip()
|
||||||
|
if action not in ("create", "update", "delete"):
|
||||||
|
raise Exception(
|
||||||
|
f"Invalid action '{action}'. Must be 'create', 'update', or 'delete'."
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[create_contact] Invoked. Email: '{user_google_email}', Name: '{given_name} {family_name}'"
|
f"[manage_contact] Invoked. Action: '{action}', Email: '{user_google_email}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if action == "create":
|
||||||
body = _build_person_body(
|
body = _build_person_body(
|
||||||
given_name=given_name,
|
given_name=given_name,
|
||||||
family_name=family_name,
|
family_name=family_name,
|
||||||
@@ -471,69 +483,22 @@ async def create_contact(
|
|||||||
response = f"Contact Created for {user_google_email}:\n\n"
|
response = f"Contact Created for {user_google_email}:\n\n"
|
||||||
response += _format_contact(result, detailed=True)
|
response += _format_contact(result, detailed=True)
|
||||||
|
|
||||||
contact_id = result.get("resourceName", "").replace("people/", "")
|
created_id = result.get("resourceName", "").replace("people/", "")
|
||||||
logger.info(f"Created contact {contact_id} for {user_google_email}")
|
logger.info(f"Created contact {created_id} for {user_google_email}")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except HttpError as error:
|
# update and delete both require contact_id
|
||||||
message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Contacts'."
|
if not contact_id:
|
||||||
logger.error(message, exc_info=True)
|
raise Exception(f"contact_id is required for '{action}' action.")
|
||||||
raise Exception(message)
|
|
||||||
except Exception as e:
|
|
||||||
message = f"Unexpected error: {e}."
|
|
||||||
logger.exception(message)
|
|
||||||
raise Exception(message)
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Extended Tier Tools
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@require_google_service("people", "contacts")
|
|
||||||
@handle_http_errors("update_contact", service_type="people")
|
|
||||||
async def update_contact(
|
|
||||||
service: Resource,
|
|
||||||
user_google_email: str,
|
|
||||||
contact_id: str,
|
|
||||||
given_name: Optional[str] = None,
|
|
||||||
family_name: Optional[str] = None,
|
|
||||||
email: Optional[str] = None,
|
|
||||||
phone: Optional[str] = None,
|
|
||||||
organization: Optional[str] = None,
|
|
||||||
job_title: Optional[str] = None,
|
|
||||||
notes: Optional[str] = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Update an existing contact. Note: This replaces fields, not merges them.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
contact_id (str): The contact ID to update.
|
|
||||||
given_name (Optional[str]): New first name.
|
|
||||||
family_name (Optional[str]): New last name.
|
|
||||||
email (Optional[str]): New email address.
|
|
||||||
phone (Optional[str]): New phone number.
|
|
||||||
organization (Optional[str]): New company/organization name.
|
|
||||||
job_title (Optional[str]): New job title.
|
|
||||||
notes (Optional[str]): New notes.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation with updated contact details.
|
|
||||||
"""
|
|
||||||
# Normalize resource name
|
# Normalize resource name
|
||||||
if not contact_id.startswith("people/"):
|
if not contact_id.startswith("people/"):
|
||||||
resource_name = f"people/{contact_id}"
|
resource_name = f"people/{contact_id}"
|
||||||
else:
|
else:
|
||||||
resource_name = contact_id
|
resource_name = contact_id
|
||||||
|
|
||||||
logger.info(
|
if action == "update":
|
||||||
f"[update_contact] Invoked. Email: '{user_google_email}', Contact: {resource_name}"
|
# Fetch the contact to get the etag
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# First fetch the contact to get the etag
|
|
||||||
current = await asyncio.to_thread(
|
current = await asyncio.to_thread(
|
||||||
service.people()
|
service.people()
|
||||||
.get(resourceName=resource_name, personFields=DETAILED_PERSON_FIELDS)
|
.get(resourceName=resource_name, personFields=DETAILED_PERSON_FIELDS)
|
||||||
@@ -544,7 +509,6 @@ async def update_contact(
|
|||||||
if not etag:
|
if not etag:
|
||||||
raise Exception("Unable to get contact etag for update.")
|
raise Exception("Unable to get contact etag for update.")
|
||||||
|
|
||||||
# Build update body
|
|
||||||
body = _build_person_body(
|
body = _build_person_body(
|
||||||
given_name=given_name,
|
given_name=given_name,
|
||||||
family_name=family_name,
|
family_name=family_name,
|
||||||
@@ -562,7 +526,6 @@ async def update_contact(
|
|||||||
|
|
||||||
body["etag"] = etag
|
body["etag"] = etag
|
||||||
|
|
||||||
# Determine which fields to update
|
|
||||||
update_person_fields = []
|
update_person_fields = []
|
||||||
if "names" in body:
|
if "names" in body:
|
||||||
update_person_fields.append("names")
|
update_person_fields.append("names")
|
||||||
@@ -594,55 +557,12 @@ async def update_contact(
|
|||||||
logger.info(f"Updated contact {resource_name} for {user_google_email}")
|
logger.info(f"Updated contact {resource_name} for {user_google_email}")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except HttpError as error:
|
# action == "delete"
|
||||||
if error.resp.status == 404:
|
|
||||||
message = f"Contact not found: {contact_id}"
|
|
||||||
logger.warning(message)
|
|
||||||
raise Exception(message)
|
|
||||||
message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Contacts'."
|
|
||||||
logger.error(message, exc_info=True)
|
|
||||||
raise Exception(message)
|
|
||||||
except Exception as e:
|
|
||||||
message = f"Unexpected error: {e}."
|
|
||||||
logger.exception(message)
|
|
||||||
raise Exception(message)
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@require_google_service("people", "contacts")
|
|
||||||
@handle_http_errors("delete_contact", service_type="people")
|
|
||||||
async def delete_contact(
|
|
||||||
service: Resource,
|
|
||||||
user_google_email: str,
|
|
||||||
contact_id: str,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Delete a contact.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
contact_id (str): The contact ID to delete.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message.
|
|
||||||
"""
|
|
||||||
# Normalize resource name
|
|
||||||
if not contact_id.startswith("people/"):
|
|
||||||
resource_name = f"people/{contact_id}"
|
|
||||||
else:
|
|
||||||
resource_name = contact_id
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"[delete_contact] Invoked. Email: '{user_google_email}', Contact: {resource_name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await asyncio.to_thread(
|
await asyncio.to_thread(
|
||||||
service.people().deleteContact(resourceName=resource_name).execute
|
service.people().deleteContact(resourceName=resource_name).execute
|
||||||
)
|
)
|
||||||
|
|
||||||
response = f"Contact {contact_id} has been deleted for {user_google_email}."
|
response = f"Contact {contact_id} has been deleted for {user_google_email}."
|
||||||
|
|
||||||
logger.info(f"Deleted contact {resource_name} for {user_google_email}")
|
logger.info(f"Deleted contact {resource_name} for {user_google_email}")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -660,6 +580,11 @@ async def delete_contact(
|
|||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Extended Tier Tools
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
@require_google_service("people", "contacts_read")
|
@require_google_service("people", "contacts_read")
|
||||||
@handle_http_errors("list_contact_groups", service_type="people")
|
@handle_http_errors("list_contact_groups", service_type="people")
|
||||||
@@ -811,40 +736,52 @@ async def get_contact_group(
|
|||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
@require_google_service("people", "contacts")
|
@require_google_service("people", "contacts")
|
||||||
@handle_http_errors("batch_create_contacts", service_type="people")
|
@handle_http_errors("manage_contacts_batch", service_type="people")
|
||||||
async def batch_create_contacts(
|
async def manage_contacts_batch(
|
||||||
service: Resource,
|
service: Resource,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
contacts: List[Dict[str, str]],
|
action: str,
|
||||||
|
contacts: Optional[List[Dict[str, str]]] = None,
|
||||||
|
updates: Optional[List[Dict[str, str]]] = None,
|
||||||
|
contact_ids: Optional[List[str]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Create multiple contacts in a batch operation.
|
Batch create, update, or delete contacts. Consolidated tool replacing
|
||||||
|
batch_create_contacts, batch_update_contacts, and batch_delete_contacts.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_google_email (str): The user's Google email address. Required.
|
user_google_email (str): The user's Google email address. Required.
|
||||||
contacts (List[Dict[str, str]]): List of contact dictionaries with fields:
|
action (str): The action to perform: "create", "update", or "delete".
|
||||||
- given_name: First name
|
contacts (Optional[List[Dict[str, str]]]): List of contact dicts for "create" action.
|
||||||
- family_name: Last name
|
Each dict may contain: given_name, family_name, email, phone, organization, job_title.
|
||||||
- email: Email address
|
updates (Optional[List[Dict[str, str]]]): List of update dicts for "update" action.
|
||||||
- phone: Phone number
|
Each dict must contain contact_id and may contain: given_name, family_name,
|
||||||
- organization: Company name
|
email, phone, organization, job_title.
|
||||||
- job_title: Job title
|
contact_ids (Optional[List[str]]): List of contact IDs for "delete" action.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Confirmation with created contacts.
|
str: Result of the batch action performed.
|
||||||
"""
|
"""
|
||||||
|
action = action.lower().strip()
|
||||||
|
if action not in ("create", "update", "delete"):
|
||||||
|
raise Exception(
|
||||||
|
f"Invalid action '{action}'. Must be 'create', 'update', or 'delete'."
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[batch_create_contacts] Invoked. Email: '{user_google_email}', Count: {len(contacts)}"
|
f"[manage_contacts_batch] Invoked. Action: '{action}', Email: '{user_google_email}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if action == "create":
|
||||||
if not contacts:
|
if not contacts:
|
||||||
raise Exception("At least one contact must be provided.")
|
raise Exception(
|
||||||
|
"contacts parameter is required for 'create' action."
|
||||||
|
)
|
||||||
|
|
||||||
if len(contacts) > 200:
|
if len(contacts) > 200:
|
||||||
raise Exception("Maximum 200 contacts can be created in a batch.")
|
raise Exception("Maximum 200 contacts can be created in a batch.")
|
||||||
|
|
||||||
# Build batch request body
|
|
||||||
contact_bodies = []
|
contact_bodies = []
|
||||||
for contact in contacts:
|
for contact in contacts:
|
||||||
body = _build_person_body(
|
body = _build_person_body(
|
||||||
@@ -884,63 +821,25 @@ async def batch_create_contacts(
|
|||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except HttpError as error:
|
if action == "update":
|
||||||
message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Contacts'."
|
|
||||||
logger.error(message, exc_info=True)
|
|
||||||
raise Exception(message)
|
|
||||||
except Exception as e:
|
|
||||||
message = f"Unexpected error: {e}."
|
|
||||||
logger.exception(message)
|
|
||||||
raise Exception(message)
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@require_google_service("people", "contacts")
|
|
||||||
@handle_http_errors("batch_update_contacts", service_type="people")
|
|
||||||
async def batch_update_contacts(
|
|
||||||
service: Resource,
|
|
||||||
user_google_email: str,
|
|
||||||
updates: List[Dict[str, str]],
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Update multiple contacts in a batch operation.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
updates (List[Dict[str, str]]): List of update dictionaries with fields:
|
|
||||||
- contact_id: The contact ID to update (required)
|
|
||||||
- given_name: New first name
|
|
||||||
- family_name: New last name
|
|
||||||
- email: New email address
|
|
||||||
- phone: New phone number
|
|
||||||
- organization: New company name
|
|
||||||
- job_title: New job title
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation with updated contacts.
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
f"[batch_update_contacts] Invoked. Email: '{user_google_email}', Count: {len(updates)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not updates:
|
if not updates:
|
||||||
raise Exception("At least one update must be provided.")
|
raise Exception(
|
||||||
|
"updates parameter is required for 'update' action."
|
||||||
|
)
|
||||||
|
|
||||||
if len(updates) > 200:
|
if len(updates) > 200:
|
||||||
raise Exception("Maximum 200 contacts can be updated in a batch.")
|
raise Exception("Maximum 200 contacts can be updated in a batch.")
|
||||||
|
|
||||||
# First, fetch all contacts to get their etags
|
# Fetch all contacts to get their etags
|
||||||
resource_names = []
|
resource_names = []
|
||||||
for update in updates:
|
for update in updates:
|
||||||
contact_id = update.get("contact_id")
|
cid = update.get("contact_id")
|
||||||
if not contact_id:
|
if not cid:
|
||||||
raise Exception("Each update must include a contact_id.")
|
raise Exception("Each update must include a contact_id.")
|
||||||
if not contact_id.startswith("people/"):
|
if not cid.startswith("people/"):
|
||||||
contact_id = f"people/{contact_id}"
|
cid = f"people/{cid}"
|
||||||
resource_names.append(contact_id)
|
resource_names.append(cid)
|
||||||
|
|
||||||
# Batch get contacts for etags
|
|
||||||
batch_get_result = await asyncio.to_thread(
|
batch_get_result = await asyncio.to_thread(
|
||||||
service.people()
|
service.people()
|
||||||
.getBatchGet(
|
.getBatchGet(
|
||||||
@@ -951,25 +850,24 @@ async def batch_update_contacts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
etags = {}
|
etags = {}
|
||||||
for response in batch_get_result.get("responses", []):
|
for resp in batch_get_result.get("responses", []):
|
||||||
person = response.get("person", {})
|
person = resp.get("person", {})
|
||||||
resource_name = person.get("resourceName")
|
rname = person.get("resourceName")
|
||||||
etag = person.get("etag")
|
etag = person.get("etag")
|
||||||
if resource_name and etag:
|
if rname and etag:
|
||||||
etags[resource_name] = etag
|
etags[rname] = etag
|
||||||
|
|
||||||
# Build batch update body
|
|
||||||
update_bodies = []
|
update_bodies = []
|
||||||
update_fields_set: set = set()
|
update_fields_set: set = set()
|
||||||
|
|
||||||
for update in updates:
|
for update in updates:
|
||||||
contact_id = update.get("contact_id", "")
|
cid = update.get("contact_id", "")
|
||||||
if not contact_id.startswith("people/"):
|
if not cid.startswith("people/"):
|
||||||
contact_id = f"people/{contact_id}"
|
cid = f"people/{cid}"
|
||||||
|
|
||||||
etag = etags.get(contact_id)
|
etag = etags.get(cid)
|
||||||
if not etag:
|
if not etag:
|
||||||
logger.warning(f"No etag found for {contact_id}, skipping")
|
logger.warning(f"No etag found for {cid}, skipping")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
body = _build_person_body(
|
body = _build_person_body(
|
||||||
@@ -982,11 +880,10 @@ async def batch_update_contacts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if body:
|
if body:
|
||||||
body["resourceName"] = contact_id
|
body["resourceName"] = cid
|
||||||
body["etag"] = etag
|
body["etag"] = etag
|
||||||
update_bodies.append({"person": body})
|
update_bodies.append({"person": body})
|
||||||
|
|
||||||
# Track which fields are being updated
|
|
||||||
if "names" in body:
|
if "names" in body:
|
||||||
update_fields_set.add("names")
|
update_fields_set.add("names")
|
||||||
if "emailAddresses" in body:
|
if "emailAddresses" in body:
|
||||||
@@ -1014,7 +911,7 @@ async def batch_update_contacts(
|
|||||||
response = f"Batch Update Results for {user_google_email}:\n\n"
|
response = f"Batch Update Results for {user_google_email}:\n\n"
|
||||||
response += f"Updated {len(update_results)} contacts:\n\n"
|
response += f"Updated {len(update_results)} contacts:\n\n"
|
||||||
|
|
||||||
for resource_name, update_result in update_results.items():
|
for rname, update_result in update_results.items():
|
||||||
person = update_result.get("person", {})
|
person = update_result.get("person", {})
|
||||||
response += _format_contact(person) + "\n\n"
|
response += _format_contact(person) + "\n\n"
|
||||||
|
|
||||||
@@ -1023,52 +920,21 @@ async def batch_update_contacts(
|
|||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except HttpError as error:
|
# action == "delete"
|
||||||
message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Contacts'."
|
|
||||||
logger.error(message, exc_info=True)
|
|
||||||
raise Exception(message)
|
|
||||||
except Exception as e:
|
|
||||||
message = f"Unexpected error: {e}."
|
|
||||||
logger.exception(message)
|
|
||||||
raise Exception(message)
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@require_google_service("people", "contacts")
|
|
||||||
@handle_http_errors("batch_delete_contacts", service_type="people")
|
|
||||||
async def batch_delete_contacts(
|
|
||||||
service: Resource,
|
|
||||||
user_google_email: str,
|
|
||||||
contact_ids: List[str],
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Delete multiple contacts in a batch operation.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
contact_ids (List[str]): List of contact IDs to delete.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message.
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
f"[batch_delete_contacts] Invoked. Email: '{user_google_email}', Count: {len(contact_ids)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not contact_ids:
|
if not contact_ids:
|
||||||
raise Exception("At least one contact ID must be provided.")
|
raise Exception(
|
||||||
|
"contact_ids parameter is required for 'delete' action."
|
||||||
|
)
|
||||||
|
|
||||||
if len(contact_ids) > 500:
|
if len(contact_ids) > 500:
|
||||||
raise Exception("Maximum 500 contacts can be deleted in a batch.")
|
raise Exception("Maximum 500 contacts can be deleted in a batch.")
|
||||||
|
|
||||||
# Normalize resource names
|
|
||||||
resource_names = []
|
resource_names = []
|
||||||
for contact_id in contact_ids:
|
for cid in contact_ids:
|
||||||
if not contact_id.startswith("people/"):
|
if not cid.startswith("people/"):
|
||||||
resource_names.append(f"people/{contact_id}")
|
resource_names.append(f"people/{cid}")
|
||||||
else:
|
else:
|
||||||
resource_names.append(contact_id)
|
resource_names.append(cid)
|
||||||
|
|
||||||
batch_body = {"resourceNames": resource_names}
|
batch_body = {"resourceNames": resource_names}
|
||||||
|
|
||||||
@@ -1077,7 +943,6 @@ async def batch_delete_contacts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = f"Batch deleted {len(contact_ids)} contacts for {user_google_email}."
|
response = f"Batch deleted {len(contact_ids)} contacts for {user_google_email}."
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Batch deleted {len(contact_ids)} contacts for {user_google_email}"
|
f"Batch deleted {len(contact_ids)} contacts for {user_google_email}"
|
||||||
)
|
)
|
||||||
@@ -1095,27 +960,51 @@ async def batch_delete_contacts(
|
|||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
@require_google_service("people", "contacts")
|
@require_google_service("people", "contacts")
|
||||||
@handle_http_errors("create_contact_group", service_type="people")
|
@handle_http_errors("manage_contact_group", service_type="people")
|
||||||
async def create_contact_group(
|
async def manage_contact_group(
|
||||||
service: Resource,
|
service: Resource,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
name: str,
|
action: str,
|
||||||
|
group_id: Optional[str] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
delete_contacts: bool = False,
|
||||||
|
add_contact_ids: Optional[List[str]] = None,
|
||||||
|
remove_contact_ids: Optional[List[str]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Create a new contact group (label).
|
Create, update, delete a contact group, or modify its members. Consolidated tool
|
||||||
|
replacing create_contact_group, update_contact_group, delete_contact_group, and
|
||||||
|
modify_contact_group_members.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_google_email (str): The user's Google email address. Required.
|
user_google_email (str): The user's Google email address. Required.
|
||||||
name (str): The name of the new contact group.
|
action (str): The action to perform: "create", "update", "delete", or "modify_members".
|
||||||
|
group_id (Optional[str]): The contact group ID. Required for "update", "delete",
|
||||||
|
and "modify_members" actions.
|
||||||
|
name (Optional[str]): The group name. Required for "create" and "update" actions.
|
||||||
|
delete_contacts (bool): If True and action is "delete", also delete contacts in
|
||||||
|
the group (default: False).
|
||||||
|
add_contact_ids (Optional[List[str]]): Contact IDs to add (for "modify_members").
|
||||||
|
remove_contact_ids (Optional[List[str]]): Contact IDs to remove (for "modify_members").
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Confirmation with the new group details.
|
str: Result of the action performed.
|
||||||
"""
|
"""
|
||||||
|
action = action.lower().strip()
|
||||||
|
if action not in ("create", "update", "delete", "modify_members"):
|
||||||
|
raise Exception(
|
||||||
|
f"Invalid action '{action}'. Must be 'create', 'update', 'delete', or 'modify_members'."
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[create_contact_group] Invoked. Email: '{user_google_email}', Name: '{name}'"
|
f"[manage_contact_group] Invoked. Action: '{action}', Email: '{user_google_email}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if action == "create":
|
||||||
|
if not name:
|
||||||
|
raise Exception("name is required for 'create' action.")
|
||||||
|
|
||||||
body = {"contactGroup": {"name": name}}
|
body = {"contactGroup": {"name": name}}
|
||||||
|
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
@@ -1123,58 +1012,31 @@ async def create_contact_group(
|
|||||||
)
|
)
|
||||||
|
|
||||||
resource_name = result.get("resourceName", "")
|
resource_name = result.get("resourceName", "")
|
||||||
group_id = resource_name.replace("contactGroups/", "")
|
created_group_id = resource_name.replace("contactGroups/", "")
|
||||||
created_name = result.get("name", name)
|
created_name = result.get("name", name)
|
||||||
|
|
||||||
response = f"Contact Group Created for {user_google_email}:\n\n"
|
response = f"Contact Group Created for {user_google_email}:\n\n"
|
||||||
response += f"Name: {created_name}\n"
|
response += f"Name: {created_name}\n"
|
||||||
response += f"ID: {group_id}\n"
|
response += f"ID: {created_group_id}\n"
|
||||||
response += f"Type: {result.get('groupType', 'USER_CONTACT_GROUP')}\n"
|
response += f"Type: {result.get('groupType', 'USER_CONTACT_GROUP')}\n"
|
||||||
|
|
||||||
logger.info(f"Created contact group '{name}' for {user_google_email}")
|
logger.info(f"Created contact group '{name}' for {user_google_email}")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except HttpError as error:
|
# All other actions require group_id
|
||||||
message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Contacts'."
|
if not group_id:
|
||||||
logger.error(message, exc_info=True)
|
raise Exception(f"group_id is required for '{action}' action.")
|
||||||
raise Exception(message)
|
|
||||||
except Exception as e:
|
|
||||||
message = f"Unexpected error: {e}."
|
|
||||||
logger.exception(message)
|
|
||||||
raise Exception(message)
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@require_google_service("people", "contacts")
|
|
||||||
@handle_http_errors("update_contact_group", service_type="people")
|
|
||||||
async def update_contact_group(
|
|
||||||
service: Resource,
|
|
||||||
user_google_email: str,
|
|
||||||
group_id: str,
|
|
||||||
name: str,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Update a contact group's name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
group_id (str): The contact group ID to update.
|
|
||||||
name (str): The new name for the contact group.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation with updated group details.
|
|
||||||
"""
|
|
||||||
# Normalize resource name
|
# Normalize resource name
|
||||||
if not group_id.startswith("contactGroups/"):
|
if not group_id.startswith("contactGroups/"):
|
||||||
resource_name = f"contactGroups/{group_id}"
|
resource_name = f"contactGroups/{group_id}"
|
||||||
else:
|
else:
|
||||||
resource_name = group_id
|
resource_name = group_id
|
||||||
|
|
||||||
logger.info(
|
if action == "update":
|
||||||
f"[update_contact_group] Invoked. Email: '{user_google_email}', Group: {resource_name}"
|
if not name:
|
||||||
)
|
raise Exception("name is required for 'update' action.")
|
||||||
|
|
||||||
try:
|
|
||||||
body = {"contactGroup": {"name": name}}
|
body = {"contactGroup": {"name": name}}
|
||||||
|
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
@@ -1189,54 +1051,12 @@ async def update_contact_group(
|
|||||||
response += f"Name: {updated_name}\n"
|
response += f"Name: {updated_name}\n"
|
||||||
response += f"ID: {group_id}\n"
|
response += f"ID: {group_id}\n"
|
||||||
|
|
||||||
logger.info(f"Updated contact group {resource_name} for {user_google_email}")
|
logger.info(
|
||||||
|
f"Updated contact group {resource_name} for {user_google_email}"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except HttpError as error:
|
if action == "delete":
|
||||||
if error.resp.status == 404:
|
|
||||||
message = f"Contact group not found: {group_id}"
|
|
||||||
logger.warning(message)
|
|
||||||
raise Exception(message)
|
|
||||||
message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Contacts'."
|
|
||||||
logger.error(message, exc_info=True)
|
|
||||||
raise Exception(message)
|
|
||||||
except Exception as e:
|
|
||||||
message = f"Unexpected error: {e}."
|
|
||||||
logger.exception(message)
|
|
||||||
raise Exception(message)
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@require_google_service("people", "contacts")
|
|
||||||
@handle_http_errors("delete_contact_group", service_type="people")
|
|
||||||
async def delete_contact_group(
|
|
||||||
service: Resource,
|
|
||||||
user_google_email: str,
|
|
||||||
group_id: str,
|
|
||||||
delete_contacts: bool = False,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Delete a contact group.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
group_id (str): The contact group ID to delete.
|
|
||||||
delete_contacts (bool): If True, also delete contacts in the group (default: False).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message.
|
|
||||||
"""
|
|
||||||
# Normalize resource name
|
|
||||||
if not group_id.startswith("contactGroups/"):
|
|
||||||
resource_name = f"contactGroups/{group_id}"
|
|
||||||
else:
|
|
||||||
resource_name = group_id
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"[delete_contact_group] Invoked. Email: '{user_google_email}', Group: {resource_name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await asyncio.to_thread(
|
await asyncio.to_thread(
|
||||||
service.contactGroups()
|
service.contactGroups()
|
||||||
.delete(resourceName=resource_name, deleteContacts=delete_contacts)
|
.delete(resourceName=resource_name, deleteContacts=delete_contacts)
|
||||||
@@ -1249,87 +1069,41 @@ async def delete_contact_group(
|
|||||||
else:
|
else:
|
||||||
response += " Contacts in the group were preserved."
|
response += " Contacts in the group were preserved."
|
||||||
|
|
||||||
logger.info(f"Deleted contact group {resource_name} for {user_google_email}")
|
logger.info(
|
||||||
|
f"Deleted contact group {resource_name} for {user_google_email}"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except HttpError as error:
|
# action == "modify_members"
|
||||||
if error.resp.status == 404:
|
|
||||||
message = f"Contact group not found: {group_id}"
|
|
||||||
logger.warning(message)
|
|
||||||
raise Exception(message)
|
|
||||||
message = f"API error: {error}. You might need to re-authenticate. LLM: Try 'start_google_auth' with the user's email ({user_google_email}) and service_name='Google Contacts'."
|
|
||||||
logger.error(message, exc_info=True)
|
|
||||||
raise Exception(message)
|
|
||||||
except Exception as e:
|
|
||||||
message = f"Unexpected error: {e}."
|
|
||||||
logger.exception(message)
|
|
||||||
raise Exception(message)
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@require_google_service("people", "contacts")
|
|
||||||
@handle_http_errors("modify_contact_group_members", service_type="people")
|
|
||||||
async def modify_contact_group_members(
|
|
||||||
service: Resource,
|
|
||||||
user_google_email: str,
|
|
||||||
group_id: str,
|
|
||||||
add_contact_ids: Optional[List[str]] = None,
|
|
||||||
remove_contact_ids: Optional[List[str]] = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Add or remove contacts from a contact group.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
group_id (str): The contact group ID.
|
|
||||||
add_contact_ids (Optional[List[str]]): Contact IDs to add to the group.
|
|
||||||
remove_contact_ids (Optional[List[str]]): Contact IDs to remove from the group.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation with results.
|
|
||||||
"""
|
|
||||||
# Normalize resource name
|
|
||||||
if not group_id.startswith("contactGroups/"):
|
|
||||||
resource_name = f"contactGroups/{group_id}"
|
|
||||||
else:
|
|
||||||
resource_name = group_id
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"[modify_contact_group_members] Invoked. Email: '{user_google_email}', Group: {resource_name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not add_contact_ids and not remove_contact_ids:
|
if not add_contact_ids and not remove_contact_ids:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"At least one of add_contact_ids or remove_contact_ids must be provided."
|
"At least one of add_contact_ids or remove_contact_ids must be provided."
|
||||||
)
|
)
|
||||||
|
|
||||||
body: Dict[str, Any] = {}
|
modify_body: Dict[str, Any] = {}
|
||||||
|
|
||||||
if add_contact_ids:
|
if add_contact_ids:
|
||||||
# Normalize resource names
|
|
||||||
add_names = []
|
add_names = []
|
||||||
for contact_id in add_contact_ids:
|
for contact_id in add_contact_ids:
|
||||||
if not contact_id.startswith("people/"):
|
if not contact_id.startswith("people/"):
|
||||||
add_names.append(f"people/{contact_id}")
|
add_names.append(f"people/{contact_id}")
|
||||||
else:
|
else:
|
||||||
add_names.append(contact_id)
|
add_names.append(contact_id)
|
||||||
body["resourceNamesToAdd"] = add_names
|
modify_body["resourceNamesToAdd"] = add_names
|
||||||
|
|
||||||
if remove_contact_ids:
|
if remove_contact_ids:
|
||||||
# Normalize resource names
|
|
||||||
remove_names = []
|
remove_names = []
|
||||||
for contact_id in remove_contact_ids:
|
for contact_id in remove_contact_ids:
|
||||||
if not contact_id.startswith("people/"):
|
if not contact_id.startswith("people/"):
|
||||||
remove_names.append(f"people/{contact_id}")
|
remove_names.append(f"people/{contact_id}")
|
||||||
else:
|
else:
|
||||||
remove_names.append(contact_id)
|
remove_names.append(contact_id)
|
||||||
body["resourceNamesToRemove"] = remove_names
|
modify_body["resourceNamesToRemove"] = remove_names
|
||||||
|
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
service.contactGroups()
|
service.contactGroups()
|
||||||
.members()
|
.members()
|
||||||
.modify(resourceName=resource_name, body=body)
|
.modify(resourceName=resource_name, body=modify_body)
|
||||||
.execute
|
.execute
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1366,3 +1140,5 @@ async def modify_contact_group_members(
|
|||||||
message = f"Unexpected error: {e}."
|
message = f"Unexpected error: {e}."
|
||||||
logger.exception(message)
|
logger.exception(message)
|
||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1774,61 +1774,103 @@ async def get_drive_shareable_link(
|
|||||||
|
|
||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
@handle_http_errors("share_drive_file", is_read_only=False, service_type="drive")
|
@handle_http_errors("manage_drive_access", is_read_only=False, service_type="drive")
|
||||||
@require_google_service("drive", "drive_file")
|
@require_google_service("drive", "drive_file")
|
||||||
async def share_drive_file(
|
async def manage_drive_access(
|
||||||
service,
|
service,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
file_id: str,
|
file_id: str,
|
||||||
|
action: str,
|
||||||
share_with: Optional[str] = None,
|
share_with: Optional[str] = None,
|
||||||
role: str = "reader",
|
role: Optional[str] = None,
|
||||||
share_type: str = "user",
|
share_type: str = "user",
|
||||||
|
permission_id: Optional[str] = None,
|
||||||
|
recipients: Optional[List[Dict[str, Any]]] = None,
|
||||||
send_notification: bool = True,
|
send_notification: bool = True,
|
||||||
email_message: Optional[str] = None,
|
email_message: Optional[str] = None,
|
||||||
expiration_time: Optional[str] = None,
|
expiration_time: Optional[str] = None,
|
||||||
allow_file_discovery: Optional[bool] = None,
|
allow_file_discovery: Optional[bool] = None,
|
||||||
|
new_owner_email: Optional[str] = None,
|
||||||
|
move_to_new_owners_root: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Shares a Google Drive file or folder with a user, group, domain, or anyone with the link.
|
Consolidated tool for managing Google Drive file and folder access permissions.
|
||||||
|
|
||||||
When sharing a folder, all files inside inherit the permission.
|
Supports granting, batch-granting, updating, revoking permissions, and
|
||||||
|
transferring file ownership -- all through a single entry point.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_google_email (str): The user's Google email address. Required.
|
user_google_email (str): The user's Google email address. Required.
|
||||||
file_id (str): The ID of the file or folder to share. Required.
|
file_id (str): The ID of the file or folder. Required.
|
||||||
share_with (Optional[str]): Email address (for user/group), domain name (for domain), or omit for 'anyone'.
|
action (str): The access management action to perform. Required. One of:
|
||||||
role (str): Permission role - 'reader', 'commenter', or 'writer'. Defaults to 'reader'.
|
- "grant": Share with a single user, group, domain, or anyone.
|
||||||
share_type (str): Type of sharing - 'user', 'group', 'domain', or 'anyone'. Defaults to 'user'.
|
- "grant_batch": Share with multiple recipients in one call.
|
||||||
send_notification (bool): Whether to send a notification email. Defaults to True.
|
- "update": Modify an existing permission (role or expiration).
|
||||||
email_message (Optional[str]): Custom message for the notification email.
|
- "revoke": Remove an existing permission.
|
||||||
expiration_time (Optional[str]): Expiration time in RFC 3339 format (e.g., "2025-01-15T00:00:00Z"). Permission auto-revokes after this time.
|
- "transfer_owner": Transfer file ownership to another user.
|
||||||
allow_file_discovery (Optional[bool]): For 'domain' or 'anyone' shares - whether the file can be found via search. Defaults to None (API default).
|
share_with (Optional[str]): Email address (user/group), domain name (domain),
|
||||||
|
or omit for 'anyone'. Used by "grant".
|
||||||
|
role (Optional[str]): Permission role -- 'reader', 'commenter', or 'writer'.
|
||||||
|
Used by "grant" (defaults to 'reader') and "update".
|
||||||
|
share_type (str): Type of sharing -- 'user', 'group', 'domain', or 'anyone'.
|
||||||
|
Used by "grant". Defaults to 'user'.
|
||||||
|
permission_id (Optional[str]): The permission ID to modify or remove.
|
||||||
|
Required for "update" and "revoke" actions.
|
||||||
|
recipients (Optional[List[Dict[str, Any]]]): List of recipient objects for
|
||||||
|
"grant_batch". Each should have: email (str), role (str, optional),
|
||||||
|
share_type (str, optional), expiration_time (str, optional). For domain
|
||||||
|
shares use 'domain' field instead of 'email'.
|
||||||
|
send_notification (bool): Whether to send notification emails. Defaults to True.
|
||||||
|
Used by "grant" and "grant_batch".
|
||||||
|
email_message (Optional[str]): Custom notification email message.
|
||||||
|
Used by "grant" and "grant_batch".
|
||||||
|
expiration_time (Optional[str]): Expiration in RFC 3339 format
|
||||||
|
(e.g., "2025-01-15T00:00:00Z"). Used by "grant" and "update".
|
||||||
|
allow_file_discovery (Optional[bool]): For 'domain'/'anyone' shares, whether
|
||||||
|
the file appears in search. Used by "grant".
|
||||||
|
new_owner_email (Optional[str]): Email of the new owner.
|
||||||
|
Required for "transfer_owner".
|
||||||
|
move_to_new_owners_root (bool): Move file to the new owner's My Drive root.
|
||||||
|
Defaults to False. Used by "transfer_owner".
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Confirmation with permission details and shareable link.
|
str: Confirmation with details of the permission change applied.
|
||||||
"""
|
"""
|
||||||
logger.info(
|
valid_actions = ("grant", "grant_batch", "update", "revoke", "transfer_owner")
|
||||||
f"[share_drive_file] Invoked. Email: '{user_google_email}', File ID: '{file_id}', Share with: '{share_with}', Role: '{role}', Type: '{share_type}'"
|
if action not in valid_actions:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
validate_share_role(role)
|
logger.info(
|
||||||
|
f"[manage_drive_access] Invoked. Email: '{user_google_email}', "
|
||||||
|
f"File ID: '{file_id}', Action: '{action}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- grant: share with a single recipient ---
|
||||||
|
if action == "grant":
|
||||||
|
effective_role = role or "reader"
|
||||||
|
validate_share_role(effective_role)
|
||||||
validate_share_type(share_type)
|
validate_share_type(share_type)
|
||||||
|
|
||||||
if share_type in ("user", "group") and not share_with:
|
if share_type in ("user", "group") and not share_with:
|
||||||
raise ValueError(f"share_with is required for share_type '{share_type}'")
|
raise ValueError(
|
||||||
|
f"share_with is required for share_type '{share_type}'"
|
||||||
|
)
|
||||||
if share_type == "domain" and not share_with:
|
if share_type == "domain" and not share_with:
|
||||||
raise ValueError("share_with (domain name) is required for share_type 'domain'")
|
raise ValueError(
|
||||||
|
"share_with (domain name) is required for share_type 'domain'"
|
||||||
|
)
|
||||||
|
|
||||||
resolved_file_id, file_metadata = await resolve_drive_item(
|
resolved_file_id, file_metadata = await resolve_drive_item(
|
||||||
service, file_id, extra_fields="name, webViewLink"
|
service, file_id, extra_fields="name, webViewLink"
|
||||||
)
|
)
|
||||||
file_id = resolved_file_id
|
file_id = resolved_file_id
|
||||||
|
|
||||||
permission_body = {
|
permission_body: Dict[str, Any] = {
|
||||||
"type": share_type,
|
"type": share_type,
|
||||||
"role": role,
|
"role": effective_role,
|
||||||
}
|
}
|
||||||
|
|
||||||
if share_type in ("user", "group"):
|
if share_type in ("user", "group"):
|
||||||
permission_body["emailAddress"] = share_with
|
permission_body["emailAddress"] = share_with
|
||||||
elif share_type == "domain":
|
elif share_type == "domain":
|
||||||
@@ -1841,13 +1883,12 @@ async def share_drive_file(
|
|||||||
if share_type in ("domain", "anyone") and allow_file_discovery is not None:
|
if share_type in ("domain", "anyone") and allow_file_discovery is not None:
|
||||||
permission_body["allowFileDiscovery"] = allow_file_discovery
|
permission_body["allowFileDiscovery"] = allow_file_discovery
|
||||||
|
|
||||||
create_params = {
|
create_params: Dict[str, Any] = {
|
||||||
"fileId": file_id,
|
"fileId": file_id,
|
||||||
"body": permission_body,
|
"body": permission_body,
|
||||||
"supportsAllDrives": True,
|
"supportsAllDrives": True,
|
||||||
"fields": "id, type, role, emailAddress, domain, expirationTime",
|
"fields": "id, type, role, emailAddress, domain, expirationTime",
|
||||||
}
|
}
|
||||||
|
|
||||||
if share_type in ("user", "group"):
|
if share_type in ("user", "group"):
|
||||||
create_params["sendNotificationEmail"] = send_notification
|
create_params["sendNotificationEmail"] = send_notification
|
||||||
if email_message:
|
if email_message:
|
||||||
@@ -1857,73 +1898,33 @@ async def share_drive_file(
|
|||||||
service.permissions().create(**create_params).execute
|
service.permissions().create(**create_params).execute
|
||||||
)
|
)
|
||||||
|
|
||||||
output_parts = [
|
return "\n".join([
|
||||||
f"Successfully shared '{file_metadata.get('name', 'Unknown')}'",
|
f"Successfully shared '{file_metadata.get('name', 'Unknown')}'",
|
||||||
"",
|
"",
|
||||||
"Permission created:",
|
"Permission created:",
|
||||||
f" - {format_permission_info(created_permission)}",
|
f" - {format_permission_info(created_permission)}",
|
||||||
"",
|
"",
|
||||||
f"View link: {file_metadata.get('webViewLink', 'N/A')}",
|
f"View link: {file_metadata.get('webViewLink', 'N/A')}",
|
||||||
]
|
])
|
||||||
|
|
||||||
return "\n".join(output_parts)
|
# --- grant_batch: share with multiple recipients ---
|
||||||
|
if action == "grant_batch":
|
||||||
|
if not recipients:
|
||||||
@server.tool()
|
raise ValueError("recipients list is required for 'grant_batch' action")
|
||||||
@handle_http_errors("batch_share_drive_file", is_read_only=False, service_type="drive")
|
|
||||||
@require_google_service("drive", "drive_file")
|
|
||||||
async def batch_share_drive_file(
|
|
||||||
service,
|
|
||||||
user_google_email: str,
|
|
||||||
file_id: str,
|
|
||||||
recipients: List[Dict[str, Any]],
|
|
||||||
send_notification: bool = True,
|
|
||||||
email_message: Optional[str] = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Shares a Google Drive file or folder with multiple users or groups in a single operation.
|
|
||||||
|
|
||||||
Each recipient can have a different role and optional expiration time.
|
|
||||||
|
|
||||||
Note: Each recipient is processed sequentially. For very large recipient lists,
|
|
||||||
consider splitting into multiple calls.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
file_id (str): The ID of the file or folder to share. Required.
|
|
||||||
recipients (List[Dict]): List of recipient objects. Each should have:
|
|
||||||
- email (str): Recipient email address. Required for 'user' or 'group' share_type.
|
|
||||||
- role (str): Permission role - 'reader', 'commenter', or 'writer'. Defaults to 'reader'.
|
|
||||||
- share_type (str, optional): 'user', 'group', or 'domain'. Defaults to 'user'.
|
|
||||||
- expiration_time (str, optional): Expiration in RFC 3339 format (e.g., "2025-01-15T00:00:00Z").
|
|
||||||
For domain shares, use 'domain' field instead of 'email':
|
|
||||||
- domain (str): Domain name. Required when share_type is 'domain'.
|
|
||||||
send_notification (bool): Whether to send notification emails. Defaults to True.
|
|
||||||
email_message (Optional[str]): Custom message for notification emails.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Summary of created permissions with success/failure for each recipient.
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
f"[batch_share_drive_file] Invoked. Email: '{user_google_email}', File ID: '{file_id}', Recipients: {len(recipients)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
resolved_file_id, file_metadata = await resolve_drive_item(
|
resolved_file_id, file_metadata = await resolve_drive_item(
|
||||||
service, file_id, extra_fields="name, webViewLink"
|
service, file_id, extra_fields="name, webViewLink"
|
||||||
)
|
)
|
||||||
file_id = resolved_file_id
|
file_id = resolved_file_id
|
||||||
|
|
||||||
if not recipients:
|
results: List[str] = []
|
||||||
raise ValueError("recipients list cannot be empty")
|
|
||||||
|
|
||||||
results = []
|
|
||||||
success_count = 0
|
success_count = 0
|
||||||
failure_count = 0
|
failure_count = 0
|
||||||
|
|
||||||
for recipient in recipients:
|
for recipient in recipients:
|
||||||
share_type = recipient.get("share_type", "user")
|
r_share_type = recipient.get("share_type", "user")
|
||||||
|
|
||||||
if share_type == "domain":
|
if r_share_type == "domain":
|
||||||
domain = recipient.get("domain")
|
domain = recipient.get("domain")
|
||||||
if not domain:
|
if not domain:
|
||||||
results.append(" - Skipped: missing domain for domain share")
|
results.append(" - Skipped: missing domain for domain share")
|
||||||
@@ -1931,64 +1932,62 @@ async def batch_share_drive_file(
|
|||||||
continue
|
continue
|
||||||
identifier = domain
|
identifier = domain
|
||||||
else:
|
else:
|
||||||
email = recipient.get("email")
|
r_email = recipient.get("email")
|
||||||
if not email:
|
if not r_email:
|
||||||
results.append(" - Skipped: missing email address")
|
results.append(" - Skipped: missing email address")
|
||||||
failure_count += 1
|
failure_count += 1
|
||||||
continue
|
continue
|
||||||
identifier = email
|
identifier = r_email
|
||||||
|
|
||||||
role = recipient.get("role", "reader")
|
r_role = recipient.get("role", "reader")
|
||||||
try:
|
try:
|
||||||
validate_share_role(role)
|
validate_share_role(r_role)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
results.append(f" - {identifier}: Failed - {e}")
|
results.append(f" - {identifier}: Failed - {e}")
|
||||||
failure_count += 1
|
failure_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
validate_share_type(share_type)
|
validate_share_type(r_share_type)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
results.append(f" - {identifier}: Failed - {e}")
|
results.append(f" - {identifier}: Failed - {e}")
|
||||||
failure_count += 1
|
failure_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
permission_body = {
|
r_perm_body: Dict[str, Any] = {
|
||||||
"type": share_type,
|
"type": r_share_type,
|
||||||
"role": role,
|
"role": r_role,
|
||||||
}
|
}
|
||||||
|
if r_share_type == "domain":
|
||||||
if share_type == "domain":
|
r_perm_body["domain"] = identifier
|
||||||
permission_body["domain"] = identifier
|
|
||||||
else:
|
else:
|
||||||
permission_body["emailAddress"] = identifier
|
r_perm_body["emailAddress"] = identifier
|
||||||
|
|
||||||
if recipient.get("expiration_time"):
|
if recipient.get("expiration_time"):
|
||||||
try:
|
try:
|
||||||
validate_expiration_time(recipient["expiration_time"])
|
validate_expiration_time(recipient["expiration_time"])
|
||||||
permission_body["expirationTime"] = recipient["expiration_time"]
|
r_perm_body["expirationTime"] = recipient["expiration_time"]
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
results.append(f" - {identifier}: Failed - {e}")
|
results.append(f" - {identifier}: Failed - {e}")
|
||||||
failure_count += 1
|
failure_count += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
create_params = {
|
r_create_params: Dict[str, Any] = {
|
||||||
"fileId": file_id,
|
"fileId": file_id,
|
||||||
"body": permission_body,
|
"body": r_perm_body,
|
||||||
"supportsAllDrives": True,
|
"supportsAllDrives": True,
|
||||||
"fields": "id, type, role, emailAddress, domain, expirationTime",
|
"fields": "id, type, role, emailAddress, domain, expirationTime",
|
||||||
}
|
}
|
||||||
|
if r_share_type in ("user", "group"):
|
||||||
if share_type in ("user", "group"):
|
r_create_params["sendNotificationEmail"] = send_notification
|
||||||
create_params["sendNotificationEmail"] = send_notification
|
|
||||||
if email_message:
|
if email_message:
|
||||||
create_params["emailMessage"] = email_message
|
r_create_params["emailMessage"] = email_message
|
||||||
|
|
||||||
try:
|
try:
|
||||||
created_permission = await asyncio.to_thread(
|
created_perm = await asyncio.to_thread(
|
||||||
service.permissions().create(**create_params).execute
|
service.permissions().create(**r_create_params).execute
|
||||||
)
|
)
|
||||||
results.append(f" - {format_permission_info(created_permission)}")
|
results.append(f" - {format_permission_info(created_perm)}")
|
||||||
success_count += 1
|
success_count += 1
|
||||||
except HttpError as e:
|
except HttpError as e:
|
||||||
results.append(f" - {identifier}: Failed - {str(e)}")
|
results.append(f" - {identifier}: Failed - {str(e)}")
|
||||||
@@ -2002,46 +2001,20 @@ async def batch_share_drive_file(
|
|||||||
"Results:",
|
"Results:",
|
||||||
]
|
]
|
||||||
output_parts.extend(results)
|
output_parts.extend(results)
|
||||||
output_parts.extend(
|
output_parts.extend([
|
||||||
[
|
|
||||||
"",
|
"",
|
||||||
f"View link: {file_metadata.get('webViewLink', 'N/A')}",
|
f"View link: {file_metadata.get('webViewLink', 'N/A')}",
|
||||||
]
|
])
|
||||||
)
|
|
||||||
|
|
||||||
return "\n".join(output_parts)
|
return "\n".join(output_parts)
|
||||||
|
|
||||||
|
# --- update: modify an existing permission ---
|
||||||
@server.tool()
|
if action == "update":
|
||||||
@handle_http_errors("update_drive_permission", is_read_only=False, service_type="drive")
|
if not permission_id:
|
||||||
@require_google_service("drive", "drive_file")
|
raise ValueError("permission_id is required for 'update' action")
|
||||||
async def update_drive_permission(
|
|
||||||
service,
|
|
||||||
user_google_email: str,
|
|
||||||
file_id: str,
|
|
||||||
permission_id: str,
|
|
||||||
role: Optional[str] = None,
|
|
||||||
expiration_time: Optional[str] = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Updates an existing permission on a Google Drive file or folder.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
file_id (str): The ID of the file or folder. Required.
|
|
||||||
permission_id (str): The ID of the permission to update (from get_drive_file_permissions). Required.
|
|
||||||
role (Optional[str]): New role - 'reader', 'commenter', or 'writer'. If not provided, role unchanged.
|
|
||||||
expiration_time (Optional[str]): Expiration time in RFC 3339 format (e.g., "2025-01-15T00:00:00Z"). Set or update when permission expires.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation with updated permission details.
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
f"[update_drive_permission] Invoked. Email: '{user_google_email}', File ID: '{file_id}', Permission ID: '{permission_id}', Role: '{role}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not role and not expiration_time:
|
if not role and not expiration_time:
|
||||||
raise ValueError("Must provide at least one of: role, expiration_time")
|
raise ValueError(
|
||||||
|
"Must provide at least one of: role, expiration_time for 'update' action"
|
||||||
|
)
|
||||||
|
|
||||||
if role:
|
if role:
|
||||||
validate_share_role(role)
|
validate_share_role(role)
|
||||||
@@ -2053,8 +2026,8 @@ async def update_drive_permission(
|
|||||||
)
|
)
|
||||||
file_id = resolved_file_id
|
file_id = resolved_file_id
|
||||||
|
|
||||||
# Google API requires role in update body, so fetch current if not provided
|
effective_role = role
|
||||||
if not role:
|
if not effective_role:
|
||||||
current_permission = await asyncio.to_thread(
|
current_permission = await asyncio.to_thread(
|
||||||
service.permissions()
|
service.permissions()
|
||||||
.get(
|
.get(
|
||||||
@@ -2065,9 +2038,9 @@ async def update_drive_permission(
|
|||||||
)
|
)
|
||||||
.execute
|
.execute
|
||||||
)
|
)
|
||||||
role = current_permission.get("role")
|
effective_role = current_permission.get("role")
|
||||||
|
|
||||||
update_body = {"role": role}
|
update_body: Dict[str, Any] = {"role": effective_role}
|
||||||
if expiration_time:
|
if expiration_time:
|
||||||
update_body["expirationTime"] = expiration_time
|
update_body["expirationTime"] = expiration_time
|
||||||
|
|
||||||
@@ -2083,39 +2056,17 @@ async def update_drive_permission(
|
|||||||
.execute
|
.execute
|
||||||
)
|
)
|
||||||
|
|
||||||
output_parts = [
|
return "\n".join([
|
||||||
f"Successfully updated permission on '{file_metadata.get('name', 'Unknown')}'",
|
f"Successfully updated permission on '{file_metadata.get('name', 'Unknown')}'",
|
||||||
"",
|
"",
|
||||||
"Updated permission:",
|
"Updated permission:",
|
||||||
f" - {format_permission_info(updated_permission)}",
|
f" - {format_permission_info(updated_permission)}",
|
||||||
]
|
])
|
||||||
|
|
||||||
return "\n".join(output_parts)
|
# --- revoke: remove an existing permission ---
|
||||||
|
if action == "revoke":
|
||||||
|
if not permission_id:
|
||||||
@server.tool()
|
raise ValueError("permission_id is required for 'revoke' action")
|
||||||
@handle_http_errors("remove_drive_permission", is_read_only=False, service_type="drive")
|
|
||||||
@require_google_service("drive", "drive_file")
|
|
||||||
async def remove_drive_permission(
|
|
||||||
service,
|
|
||||||
user_google_email: str,
|
|
||||||
file_id: str,
|
|
||||||
permission_id: str,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Removes a permission from a Google Drive file or folder, revoking access.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
file_id (str): The ID of the file or folder. Required.
|
|
||||||
permission_id (str): The ID of the permission to remove (from get_drive_file_permissions). Required.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation of the removed permission.
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
f"[remove_drive_permission] Invoked. Email: '{user_google_email}', File ID: '{file_id}', Permission ID: '{permission_id}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
resolved_file_id, file_metadata = await resolve_drive_item(
|
resolved_file_id, file_metadata = await resolve_drive_item(
|
||||||
service, file_id, extra_fields="name"
|
service, file_id, extra_fields="name"
|
||||||
@@ -2124,19 +2075,73 @@ async def remove_drive_permission(
|
|||||||
|
|
||||||
await asyncio.to_thread(
|
await asyncio.to_thread(
|
||||||
service.permissions()
|
service.permissions()
|
||||||
.delete(fileId=file_id, permissionId=permission_id, supportsAllDrives=True)
|
.delete(
|
||||||
|
fileId=file_id,
|
||||||
|
permissionId=permission_id,
|
||||||
|
supportsAllDrives=True,
|
||||||
|
)
|
||||||
|
.execute
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join([
|
||||||
|
f"Successfully removed permission from '{file_metadata.get('name', 'Unknown')}'",
|
||||||
|
"",
|
||||||
|
f"Permission ID '{permission_id}' has been revoked.",
|
||||||
|
])
|
||||||
|
|
||||||
|
# --- transfer_owner: transfer file ownership ---
|
||||||
|
# action == "transfer_owner"
|
||||||
|
if not new_owner_email:
|
||||||
|
raise ValueError("new_owner_email is required for 'transfer_owner' action")
|
||||||
|
|
||||||
|
resolved_file_id, file_metadata = await resolve_drive_item(
|
||||||
|
service, file_id, extra_fields="name, owners"
|
||||||
|
)
|
||||||
|
file_id = resolved_file_id
|
||||||
|
|
||||||
|
current_owners = file_metadata.get("owners", [])
|
||||||
|
current_owner_emails = [o.get("emailAddress", "") for o in current_owners]
|
||||||
|
|
||||||
|
transfer_body: Dict[str, Any] = {
|
||||||
|
"type": "user",
|
||||||
|
"role": "owner",
|
||||||
|
"emailAddress": new_owner_email,
|
||||||
|
}
|
||||||
|
|
||||||
|
await asyncio.to_thread(
|
||||||
|
service.permissions()
|
||||||
|
.create(
|
||||||
|
fileId=file_id,
|
||||||
|
body=transfer_body,
|
||||||
|
transferOwnership=True,
|
||||||
|
moveToNewOwnersRoot=move_to_new_owners_root,
|
||||||
|
supportsAllDrives=True,
|
||||||
|
fields="id, type, role, emailAddress",
|
||||||
|
)
|
||||||
.execute
|
.execute
|
||||||
)
|
)
|
||||||
|
|
||||||
output_parts = [
|
output_parts = [
|
||||||
f"Successfully removed permission from '{file_metadata.get('name', 'Unknown')}'",
|
f"Successfully transferred ownership of '{file_metadata.get('name', 'Unknown')}'",
|
||||||
"",
|
"",
|
||||||
f"Permission ID '{permission_id}' has been revoked.",
|
f"New owner: {new_owner_email}",
|
||||||
|
f"Previous owner(s): {', '.join(current_owner_emails) or 'Unknown'}",
|
||||||
]
|
]
|
||||||
|
if move_to_new_owners_root:
|
||||||
|
output_parts.append(f"File moved to {new_owner_email}'s My Drive root.")
|
||||||
|
output_parts.extend(["", "Note: Previous owner now has editor access."])
|
||||||
|
|
||||||
return "\n".join(output_parts)
|
return "\n".join(output_parts)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
@handle_http_errors("copy_drive_file", is_read_only=False, service_type="drive")
|
@handle_http_errors("copy_drive_file", is_read_only=False, service_type="drive")
|
||||||
@require_google_service("drive", "drive_file")
|
@require_google_service("drive", "drive_file")
|
||||||
@@ -2209,77 +2214,6 @@ async def copy_drive_file(
|
|||||||
return "\n".join(output_parts)
|
return "\n".join(output_parts)
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@handle_http_errors(
|
|
||||||
"transfer_drive_ownership", is_read_only=False, service_type="drive"
|
|
||||||
)
|
|
||||||
@require_google_service("drive", "drive_file")
|
|
||||||
async def transfer_drive_ownership(
|
|
||||||
service,
|
|
||||||
user_google_email: str,
|
|
||||||
file_id: str,
|
|
||||||
new_owner_email: str,
|
|
||||||
move_to_new_owners_root: bool = False,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Transfers ownership of a Google Drive file or folder to another user.
|
|
||||||
|
|
||||||
This is an irreversible operation. The current owner will become an editor.
|
|
||||||
Only works within the same Google Workspace domain or for personal accounts.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
file_id (str): The ID of the file or folder to transfer. Required.
|
|
||||||
new_owner_email (str): Email address of the new owner. Required.
|
|
||||||
move_to_new_owners_root (bool): If True, moves the file to the new owner's My Drive root. Defaults to False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation of the ownership transfer.
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
f"[transfer_drive_ownership] Invoked. Email: '{user_google_email}', File ID: '{file_id}', New owner: '{new_owner_email}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
resolved_file_id, file_metadata = await resolve_drive_item(
|
|
||||||
service, file_id, extra_fields="name, owners"
|
|
||||||
)
|
|
||||||
file_id = resolved_file_id
|
|
||||||
|
|
||||||
current_owners = file_metadata.get("owners", [])
|
|
||||||
current_owner_emails = [o.get("emailAddress", "") for o in current_owners]
|
|
||||||
|
|
||||||
permission_body = {
|
|
||||||
"type": "user",
|
|
||||||
"role": "owner",
|
|
||||||
"emailAddress": new_owner_email,
|
|
||||||
}
|
|
||||||
|
|
||||||
await asyncio.to_thread(
|
|
||||||
service.permissions()
|
|
||||||
.create(
|
|
||||||
fileId=file_id,
|
|
||||||
body=permission_body,
|
|
||||||
transferOwnership=True,
|
|
||||||
moveToNewOwnersRoot=move_to_new_owners_root,
|
|
||||||
supportsAllDrives=True,
|
|
||||||
fields="id, type, role, emailAddress",
|
|
||||||
)
|
|
||||||
.execute
|
|
||||||
)
|
|
||||||
|
|
||||||
output_parts = [
|
|
||||||
f"Successfully transferred ownership of '{file_metadata.get('name', 'Unknown')}'",
|
|
||||||
"",
|
|
||||||
f"New owner: {new_owner_email}",
|
|
||||||
f"Previous owner(s): {', '.join(current_owner_emails) or 'Unknown'}",
|
|
||||||
]
|
|
||||||
|
|
||||||
if move_to_new_owners_root:
|
|
||||||
output_parts.append(f"File moved to {new_owner_email}'s My Drive root.")
|
|
||||||
|
|
||||||
output_parts.extend(["", "Note: Previous owner now has editor access."])
|
|
||||||
|
|
||||||
return "\n".join(output_parts)
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
|
|||||||
@@ -1822,39 +1822,35 @@ async def list_gmail_filters(service, user_google_email: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
@handle_http_errors("create_gmail_filter", service_type="gmail")
|
@handle_http_errors("manage_gmail_filter", service_type="gmail")
|
||||||
@require_google_service("gmail", "gmail_settings_basic")
|
@require_google_service("gmail", "gmail_settings_basic")
|
||||||
async def create_gmail_filter(
|
async def manage_gmail_filter(
|
||||||
service,
|
service,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
criteria: Annotated[
|
action: str,
|
||||||
Dict[str, Any],
|
criteria: Optional[Dict[str, Any]] = None,
|
||||||
Field(
|
filter_action: Optional[Dict[str, Any]] = None,
|
||||||
description="Filter criteria object as defined in the Gmail API.",
|
filter_id: Optional[str] = None,
|
||||||
),
|
|
||||||
],
|
|
||||||
action: Annotated[
|
|
||||||
Dict[str, Any],
|
|
||||||
Field(
|
|
||||||
description="Filter action object as defined in the Gmail API.",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Creates a Gmail filter using the users.settings.filters API.
|
Manages Gmail filters. Supports creating and deleting filters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_google_email (str): The user's Google email address. Required.
|
user_google_email (str): The user's Google email address. Required.
|
||||||
criteria (Dict[str, Any]): Criteria for matching messages.
|
action (str): Action to perform - "create" or "delete".
|
||||||
action (Dict[str, Any]): Actions to apply to matched messages.
|
criteria (Optional[Dict[str, Any]]): Filter criteria object (required for create).
|
||||||
|
filter_action (Optional[Dict[str, Any]]): Filter action object (required for create). Named 'filter_action' to avoid shadowing the 'action' parameter.
|
||||||
|
filter_id (Optional[str]): ID of the filter to delete (required for delete).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Confirmation message with the created filter ID.
|
str: Confirmation message with filter details.
|
||||||
"""
|
"""
|
||||||
logger.info("[create_gmail_filter] Invoked")
|
action_lower = action.lower().strip()
|
||||||
|
if action_lower == "create":
|
||||||
filter_body = {"criteria": criteria, "action": action}
|
if not criteria or not filter_action:
|
||||||
|
raise ValueError("criteria and filter_action are required for create action")
|
||||||
|
logger.info("[manage_gmail_filter] Creating filter")
|
||||||
|
filter_body = {"criteria": criteria, "action": filter_action}
|
||||||
created_filter = await asyncio.to_thread(
|
created_filter = await asyncio.to_thread(
|
||||||
service.users()
|
service.users()
|
||||||
.settings()
|
.settings()
|
||||||
@@ -1862,48 +1858,32 @@ async def create_gmail_filter(
|
|||||||
.create(userId="me", body=filter_body)
|
.create(userId="me", body=filter_body)
|
||||||
.execute
|
.execute
|
||||||
)
|
)
|
||||||
|
fid = created_filter.get("id", "(unknown)")
|
||||||
filter_id = created_filter.get("id", "(unknown)")
|
return f"Filter created successfully!\nFilter ID: {fid}"
|
||||||
return f"Filter created successfully!\nFilter ID: {filter_id}"
|
elif action_lower == "delete":
|
||||||
|
if not filter_id:
|
||||||
|
raise ValueError("filter_id is required for delete action")
|
||||||
@server.tool()
|
logger.info(f"[manage_gmail_filter] Deleting filter {filter_id}")
|
||||||
@handle_http_errors("delete_gmail_filter", service_type="gmail")
|
|
||||||
@require_google_service("gmail", "gmail_settings_basic")
|
|
||||||
async def delete_gmail_filter(
|
|
||||||
service,
|
|
||||||
user_google_email: str,
|
|
||||||
filter_id: str = Field(..., description="ID of the filter to delete."),
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Deletes a Gmail filter by ID.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
filter_id (str): The ID of the filter to delete.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message for the deletion.
|
|
||||||
"""
|
|
||||||
logger.info(f"[delete_gmail_filter] Invoked. Filter ID: '{filter_id}'")
|
|
||||||
|
|
||||||
filter_details = await asyncio.to_thread(
|
filter_details = await asyncio.to_thread(
|
||||||
service.users().settings().filters().get(userId="me", id=filter_id).execute
|
service.users().settings().filters().get(userId="me", id=filter_id).execute
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncio.to_thread(
|
await asyncio.to_thread(
|
||||||
service.users().settings().filters().delete(userId="me", id=filter_id).execute
|
service.users().settings().filters().delete(userId="me", id=filter_id).execute
|
||||||
)
|
)
|
||||||
|
criteria_info = filter_details.get("criteria", {})
|
||||||
criteria = filter_details.get("criteria", {})
|
action_info = filter_details.get("action", {})
|
||||||
action = filter_details.get("action", {})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
"Filter deleted successfully!\n"
|
"Filter deleted successfully!\n"
|
||||||
f"Filter ID: {filter_id}\n"
|
f"Filter ID: {filter_id}\n"
|
||||||
f"Criteria: {criteria or '(none)'}\n"
|
f"Criteria: {criteria_info or '(none)'}\n"
|
||||||
f"Action: {action or '(none)'}"
|
f"Action: {action_info or '(none)'}"
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid action '{action_lower}'. Must be 'create' or 'delete'.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ async def search_custom(
|
|||||||
file_type: Optional[str] = None,
|
file_type: Optional[str] = None,
|
||||||
language: Optional[str] = None,
|
language: Optional[str] = None,
|
||||||
country: Optional[str] = None,
|
country: Optional[str] = None,
|
||||||
|
sites: Optional[List[str]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Performs a search using Google Custom Search JSON API.
|
Performs a search using Google Custom Search JSON API.
|
||||||
@@ -50,6 +51,7 @@ async def search_custom(
|
|||||||
file_type (Optional[str]): Filter by file type (e.g., "pdf", "doc").
|
file_type (Optional[str]): Filter by file type (e.g., "pdf", "doc").
|
||||||
language (Optional[str]): Language code for results (e.g., "lang_en").
|
language (Optional[str]): Language code for results (e.g., "lang_en").
|
||||||
country (Optional[str]): Country code for results (e.g., "countryUS").
|
country (Optional[str]): Country code for results (e.g., "countryUS").
|
||||||
|
sites (Optional[List[str]]): List of sites/domains to restrict search to (e.g., ["example.com", "docs.example.com"]). When provided, results are limited to these sites.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Formatted search results including title, link, and snippet for each result.
|
str: Formatted search results including title, link, and snippet for each result.
|
||||||
@@ -71,6 +73,12 @@ async def search_custom(
|
|||||||
f"[search_custom] Invoked. Email: '{user_google_email}', Query: '{q}', CX: '{cx}'"
|
f"[search_custom] Invoked. Email: '{user_google_email}', Query: '{q}', CX: '{cx}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Apply site restriction if sites are provided
|
||||||
|
if sites:
|
||||||
|
site_query = " OR ".join([f"site:{site}" for site in sites])
|
||||||
|
q = f"{q} ({site_query})"
|
||||||
|
logger.info(f"[search_custom] Applied site restriction: {sites}")
|
||||||
|
|
||||||
# Build the request parameters
|
# Build the request parameters
|
||||||
params = {
|
params = {
|
||||||
"key": api_key,
|
"key": api_key,
|
||||||
@@ -226,48 +234,3 @@ async def get_search_engine_info(service, user_google_email: str) -> str:
|
|||||||
return confirmation_message
|
return confirmation_message
|
||||||
|
|
||||||
|
|
||||||
@server.tool()
|
|
||||||
@handle_http_errors(
|
|
||||||
"search_custom_siterestrict", is_read_only=True, service_type="customsearch"
|
|
||||||
)
|
|
||||||
@require_google_service("customsearch", "customsearch")
|
|
||||||
async def search_custom_siterestrict(
|
|
||||||
service,
|
|
||||||
user_google_email: str,
|
|
||||||
q: str,
|
|
||||||
sites: List[str],
|
|
||||||
num: int = 10,
|
|
||||||
start: int = 1,
|
|
||||||
safe: Literal["active", "moderate", "off"] = "off",
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Performs a search restricted to specific sites using Google Custom Search.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
q (str): The search query. Required.
|
|
||||||
sites (List[str]): List of sites/domains to search within.
|
|
||||||
num (int): Number of results to return (1-10). Defaults to 10.
|
|
||||||
start (int): The index of the first result to return (1-based). Defaults to 1.
|
|
||||||
safe (Literal["active", "moderate", "off"]): Safe search level. Defaults to "off".
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Formatted search results from the specified sites.
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
f"[search_custom_siterestrict] Invoked. Email: '{user_google_email}', Query: '{q}', Sites: {sites}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build site restriction query
|
|
||||||
site_query = " OR ".join([f"site:{site}" for site in sites])
|
|
||||||
full_query = f"{q} ({site_query})"
|
|
||||||
|
|
||||||
# Use the main search function with the modified query
|
|
||||||
return await search_custom(
|
|
||||||
service=service,
|
|
||||||
user_google_email=user_google_email,
|
|
||||||
q=full_query,
|
|
||||||
num=num,
|
|
||||||
start=start,
|
|
||||||
safe=safe,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -729,58 +729,98 @@ async def format_sheet_range(
|
|||||||
|
|
||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
@handle_http_errors("add_conditional_formatting", service_type="sheets")
|
@handle_http_errors("manage_conditional_formatting", service_type="sheets")
|
||||||
@require_google_service("sheets", "sheets_write")
|
@require_google_service("sheets", "sheets_write")
|
||||||
async def add_conditional_formatting(
|
async def manage_conditional_formatting(
|
||||||
service,
|
service,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
spreadsheet_id: str,
|
spreadsheet_id: str,
|
||||||
range_name: str,
|
action: str,
|
||||||
condition_type: str,
|
range_name: Optional[str] = None,
|
||||||
|
condition_type: Optional[str] = None,
|
||||||
condition_values: Optional[Union[str, List[Union[str, int, float]]]] = None,
|
condition_values: Optional[Union[str, List[Union[str, int, float]]]] = None,
|
||||||
background_color: Optional[str] = None,
|
background_color: Optional[str] = None,
|
||||||
text_color: Optional[str] = None,
|
text_color: Optional[str] = None,
|
||||||
rule_index: Optional[int] = None,
|
rule_index: Optional[int] = None,
|
||||||
gradient_points: Optional[Union[str, List[dict]]] = None,
|
gradient_points: Optional[Union[str, List[dict]]] = None,
|
||||||
|
sheet_name: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Adds a conditional formatting rule to a range.
|
Manages conditional formatting rules on a Google Sheet. Supports adding,
|
||||||
|
updating, and deleting conditional formatting rules via a single tool.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_google_email (str): The user's Google email address. Required.
|
user_google_email (str): The user's Google email address. Required.
|
||||||
spreadsheet_id (str): The ID of the spreadsheet. Required.
|
spreadsheet_id (str): The ID of the spreadsheet. Required.
|
||||||
range_name (str): A1-style range (optionally with sheet name). Required.
|
action (str): The operation to perform. Must be one of "add", "update",
|
||||||
condition_type (str): Sheets condition type (e.g., NUMBER_GREATER, TEXT_CONTAINS, DATE_BEFORE, CUSTOM_FORMULA).
|
or "delete".
|
||||||
condition_values (Optional[Union[str, List[Union[str, int, float]]]]): Values for the condition; accepts a list or a JSON string representing a list. Depends on condition_type.
|
range_name (Optional[str]): A1-style range (optionally with sheet name).
|
||||||
background_color (Optional[str]): Hex background color to apply when condition matches.
|
Required for "add". Optional for "update" (preserves existing ranges
|
||||||
|
if omitted). Not used for "delete".
|
||||||
|
condition_type (Optional[str]): Sheets condition type (e.g., NUMBER_GREATER,
|
||||||
|
TEXT_CONTAINS, DATE_BEFORE, CUSTOM_FORMULA). Required for "add".
|
||||||
|
Optional for "update" (preserves existing type if omitted).
|
||||||
|
condition_values (Optional[Union[str, List[Union[str, int, float]]]]): Values
|
||||||
|
for the condition; accepts a list or a JSON string representing a list.
|
||||||
|
Depends on condition_type. Used by "add" and "update".
|
||||||
|
background_color (Optional[str]): Hex background color to apply when
|
||||||
|
condition matches. Used by "add" and "update".
|
||||||
text_color (Optional[str]): Hex text color to apply when condition matches.
|
text_color (Optional[str]): Hex text color to apply when condition matches.
|
||||||
rule_index (Optional[int]): Optional position to insert the rule (0-based) within the sheet's rules.
|
Used by "add" and "update".
|
||||||
gradient_points (Optional[Union[str, List[dict]]]): List (or JSON list) of gradient points for a color scale. If provided, a gradient rule is created and boolean parameters are ignored.
|
rule_index (Optional[int]): 0-based index of the rule. For "add", optionally
|
||||||
|
specifies insertion position. Required for "update" and "delete".
|
||||||
|
gradient_points (Optional[Union[str, List[dict]]]): List (or JSON list) of
|
||||||
|
gradient points for a color scale. If provided, a gradient rule is created
|
||||||
|
and boolean parameters are ignored. Used by "add" and "update".
|
||||||
|
sheet_name (Optional[str]): Sheet name to locate the rule when range_name is
|
||||||
|
omitted. Defaults to the first sheet. Used by "update" and "delete".
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Confirmation of the added rule.
|
str: Confirmation of the operation and the current rule state.
|
||||||
"""
|
"""
|
||||||
logger.info(
|
allowed_actions = {"add", "update", "delete"}
|
||||||
"[add_conditional_formatting] Invoked. Email: '%s', Spreadsheet: %s, Range: %s, Type: %s, Values: %s",
|
action_normalized = action.strip().lower()
|
||||||
user_google_email,
|
if action_normalized not in allowed_actions:
|
||||||
spreadsheet_id,
|
raise UserInputError(
|
||||||
range_name,
|
f"action must be one of {sorted(allowed_actions)}, got '{action}'."
|
||||||
condition_type,
|
|
||||||
condition_values,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if rule_index is not None and (not isinstance(rule_index, int) or rule_index < 0):
|
logger.info(
|
||||||
raise UserInputError("rule_index must be a non-negative integer when provided.")
|
"[manage_conditional_formatting] Invoked. Action: '%s', Email: '%s', Spreadsheet: %s",
|
||||||
|
action_normalized,
|
||||||
|
user_google_email,
|
||||||
|
spreadsheet_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if action_normalized == "add":
|
||||||
|
if not range_name:
|
||||||
|
raise UserInputError("range_name is required for action 'add'.")
|
||||||
|
if not condition_type and not gradient_points:
|
||||||
|
raise UserInputError(
|
||||||
|
"condition_type (or gradient_points) is required for action 'add'."
|
||||||
|
)
|
||||||
|
|
||||||
|
if rule_index is not None and (
|
||||||
|
not isinstance(rule_index, int) or rule_index < 0
|
||||||
|
):
|
||||||
|
raise UserInputError(
|
||||||
|
"rule_index must be a non-negative integer when provided."
|
||||||
|
)
|
||||||
|
|
||||||
condition_values_list = _parse_condition_values(condition_values)
|
condition_values_list = _parse_condition_values(condition_values)
|
||||||
gradient_points_list = _parse_gradient_points(gradient_points)
|
gradient_points_list = _parse_gradient_points(gradient_points)
|
||||||
|
|
||||||
sheets, sheet_titles = await _fetch_sheets_with_rules(service, spreadsheet_id)
|
sheets, sheet_titles = await _fetch_sheets_with_rules(
|
||||||
|
service, spreadsheet_id
|
||||||
|
)
|
||||||
grid_range = _parse_a1_range(range_name, sheets)
|
grid_range = _parse_a1_range(range_name, sheets)
|
||||||
|
|
||||||
target_sheet = None
|
target_sheet = None
|
||||||
for sheet in sheets:
|
for sheet in sheets:
|
||||||
if sheet.get("properties", {}).get("sheetId") == grid_range.get("sheetId"):
|
if (
|
||||||
|
sheet.get("properties", {}).get("sheetId")
|
||||||
|
== grid_range.get("sheetId")
|
||||||
|
):
|
||||||
target_sheet = sheet
|
target_sheet = sheet
|
||||||
break
|
break
|
||||||
if target_sheet is None:
|
if target_sheet is None:
|
||||||
@@ -793,7 +833,8 @@ async def add_conditional_formatting(
|
|||||||
insert_at = rule_index if rule_index is not None else len(current_rules)
|
insert_at = rule_index if rule_index is not None else len(current_rules)
|
||||||
if insert_at > len(current_rules):
|
if insert_at > len(current_rules):
|
||||||
raise UserInputError(
|
raise UserInputError(
|
||||||
f"rule_index {insert_at} is out of range for sheet '{target_sheet.get('properties', {}).get('title', 'Unknown')}' "
|
f"rule_index {insert_at} is out of range for sheet "
|
||||||
|
f"'{target_sheet.get('properties', {}).get('title', 'Unknown')}' "
|
||||||
f"(current count: {len(current_rules)})."
|
f"(current count: {len(current_rules)})."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -828,7 +869,9 @@ async def add_conditional_formatting(
|
|||||||
if rule_index is not None:
|
if rule_index is not None:
|
||||||
add_rule_request["index"] = rule_index
|
add_rule_request["index"] = rule_index
|
||||||
|
|
||||||
request_body = {"requests": [{"addConditionalFormatRule": add_rule_request}]}
|
request_body = {
|
||||||
|
"requests": [{"addConditionalFormatRule": add_rule_request}]
|
||||||
|
}
|
||||||
|
|
||||||
await asyncio.to_thread(
|
await asyncio.to_thread(
|
||||||
service.spreadsheets()
|
service.spreadsheets()
|
||||||
@@ -836,7 +879,9 @@ async def add_conditional_formatting(
|
|||||||
.execute
|
.execute
|
||||||
)
|
)
|
||||||
|
|
||||||
format_desc = ", ".join(applied_parts) if applied_parts else "format applied"
|
format_desc = (
|
||||||
|
", ".join(applied_parts) if applied_parts else "format applied"
|
||||||
|
)
|
||||||
|
|
||||||
sheet_title = target_sheet.get("properties", {}).get("title", "Unknown")
|
sheet_title = target_sheet.get("properties", {}).get("title", "Unknown")
|
||||||
state_text = _format_conditional_rules_section(
|
state_text = _format_conditional_rules_section(
|
||||||
@@ -845,69 +890,35 @@ async def add_conditional_formatting(
|
|||||||
|
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
[
|
[
|
||||||
f"Added conditional format on '{range_name}' in spreadsheet {spreadsheet_id} "
|
f"Added conditional format on '{range_name}' in spreadsheet "
|
||||||
f"for {user_google_email}: {rule_desc}{values_desc}; format: {format_desc}.",
|
f"{spreadsheet_id} for {user_google_email}: "
|
||||||
|
f"{rule_desc}{values_desc}; format: {format_desc}.",
|
||||||
state_text,
|
state_text,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif action_normalized == "update":
|
||||||
@server.tool()
|
if rule_index is None:
|
||||||
@handle_http_errors("update_conditional_formatting", service_type="sheets")
|
raise UserInputError("rule_index is required for action 'update'.")
|
||||||
@require_google_service("sheets", "sheets_write")
|
|
||||||
async def update_conditional_formatting(
|
|
||||||
service,
|
|
||||||
user_google_email: str,
|
|
||||||
spreadsheet_id: str,
|
|
||||||
rule_index: int,
|
|
||||||
range_name: Optional[str] = None,
|
|
||||||
condition_type: Optional[str] = None,
|
|
||||||
condition_values: Optional[Union[str, List[Union[str, int, float]]]] = None,
|
|
||||||
background_color: Optional[str] = None,
|
|
||||||
text_color: Optional[str] = None,
|
|
||||||
sheet_name: Optional[str] = None,
|
|
||||||
gradient_points: Optional[Union[str, List[dict]]] = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Updates an existing conditional formatting rule by index on a sheet.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
spreadsheet_id (str): The ID of the spreadsheet. Required.
|
|
||||||
range_name (Optional[str]): A1-style range to apply the updated rule (optionally with sheet name). If omitted, existing ranges are preserved.
|
|
||||||
rule_index (int): Index of the rule to update (0-based).
|
|
||||||
condition_type (Optional[str]): Sheets condition type. If omitted, the existing rule's type is preserved.
|
|
||||||
condition_values (Optional[Union[str, List[Union[str, int, float]]]]): Values for the condition.
|
|
||||||
background_color (Optional[str]): Hex background color when condition matches.
|
|
||||||
text_color (Optional[str]): Hex text color when condition matches.
|
|
||||||
sheet_name (Optional[str]): Sheet name to locate the rule when range_name is omitted. Defaults to first sheet.
|
|
||||||
gradient_points (Optional[Union[str, List[dict]]]): If provided, updates the rule to a gradient color scale using these points.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation of the updated rule and the current rule state.
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
"[update_conditional_formatting] Invoked. Email: '%s', Spreadsheet: %s, Range: %s, Rule Index: %s",
|
|
||||||
user_google_email,
|
|
||||||
spreadsheet_id,
|
|
||||||
range_name,
|
|
||||||
rule_index,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(rule_index, int) or rule_index < 0:
|
if not isinstance(rule_index, int) or rule_index < 0:
|
||||||
raise UserInputError("rule_index must be a non-negative integer.")
|
raise UserInputError("rule_index must be a non-negative integer.")
|
||||||
|
|
||||||
condition_values_list = _parse_condition_values(condition_values)
|
condition_values_list = _parse_condition_values(condition_values)
|
||||||
gradient_points_list = _parse_gradient_points(gradient_points)
|
gradient_points_list = _parse_gradient_points(gradient_points)
|
||||||
|
|
||||||
sheets, sheet_titles = await _fetch_sheets_with_rules(service, spreadsheet_id)
|
sheets, sheet_titles = await _fetch_sheets_with_rules(
|
||||||
|
service, spreadsheet_id
|
||||||
|
)
|
||||||
|
|
||||||
target_sheet = None
|
target_sheet = None
|
||||||
grid_range = None
|
grid_range = None
|
||||||
if range_name:
|
if range_name:
|
||||||
grid_range = _parse_a1_range(range_name, sheets)
|
grid_range = _parse_a1_range(range_name, sheets)
|
||||||
for sheet in sheets:
|
for sheet in sheets:
|
||||||
if sheet.get("properties", {}).get("sheetId") == grid_range.get("sheetId"):
|
if (
|
||||||
|
sheet.get("properties", {}).get("sheetId")
|
||||||
|
== grid_range.get("sheetId")
|
||||||
|
):
|
||||||
target_sheet = sheet
|
target_sheet = sheet
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -925,7 +936,8 @@ async def update_conditional_formatting(
|
|||||||
rules = target_sheet.get("conditionalFormats", []) or []
|
rules = target_sheet.get("conditionalFormats", []) or []
|
||||||
if rule_index >= len(rules):
|
if rule_index >= len(rules):
|
||||||
raise UserInputError(
|
raise UserInputError(
|
||||||
f"rule_index {rule_index} is out of range for sheet '{sheet_title}' (current count: {len(rules)})."
|
f"rule_index {rule_index} is out of range for sheet "
|
||||||
|
f"'{sheet_title}' (current count: {len(rules)})."
|
||||||
)
|
)
|
||||||
|
|
||||||
existing_rule = rules[rule_index]
|
existing_rule = rules[rule_index]
|
||||||
@@ -945,9 +957,18 @@ async def update_conditional_formatting(
|
|||||||
rule_desc = "gradient"
|
rule_desc = "gradient"
|
||||||
format_desc = f"gradient points {len(gradient_points_list)}"
|
format_desc = f"gradient points {len(gradient_points_list)}"
|
||||||
elif "gradientRule" in existing_rule:
|
elif "gradientRule" in existing_rule:
|
||||||
if any([background_color, text_color, condition_type, condition_values_list]):
|
if any(
|
||||||
|
[
|
||||||
|
background_color,
|
||||||
|
text_color,
|
||||||
|
condition_type,
|
||||||
|
condition_values_list,
|
||||||
|
]
|
||||||
|
):
|
||||||
raise UserInputError(
|
raise UserInputError(
|
||||||
"Existing rule is a gradient rule. Provide gradient_points to update it, or omit formatting/condition parameters to keep it unchanged."
|
"Existing rule is a gradient rule. Provide gradient_points "
|
||||||
|
"to update it, or omit formatting/condition parameters to "
|
||||||
|
"keep it unchanged."
|
||||||
)
|
)
|
||||||
new_rule = {
|
new_rule = {
|
||||||
"ranges": ranges_to_use,
|
"ranges": ranges_to_use,
|
||||||
@@ -958,11 +979,17 @@ async def update_conditional_formatting(
|
|||||||
else:
|
else:
|
||||||
existing_boolean = existing_rule.get("booleanRule", {})
|
existing_boolean = existing_rule.get("booleanRule", {})
|
||||||
existing_condition = existing_boolean.get("condition", {})
|
existing_condition = existing_boolean.get("condition", {})
|
||||||
existing_format = copy.deepcopy(existing_boolean.get("format", {}))
|
existing_format = copy.deepcopy(
|
||||||
|
existing_boolean.get("format", {})
|
||||||
|
)
|
||||||
|
|
||||||
cond_type = (condition_type or existing_condition.get("type", "")).upper()
|
cond_type = (
|
||||||
|
condition_type or existing_condition.get("type", "")
|
||||||
|
).upper()
|
||||||
if not cond_type:
|
if not cond_type:
|
||||||
raise UserInputError("condition_type is required for boolean rules.")
|
raise UserInputError(
|
||||||
|
"condition_type is required for boolean rules."
|
||||||
|
)
|
||||||
if cond_type not in CONDITION_TYPES:
|
if cond_type not in CONDITION_TYPES:
|
||||||
raise UserInputError(
|
raise UserInputError(
|
||||||
f"condition_type must be one of {sorted(CONDITION_TYPES)}."
|
f"condition_type must be one of {sorted(CONDITION_TYPES)}."
|
||||||
@@ -970,12 +997,15 @@ async def update_conditional_formatting(
|
|||||||
|
|
||||||
if condition_values_list is not None:
|
if condition_values_list is not None:
|
||||||
cond_values = [
|
cond_values = [
|
||||||
{"userEnteredValue": str(val)} for val in condition_values_list
|
{"userEnteredValue": str(val)}
|
||||||
|
for val in condition_values_list
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
cond_values = existing_condition.get("values")
|
cond_values = existing_condition.get("values")
|
||||||
|
|
||||||
new_format = copy.deepcopy(existing_format) if existing_format else {}
|
new_format = (
|
||||||
|
copy.deepcopy(existing_format) if existing_format else {}
|
||||||
|
)
|
||||||
if background_color is not None:
|
if background_color is not None:
|
||||||
bg_color_parsed = _parse_hex_color(background_color)
|
bg_color_parsed = _parse_hex_color(background_color)
|
||||||
if bg_color_parsed:
|
if bg_color_parsed:
|
||||||
@@ -984,7 +1014,9 @@ async def update_conditional_formatting(
|
|||||||
del new_format["backgroundColor"]
|
del new_format["backgroundColor"]
|
||||||
if text_color is not None:
|
if text_color is not None:
|
||||||
text_color_parsed = _parse_hex_color(text_color)
|
text_color_parsed = _parse_hex_color(text_color)
|
||||||
text_format = copy.deepcopy(new_format.get("textFormat", {}))
|
text_format = copy.deepcopy(
|
||||||
|
new_format.get("textFormat", {})
|
||||||
|
)
|
||||||
if text_color_parsed:
|
if text_color_parsed:
|
||||||
text_format["foregroundColor"] = text_color_parsed
|
text_format["foregroundColor"] = text_color_parsed
|
||||||
elif "foregroundColor" in text_format:
|
elif "foregroundColor" in text_format:
|
||||||
@@ -995,7 +1027,9 @@ async def update_conditional_formatting(
|
|||||||
del new_format["textFormat"]
|
del new_format["textFormat"]
|
||||||
|
|
||||||
if not new_format:
|
if not new_format:
|
||||||
raise UserInputError("At least one format option must remain on the rule.")
|
raise UserInputError(
|
||||||
|
"At least one format option must remain on the rule."
|
||||||
|
)
|
||||||
|
|
||||||
new_rule = {
|
new_rule = {
|
||||||
"ranges": ranges_to_use,
|
"ranges": ranges_to_use,
|
||||||
@@ -1017,7 +1051,9 @@ async def update_conditional_formatting(
|
|||||||
"foregroundColor"
|
"foregroundColor"
|
||||||
):
|
):
|
||||||
format_parts.append("text color updated")
|
format_parts.append("text color updated")
|
||||||
format_desc = ", ".join(format_parts) if format_parts else "format preserved"
|
format_desc = (
|
||||||
|
", ".join(format_parts) if format_parts else "format preserved"
|
||||||
|
)
|
||||||
|
|
||||||
new_rules_state = copy.deepcopy(rules)
|
new_rules_state = copy.deepcopy(rules)
|
||||||
new_rules_state[rule_index] = new_rule
|
new_rules_state[rule_index] = new_rule
|
||||||
@@ -1046,47 +1082,23 @@ async def update_conditional_formatting(
|
|||||||
|
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
[
|
[
|
||||||
f"Updated conditional format at index {rule_index} on sheet '{sheet_title}' in spreadsheet {spreadsheet_id} "
|
f"Updated conditional format at index {rule_index} on sheet "
|
||||||
f"for {user_google_email}: {rule_desc}{values_desc}; format: {format_desc}.",
|
f"'{sheet_title}' in spreadsheet {spreadsheet_id} "
|
||||||
|
f"for {user_google_email}: "
|
||||||
|
f"{rule_desc}{values_desc}; format: {format_desc}.",
|
||||||
state_text,
|
state_text,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
else: # action_normalized == "delete"
|
||||||
@server.tool()
|
if rule_index is None:
|
||||||
@handle_http_errors("delete_conditional_formatting", service_type="sheets")
|
raise UserInputError("rule_index is required for action 'delete'.")
|
||||||
@require_google_service("sheets", "sheets_write")
|
|
||||||
async def delete_conditional_formatting(
|
|
||||||
service,
|
|
||||||
user_google_email: str,
|
|
||||||
spreadsheet_id: str,
|
|
||||||
rule_index: int,
|
|
||||||
sheet_name: Optional[str] = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Deletes an existing conditional formatting rule by index on a sheet.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
spreadsheet_id (str): The ID of the spreadsheet. Required.
|
|
||||||
rule_index (int): Index of the rule to delete (0-based).
|
|
||||||
sheet_name (Optional[str]): Name of the sheet that contains the rule. Defaults to the first sheet if not provided.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation of the deletion and the current rule state.
|
|
||||||
"""
|
|
||||||
logger.info(
|
|
||||||
"[delete_conditional_formatting] Invoked. Email: '%s', Spreadsheet: %s, Sheet: %s, Rule Index: %s",
|
|
||||||
user_google_email,
|
|
||||||
spreadsheet_id,
|
|
||||||
sheet_name,
|
|
||||||
rule_index,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(rule_index, int) or rule_index < 0:
|
if not isinstance(rule_index, int) or rule_index < 0:
|
||||||
raise UserInputError("rule_index must be a non-negative integer.")
|
raise UserInputError("rule_index must be a non-negative integer.")
|
||||||
|
|
||||||
sheets, sheet_titles = await _fetch_sheets_with_rules(service, spreadsheet_id)
|
sheets, sheet_titles = await _fetch_sheets_with_rules(
|
||||||
|
service, spreadsheet_id
|
||||||
|
)
|
||||||
target_sheet = _select_sheet(sheets, sheet_name)
|
target_sheet = _select_sheet(sheets, sheet_name)
|
||||||
|
|
||||||
sheet_props = target_sheet.get("properties", {})
|
sheet_props = target_sheet.get("properties", {})
|
||||||
@@ -1095,7 +1107,8 @@ async def delete_conditional_formatting(
|
|||||||
rules = target_sheet.get("conditionalFormats", []) or []
|
rules = target_sheet.get("conditionalFormats", []) or []
|
||||||
if rule_index >= len(rules):
|
if rule_index >= len(rules):
|
||||||
raise UserInputError(
|
raise UserInputError(
|
||||||
f"rule_index {rule_index} is out of range for sheet '{target_sheet_name}' (current count: {len(rules)})."
|
f"rule_index {rule_index} is out of range for sheet "
|
||||||
|
f"'{target_sheet_name}' (current count: {len(rules)})."
|
||||||
)
|
)
|
||||||
|
|
||||||
new_rules_state = copy.deepcopy(rules)
|
new_rules_state = copy.deepcopy(rules)
|
||||||
@@ -1124,7 +1137,9 @@ async def delete_conditional_formatting(
|
|||||||
|
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
[
|
[
|
||||||
f"Deleted conditional format at index {rule_index} on sheet '{target_sheet_name}' in spreadsheet {spreadsheet_id} for {user_google_email}.",
|
f"Deleted conditional format at index {rule_index} on sheet "
|
||||||
|
f"'{target_sheet_name}' in spreadsheet {spreadsheet_id} "
|
||||||
|
f"for {user_google_email}.",
|
||||||
state_text,
|
state_text,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -193,22 +193,13 @@ async def get_task_list(
|
|||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
@server.tool() # type: ignore
|
# --- Task list _impl functions ---
|
||||||
@require_google_service("tasks", "tasks") # type: ignore
|
|
||||||
@handle_http_errors("create_task_list", service_type="tasks") # type: ignore
|
|
||||||
async def create_task_list(
|
async def _create_task_list_impl(
|
||||||
service: Resource, user_google_email: str, title: str
|
service: Resource, user_google_email: str, title: str
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Implementation for creating a new task list."""
|
||||||
Create a new task list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
title (str): The title of the new task list.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message with the new task list ID and details.
|
|
||||||
"""
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[create_task_list] Invoked. Email: '{user_google_email}', Title: '{title}'"
|
f"[create_task_list] Invoked. Email: '{user_google_email}', Title: '{title}'"
|
||||||
)
|
)
|
||||||
@@ -239,23 +230,10 @@ async def create_task_list(
|
|||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
@server.tool() # type: ignore
|
async def _update_task_list_impl(
|
||||||
@require_google_service("tasks", "tasks") # type: ignore
|
|
||||||
@handle_http_errors("update_task_list", service_type="tasks") # type: ignore
|
|
||||||
async def update_task_list(
|
|
||||||
service: Resource, user_google_email: str, task_list_id: str, title: str
|
service: Resource, user_google_email: str, task_list_id: str, title: str
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Implementation for updating an existing task list."""
|
||||||
Update an existing task list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
task_list_id (str): The ID of the task list to update.
|
|
||||||
title (str): The new title for the task list.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message with updated task list details.
|
|
||||||
"""
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[update_task_list] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}, New Title: '{title}'"
|
f"[update_task_list] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}, New Title: '{title}'"
|
||||||
)
|
)
|
||||||
@@ -287,22 +265,10 @@ async def update_task_list(
|
|||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
@server.tool() # type: ignore
|
async def _delete_task_list_impl(
|
||||||
@require_google_service("tasks", "tasks") # type: ignore
|
|
||||||
@handle_http_errors("delete_task_list", service_type="tasks") # type: ignore
|
|
||||||
async def delete_task_list(
|
|
||||||
service: Resource, user_google_email: str, task_list_id: str
|
service: Resource, user_google_email: str, task_list_id: str
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Implementation for deleting a task list."""
|
||||||
Delete a task list. Note: This will also delete all tasks in the list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
task_list_id (str): The ID of the task list to delete.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message.
|
|
||||||
"""
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[delete_task_list] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}"
|
f"[delete_task_list] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}"
|
||||||
)
|
)
|
||||||
@@ -327,6 +293,105 @@ async def delete_task_list(
|
|||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
|
async def _clear_completed_tasks_impl(
|
||||||
|
service: Resource, user_google_email: str, task_list_id: str
|
||||||
|
) -> str:
|
||||||
|
"""Implementation for clearing completed tasks from a task list."""
|
||||||
|
logger.info(
|
||||||
|
f"[clear_completed_tasks] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.to_thread(service.tasks().clear(tasklist=task_list_id).execute)
|
||||||
|
|
||||||
|
response = f"All completed tasks have been cleared from task list {task_list_id} for {user_google_email}. The tasks are now hidden and won't appear in default task list views."
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Cleared completed tasks from list {task_list_id} for {user_google_email}"
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
except HttpError as error:
|
||||||
|
message = _format_reauth_message(error, user_google_email)
|
||||||
|
logger.error(message, exc_info=True)
|
||||||
|
raise Exception(message)
|
||||||
|
except Exception as e:
|
||||||
|
message = f"Unexpected error: {e}."
|
||||||
|
logger.exception(message)
|
||||||
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Consolidated manage_task_list tool ---
|
||||||
|
|
||||||
|
|
||||||
|
@server.tool() # type: ignore
|
||||||
|
@require_google_service("tasks", "tasks") # type: ignore
|
||||||
|
@handle_http_errors("manage_task_list", service_type="tasks") # type: ignore
|
||||||
|
async def manage_task_list(
|
||||||
|
service: Resource,
|
||||||
|
user_google_email: str,
|
||||||
|
action: str,
|
||||||
|
task_list_id: Optional[str] = None,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Manage task lists: create, update, delete, or clear completed tasks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_google_email (str): The user's Google email address. Required.
|
||||||
|
action (str): The action to perform. Must be one of: "create", "update", "delete", "clear_completed".
|
||||||
|
task_list_id (Optional[str]): The ID of the task list. Required for "update", "delete", and "clear_completed" actions.
|
||||||
|
title (Optional[str]): The title for the task list. Required for "create" and "update" actions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Result of the requested action.
|
||||||
|
"""
|
||||||
|
logger.info(
|
||||||
|
f"[manage_task_list] Invoked. Email: '{user_google_email}', Action: '{action}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_actions = ("create", "update", "delete", "clear_completed")
|
||||||
|
if action not in valid_actions:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if action == "create":
|
||||||
|
if not title:
|
||||||
|
raise ValueError("'title' is required for the 'create' action.")
|
||||||
|
return await _create_task_list_impl(service, user_google_email, title)
|
||||||
|
|
||||||
|
if action == "update":
|
||||||
|
if not task_list_id:
|
||||||
|
raise ValueError("'task_list_id' is required for the 'update' action.")
|
||||||
|
if not title:
|
||||||
|
raise ValueError("'title' is required for the 'update' action.")
|
||||||
|
return await _update_task_list_impl(
|
||||||
|
service, user_google_email, task_list_id, title
|
||||||
|
)
|
||||||
|
|
||||||
|
if action == "delete":
|
||||||
|
if not task_list_id:
|
||||||
|
raise ValueError("'task_list_id' is required for the 'delete' action.")
|
||||||
|
return await _delete_task_list_impl(service, user_google_email, task_list_id)
|
||||||
|
|
||||||
|
# action == "clear_completed"
|
||||||
|
if not task_list_id:
|
||||||
|
raise ValueError(
|
||||||
|
"'task_list_id' is required for the 'clear_completed' action."
|
||||||
|
)
|
||||||
|
return await _clear_completed_tasks_impl(service, user_google_email, task_list_id)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Legacy task list tools (wrappers around _impl functions) ---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@server.tool() # type: ignore
|
@server.tool() # type: ignore
|
||||||
@require_google_service("tasks", "tasks_read") # type: ignore
|
@require_google_service("tasks", "tasks_read") # type: ignore
|
||||||
@handle_http_errors("list_tasks", service_type="tasks") # type: ignore
|
@handle_http_errors("list_tasks", service_type="tasks") # type: ignore
|
||||||
@@ -633,10 +698,10 @@ async def get_task(
|
|||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
@server.tool() # type: ignore
|
# --- Task _impl functions ---
|
||||||
@require_google_service("tasks", "tasks") # type: ignore
|
|
||||||
@handle_http_errors("create_task", service_type="tasks") # type: ignore
|
|
||||||
async def create_task(
|
async def _create_task_impl(
|
||||||
service: Resource,
|
service: Resource,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
task_list_id: str,
|
task_list_id: str,
|
||||||
@@ -646,21 +711,7 @@ async def create_task(
|
|||||||
parent: Optional[str] = None,
|
parent: Optional[str] = None,
|
||||||
previous: Optional[str] = None,
|
previous: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Implementation for creating a new task in a task list."""
|
||||||
Create a new task in a task list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
task_list_id (str): The ID of the task list to create the task in.
|
|
||||||
title (str): The title of the task.
|
|
||||||
notes (Optional[str]): Notes/description for the task.
|
|
||||||
due (Optional[str]): Due date in RFC 3339 format (e.g., "2024-12-31T23:59:59Z").
|
|
||||||
parent (Optional[str]): Parent task ID (for subtasks).
|
|
||||||
previous (Optional[str]): Previous sibling task ID (for positioning).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message with the new task ID and details.
|
|
||||||
"""
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[create_task] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}, Title: '{title}'"
|
f"[create_task] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}, Title: '{title}'"
|
||||||
)
|
)
|
||||||
@@ -708,10 +759,7 @@ async def create_task(
|
|||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
@server.tool() # type: ignore
|
async def _update_task_impl(
|
||||||
@require_google_service("tasks", "tasks") # type: ignore
|
|
||||||
@handle_http_errors("update_task", service_type="tasks") # type: ignore
|
|
||||||
async def update_task(
|
|
||||||
service: Resource,
|
service: Resource,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
task_list_id: str,
|
task_list_id: str,
|
||||||
@@ -721,21 +769,7 @@ async def update_task(
|
|||||||
status: Optional[str] = None,
|
status: Optional[str] = None,
|
||||||
due: Optional[str] = None,
|
due: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Implementation for updating an existing task."""
|
||||||
Update an existing task.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
task_list_id (str): The ID of the task list containing the task.
|
|
||||||
task_id (str): The ID of the task to update.
|
|
||||||
title (Optional[str]): New title for the task.
|
|
||||||
notes (Optional[str]): New notes/description for the task.
|
|
||||||
status (Optional[str]): New status ("needsAction" or "completed").
|
|
||||||
due (Optional[str]): New due date in RFC 3339 format.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message with updated task details.
|
|
||||||
"""
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[update_task] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}, Task ID: {task_id}"
|
f"[update_task] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}, Task ID: {task_id}"
|
||||||
)
|
)
|
||||||
@@ -796,23 +830,10 @@ async def update_task(
|
|||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
@server.tool() # type: ignore
|
async def _delete_task_impl(
|
||||||
@require_google_service("tasks", "tasks") # type: ignore
|
|
||||||
@handle_http_errors("delete_task", service_type="tasks") # type: ignore
|
|
||||||
async def delete_task(
|
|
||||||
service: Resource, user_google_email: str, task_list_id: str, task_id: str
|
service: Resource, user_google_email: str, task_list_id: str, task_id: str
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Implementation for deleting a task from a task list."""
|
||||||
Delete a task from a task list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
task_list_id (str): The ID of the task list containing the task.
|
|
||||||
task_id (str): The ID of the task to delete.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message.
|
|
||||||
"""
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[delete_task] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}, Task ID: {task_id}"
|
f"[delete_task] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}, Task ID: {task_id}"
|
||||||
)
|
)
|
||||||
@@ -837,10 +858,7 @@ async def delete_task(
|
|||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
@server.tool() # type: ignore
|
async def _move_task_impl(
|
||||||
@require_google_service("tasks", "tasks") # type: ignore
|
|
||||||
@handle_http_errors("move_task", service_type="tasks") # type: ignore
|
|
||||||
async def move_task(
|
|
||||||
service: Resource,
|
service: Resource,
|
||||||
user_google_email: str,
|
user_google_email: str,
|
||||||
task_list_id: str,
|
task_list_id: str,
|
||||||
@@ -849,20 +867,7 @@ async def move_task(
|
|||||||
previous: Optional[str] = None,
|
previous: Optional[str] = None,
|
||||||
destination_task_list: Optional[str] = None,
|
destination_task_list: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""Implementation for moving a task to a different position, parent, or list."""
|
||||||
Move a task to a different position or parent within the same list, or to a different list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_google_email (str): The user's Google email address. Required.
|
|
||||||
task_list_id (str): The ID of the current task list containing the task.
|
|
||||||
task_id (str): The ID of the task to move.
|
|
||||||
parent (Optional[str]): New parent task ID (for making it a subtask).
|
|
||||||
previous (Optional[str]): Previous sibling task ID (for positioning).
|
|
||||||
destination_task_list (Optional[str]): Destination task list ID (for moving between lists).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Confirmation message with updated task details.
|
|
||||||
"""
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[move_task] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}, Task ID: {task_id}"
|
f"[move_task] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}, Task ID: {task_id}"
|
||||||
)
|
)
|
||||||
@@ -913,41 +918,112 @@ async def move_task(
|
|||||||
raise Exception(message)
|
raise Exception(message)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Consolidated manage_task tool ---
|
||||||
|
|
||||||
|
|
||||||
@server.tool() # type: ignore
|
@server.tool() # type: ignore
|
||||||
@require_google_service("tasks", "tasks") # type: ignore
|
@require_google_service("tasks", "tasks") # type: ignore
|
||||||
@handle_http_errors("clear_completed_tasks", service_type="tasks") # type: ignore
|
@handle_http_errors("manage_task", service_type="tasks") # type: ignore
|
||||||
async def clear_completed_tasks(
|
async def manage_task(
|
||||||
service: Resource, user_google_email: str, task_list_id: str
|
service: Resource,
|
||||||
|
user_google_email: str,
|
||||||
|
action: str,
|
||||||
|
task_list_id: str,
|
||||||
|
task_id: Optional[str] = None,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
notes: Optional[str] = None,
|
||||||
|
status: Optional[str] = None,
|
||||||
|
due: Optional[str] = None,
|
||||||
|
parent: Optional[str] = None,
|
||||||
|
previous: Optional[str] = None,
|
||||||
|
destination_task_list: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Clear all completed tasks from a task list. The tasks will be marked as hidden.
|
Manage tasks: create, update, delete, or move tasks within task lists.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_google_email (str): The user's Google email address. Required.
|
user_google_email (str): The user's Google email address. Required.
|
||||||
task_list_id (str): The ID of the task list to clear completed tasks from.
|
action (str): The action to perform. Must be one of: "create", "update", "delete", "move".
|
||||||
|
task_list_id (str): The ID of the task list. Required for all actions.
|
||||||
|
task_id (Optional[str]): The ID of the task. Required for "update", "delete", and "move" actions.
|
||||||
|
title (Optional[str]): The title of the task. Required for "create", optional for "update".
|
||||||
|
notes (Optional[str]): Notes/description for the task. Used by "create" and "update" actions.
|
||||||
|
status (Optional[str]): Task status ("needsAction" or "completed"). Used by "update" action.
|
||||||
|
due (Optional[str]): Due date in RFC 3339 format (e.g., "2024-12-31T23:59:59Z"). Used by "create" and "update" actions.
|
||||||
|
parent (Optional[str]): Parent task ID (for subtasks). Used by "create" and "move" actions.
|
||||||
|
previous (Optional[str]): Previous sibling task ID (for positioning). Used by "create" and "move" actions.
|
||||||
|
destination_task_list (Optional[str]): Destination task list ID (for moving between lists). Used by "move" action.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Confirmation message.
|
str: Result of the requested action.
|
||||||
"""
|
"""
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[clear_completed_tasks] Invoked. Email: '{user_google_email}', Task List ID: {task_list_id}"
|
f"[manage_task] Invoked. Email: '{user_google_email}', Action: '{action}', Task List ID: {task_list_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
valid_actions = ("create", "update", "delete", "move")
|
||||||
await asyncio.to_thread(service.tasks().clear(tasklist=task_list_id).execute)
|
if action not in valid_actions:
|
||||||
|
raise ValueError(
|
||||||
response = f"All completed tasks have been cleared from task list {task_list_id} for {user_google_email}. The tasks are now hidden and won't appear in default task list views."
|
f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}"
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"Cleared completed tasks from list {task_list_id} for {user_google_email}"
|
|
||||||
)
|
)
|
||||||
return response
|
|
||||||
|
|
||||||
except HttpError as error:
|
if action == "create":
|
||||||
message = _format_reauth_message(error, user_google_email)
|
if not title:
|
||||||
logger.error(message, exc_info=True)
|
raise ValueError("'title' is required for the 'create' action.")
|
||||||
raise Exception(message)
|
return await _create_task_impl(
|
||||||
except Exception as e:
|
service,
|
||||||
message = f"Unexpected error: {e}."
|
user_google_email,
|
||||||
logger.exception(message)
|
task_list_id,
|
||||||
raise Exception(message)
|
title,
|
||||||
|
notes=notes,
|
||||||
|
due=due,
|
||||||
|
parent=parent,
|
||||||
|
previous=previous,
|
||||||
|
)
|
||||||
|
|
||||||
|
if action == "update":
|
||||||
|
if not task_id:
|
||||||
|
raise ValueError("'task_id' is required for the 'update' action.")
|
||||||
|
return await _update_task_impl(
|
||||||
|
service,
|
||||||
|
user_google_email,
|
||||||
|
task_list_id,
|
||||||
|
task_id,
|
||||||
|
title=title,
|
||||||
|
notes=notes,
|
||||||
|
status=status,
|
||||||
|
due=due,
|
||||||
|
)
|
||||||
|
|
||||||
|
if action == "delete":
|
||||||
|
if not task_id:
|
||||||
|
raise ValueError("'task_id' is required for the 'delete' action.")
|
||||||
|
return await _delete_task_impl(
|
||||||
|
service, user_google_email, task_list_id, task_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# action == "move"
|
||||||
|
if not task_id:
|
||||||
|
raise ValueError("'task_id' is required for the 'move' action.")
|
||||||
|
return await _move_task_impl(
|
||||||
|
service,
|
||||||
|
user_google_email,
|
||||||
|
task_list_id,
|
||||||
|
task_id,
|
||||||
|
parent=parent,
|
||||||
|
previous=previous,
|
||||||
|
destination_task_list=destination_task_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Legacy task tools (wrappers around _impl functions) ---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user