Skip to content

feat(gmail): add list_gmail_drafts and delete_gmail_draft tools#781

Open
bcsmith528 wants to merge 1 commit into
taylorwilsdon:mainfrom
bcsmith528:feat/gmail-list-and-delete-drafts
Open

feat(gmail): add list_gmail_drafts and delete_gmail_draft tools#781
bcsmith528 wants to merge 1 commit into
taylorwilsdon:mainfrom
bcsmith528:feat/gmail-list-and-delete-drafts

Conversation

@bcsmith528
Copy link
Copy Markdown

@bcsmith528 bcsmith528 commented May 11, 2026

Summary

Adds two tools that surface Gmail API endpoints not currently exposed by this server:

  • list_gmail_drafts — wraps users.drafts.list with optional Gmail-search query filtering and pagination. For each draft, resolves the underlying message metadata (Subject, From, To, Cc, Date, snippet, message/thread IDs) so callers can identify drafts without an extra round-trip.
  • delete_gmail_draft — wraps users.drafts.delete, permanently removing the draft container and its underlying message.

Both tools live at the gmail.compose permission level (same scope as the existing draft_gmail_message), so they require no new scopes when granted at the drafts service level or above. Both are registered in the extended tier in core/tool_tiers.yaml and added to the README tools table.

Motivation

Today the only draft-related tool is draft_gmail_message. Once a draft is created there's no way through MCP to enumerate drafts or remove them, which means anything that creates a draft (e.g. an agent preparing emails for human review) accumulates clutter that the user has to clean up by hand in the Gmail web UI. Adding these two endpoints closes the gap and keeps the draft lifecycle self-contained within the server's existing scope grant.

Test plan

  • Unit tests added in tests/gmail/test_list_and_delete_drafts.py covering:
    • formatted output for a non-empty draft list (Subject/From/To resolution)
    • empty-account case (No drafts found.)
    • propagation of query parameter to users.drafts.list
    • propagation of page_token and emission of nextPageToken instructions
    • delete_gmail_draft invokes users.drafts.delete with the correct id and userId
  • Full tests/gmail/ suite passes (119 tests) locally
  • ruff check clean on touched files

Notes

  • list_gmail_drafts makes one drafts.list call plus one drafts.get per returned draft. For typical drafts folders this is small; users with very large drafts folders should rely on query filtering or page_size to keep the round-trip count bounded. A future improvement could batch the drafts.get calls through Gmail's HTTP batch endpoint.
  • delete_gmail_draft is annotated destructiveHint=True, idempotentHint=True. Idempotent because deleting a non-existent draft returns a 404 that handle_http_errors surfaces predictably.

Summary by CodeRabbit

  • New Features

    • List Gmail drafts with optional search filtering and configurable pagination
    • Permanently delete specific Gmail drafts by ID
    • View draft metadata including subject, recipients, and date information
  • Documentation

    • Updated tool reference documentation to include new draft management capabilities

Review Change Stack

Surfaces two endpoints from the Gmail API that were not previously exposed:

- list_gmail_drafts: wraps users.drafts.list with optional Gmail-search
  query filtering and pagination. Resolves each draft to its underlying
  message metadata (subject, from, to, cc, date, snippet, message/thread
  IDs) so callers can identify drafts without a second round-trip.
- delete_gmail_draft: wraps users.drafts.delete, permanently removing a
  draft container and its underlying message.

Both tools live at the gmail.compose permission level (same scope as the
existing draft_gmail_message), so they require no new scopes when granted
at the "drafts" service level or above. Added to the extended tier in
tool_tiers.yaml and the README tools table.

Includes tests covering: formatted output for non-empty drafts, the empty-
account case, query and page_token propagation to the underlying API call,
and that delete invokes drafts.delete with the correct draft_id and userId.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

📝 Walkthrough

Walkthrough

This PR adds two new Gmail MCP tools for draft management. list_gmail_drafts retrieves a paginated list of drafts with metadata (subject, recipients, dates, IDs), supports Gmail search queries, and handles pagination. delete_gmail_draft permanently removes a draft by ID. Both tools are registered in the extended tier, implemented with complete API integration, tested comprehensively, and documented in the README.

Changes

Gmail Draft Management Tools

Layer / File(s) Summary
Tool Registration
core/tool_tiers.yaml
list_gmail_drafts and delete_gmail_draft are added to the gmail.extended tier.
List Drafts Implementation
gmail/gmail_tools.py
list_gmail_drafts lists drafts with optional query filtering and pagination; fetches metadata (Subject, From, To, Cc, Date, IDs) for each draft; formats results with draft count and per-draft details; includes next-page token when present.
Delete Draft Implementation
gmail/gmail_tools.py
delete_gmail_draft calls the Gmail drafts delete API endpoint with the draft ID and returns a success confirmation message.
Test Coverage
tests/gmail/test_list_and_delete_drafts.py
Helpers unwrap decorators and construct draft payloads. Tests verify list drafts formatting, empty account handling, query parameter forwarding, pagination token propagation, and delete confirmation.
Documentation
README.md
Tool reference entries added for the two new Extended-tier Gmail draft tools.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

enhancement

Suggested reviewers

  • taylorwilsdon

Poem

🐰 Drafts now dance in Gmail's domain,
With lists to browse and delete to claim,
Paginated pages, metadata bright,
Draft management tools—a rabbit's delight!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description covers motivation, implementation details, and test plan, but does not follow the required template structure with checkboxes and checklist items. Restructure the description to match the template format, including Type of Change selection, Testing checkboxes, and the required Checklist section confirming 'Allow edits from maintainers' is enabled.
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding two new Gmail tools for listing and deleting drafts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gmail/gmail_tools.py`:
- Around line 2501-2506: The page_size parameter currently allows unbounded
values which can trigger massive drafts.get fan-out; add a hard upper bound
(e.g., 100) to the page_size Field and/or validate/clamp it before making calls
to drafts.get to prevent long-running listing paths. Specifically, update the
page_size Field definition (the Annotated Field at page_size) to include an
upper limit (use le or max in the Field) and ensure the code path that
iterates/calls drafts.get (the draft listing logic referenced around lines
2535-2543) enforces the same cap or returns a 400 error when page_size exceeds
the limit; also update the description to document the maximum accepted value.
- Around line 2514-2530: Replace the multi-paragraph docstring for the
drafts-listing tool (the docstring that begins "Lists drafts in the user's Gmail
account...") with a single present-tense sentence that describes the tool's
purpose, and remove parameter explanations from the docstring; instead ensure
parameter hints appear in the function signature/type annotations or the tool's
argument metadata. Apply the same change to the other multi-paragraph docstring
in gmail_tools.py around the later drafts-related tool (the block currently
spanning lines ~2631-2643), making each tool description one present-tense
sentence and moving parameter details into argument metadata/type hints. Ensure
formatting remains a single-line docstring per tool.
- Line 2492: Rename the new tool identifiers that use snake_case to imperative
camelCase (≤3 words): change the function named list_gmail_drafts to
listGmailDrafts and likewise rename the other snake_case tool introduced around
line 2620 to an imperative camelCase name (e.g., listGmailMessages or similar);
update every reference, import/export, and any registration or tool mapping that
uses the old names (including decorator usages, exported lists, and tests) so
the code and MCP surface remain consistent with the tool naming rule.
- Around line 2597-2599: The except HttpError handler that appends per-draft
error text should not swallow Gmail API failures; replace the current block that
does "lines.append(f\"... (failed to fetch metadata: {e})\")" with logic that
maps or raises a ToolExecutionError for HttpError (e.g., use the project's
Google API error-mapping helper if available, otherwise raise ToolExecutionError
with the HttpError details and context about the draft_id). Ensure you reference
the HttpError caught in this block and raise ToolExecutionError instead of
returning partial success text so centralized error handling can detect
rate-limit/quota and other API failures.

In `@tests/gmail/test_list_and_delete_drafts.py`:
- Around line 44-142: Tests in this file currently use chained Mock objects
instead of the repo-standard httpretty + canned fixtures approach; replace the
Mock-based Gmail API stubs in tests referencing list_gmail_drafts,
delete_gmail_draft (and helper _draft_metadata) with httpretty HTTP mocks that
load the canned JSON fixtures for the Gmail endpoints (/gmail/v1/users/me/drafts
list, get and delete) and return the same payloads used now; ensure the
httpretty responses assert/accept query params (q, pageToken, userId) and return
nextPageToken or empty bodies as appropriate so the existing assertions (e.g.,
checking "matching 'subject:invoice'", page_token='token-next', and delete
confirmation) continue to pass.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b7562891-5633-44e8-95bf-7a65c666d9f3

📥 Commits

Reviewing files that changed from the base of the PR and between f73c75b and 7ed8fc5.

📒 Files selected for processing (4)
  • README.md
  • core/tool_tiers.yaml
  • gmail/gmail_tools.py
  • tests/gmail/test_list_and_delete_drafts.py

Comment thread gmail/gmail_tools.py
)
@handle_http_errors("list_gmail_drafts", is_read_only=True, service_type="gmail")
@require_google_service("gmail", GMAIL_COMPOSE_SCOPE)
async def list_gmail_drafts(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Rename new tool identifiers to imperative camelCase.

Line 2492 and Line 2620 introduce snake_case names, which violate the tool naming rule and make the MCP surface inconsistent with the required schema convention.

As per coding guidelines **/*.py: "Tool names must be imperative, camelCase, and ≤3 words".

Also applies to: 2620-2620

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gmail/gmail_tools.py` at line 2492, Rename the new tool identifiers that use
snake_case to imperative camelCase (≤3 words): change the function named
list_gmail_drafts to listGmailDrafts and likewise rename the other snake_case
tool introduced around line 2620 to an imperative camelCase name (e.g.,
listGmailMessages or similar); update every reference, import/export, and any
registration or tool mapping that uses the old names (including decorator
usages, exported lists, and tests) so the code and MCP surface remain consistent
with the tool naming rule.

Comment thread gmail/gmail_tools.py
Comment on lines +2501 to +2506
page_size: Annotated[
int,
Field(
description="Maximum number of drafts to return. Defaults to 25.",
),
] = 25,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a hard upper bound for page_size to prevent slow draft listing paths.

Line 2501 accepts unbounded values, which can trigger very large drafts.get fan-out and long request times.

Proposed guard
 async def list_gmail_drafts(
@@
     page_size: Annotated[
         int,
         Field(
             description="Maximum number of drafts to return. Defaults to 25.",
         ),
     ] = 25,
@@
 ) -> str:
@@
+    if page_size < 1 or page_size > 100:
+        raise UserInputError("page_size must be between 1 and 100.")
+
     list_kwargs = {"userId": "me", "maxResults": page_size}
As per coding guidelines `**/*.py`: "Avoid long-running operations (>30s) inside request context; stream partial results or schedule background tasks instead".

Also applies to: 2535-2543

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gmail/gmail_tools.py` around lines 2501 - 2506, The page_size parameter
currently allows unbounded values which can trigger massive drafts.get fan-out;
add a hard upper bound (e.g., 100) to the page_size Field and/or validate/clamp
it before making calls to drafts.get to prevent long-running listing paths.
Specifically, update the page_size Field definition (the Annotated Field at
page_size) to include an upper limit (use le or max in the Field) and ensure the
code path that iterates/calls drafts.get (the draft listing logic referenced
around lines 2535-2543) enforces the same cap or returns a 400 error when
page_size exceeds the limit; also update the description to document the maximum
accepted value.

Comment thread gmail/gmail_tools.py
Comment on lines +2514 to +2530
"""
Lists drafts in the user's Gmail account, optionally filtered by a Gmail search query.

For each draft, returns the Draft ID along with the underlying message's Subject,
From, To, and a short snippet so you can identify which draft is which without
needing to fetch each one separately.

Args:
user_google_email (str): The user's Google email address. Required.
query (Optional[str]): Optional Gmail search query to filter the drafts list.
page_size (int): Maximum number of drafts to return. Defaults to 25.
page_token (Optional[str]): Pagination token from a previous response.

Returns:
str: A formatted list of drafts with their Draft IDs and key headers, or a
message indicating no drafts were found.
"""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Tool descriptions should be reduced to a single present-tense sentence.

Line 2514 and Line 2631 currently use multi-paragraph docstrings; these should be concise one-sentence tool descriptions with parameter hints in argument metadata.

As per coding guidelines **/*.py: "Tool descriptions must be a single sentence in present tense with parameter hints included".

Also applies to: 2631-2643

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gmail/gmail_tools.py` around lines 2514 - 2530, Replace the multi-paragraph
docstring for the drafts-listing tool (the docstring that begins "Lists drafts
in the user's Gmail account...") with a single present-tense sentence that
describes the tool's purpose, and remove parameter explanations from the
docstring; instead ensure parameter hints appear in the function signature/type
annotations or the tool's argument metadata. Apply the same change to the other
multi-paragraph docstring in gmail_tools.py around the later drafts-related tool
(the block currently spanning lines ~2631-2643), making each tool description
one present-tense sentence and moving parameter details into argument
metadata/type hints. Ensure formatting remains a single-line docstring per tool.

Comment thread gmail/gmail_tools.py
Comment on lines +2597 to +2599
except HttpError as e:
lines.append(f"{idx}. Draft ID: {draft_id} (failed to fetch metadata: {e})")
lines.append("")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not swallow Gmail API failures inside per-draft metadata fetch.

Line 2597 converts HttpError into text output, which bypasses centralized tool error mapping and can return partial-success responses during API failure conditions.

Proposed handling adjustment
-        except HttpError as e:
-            lines.append(f"{idx}. Draft ID: {draft_id} (failed to fetch metadata: {e})")
-            lines.append("")
+        except HttpError:
+            raise
+        except Exception as e:
+            lines.append(f"{idx}. Draft ID: {draft_id} (failed to fetch metadata: {e})")
+            lines.append("")
As per coding guidelines `**/*.py`: "Catch rate-limit and quota errors from Google APIs and surface them as MCP ToolExecutionError".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gmail/gmail_tools.py` around lines 2597 - 2599, The except HttpError handler
that appends per-draft error text should not swallow Gmail API failures; replace
the current block that does "lines.append(f\"... (failed to fetch metadata:
{e})\")" with logic that maps or raises a ToolExecutionError for HttpError
(e.g., use the project's Google API error-mapping helper if available, otherwise
raise ToolExecutionError with the HttpError details and context about the
draft_id). Ensure you reference the HttpError caught in this block and raise
ToolExecutionError instead of returning partial success text so centralized
error handling can detect rate-limit/quota and other API failures.

Comment on lines +44 to +142
mock_service = Mock()
mock_service.users().drafts().list().execute.return_value = {
"drafts": [
{"id": "r123", "message": {"id": "msg_r123", "threadId": "t1"}},
{"id": "r456", "message": {"id": "msg_r456", "threadId": "t2"}},
]
}
mock_service.users().drafts().get().execute.side_effect = [
_draft_metadata("r123", "First draft", "alice@example.com", snippet="Hi Alice"),
_draft_metadata("r456", "Second draft", "bob@example.com", snippet="Hi Bob"),
]

result = await _unwrap(list_gmail_drafts)(
service=mock_service,
user_google_email="user@example.com",
)

assert "Found 2 draft(s)" in result
assert "Draft ID: r123" in result
assert "First draft" in result
assert "alice@example.com" in result
assert "Draft ID: r456" in result
assert "Second draft" in result
assert "bob@example.com" in result


@pytest.mark.asyncio
async def test_list_gmail_drafts_empty_account():
mock_service = Mock()
mock_service.users().drafts().list().execute.return_value = {}

result = await _unwrap(list_gmail_drafts)(
service=mock_service,
user_google_email="user@example.com",
)

assert result == "No drafts found."


@pytest.mark.asyncio
async def test_list_gmail_drafts_passes_query_to_api():
mock_service = Mock()
mock_service.users().drafts().list().execute.return_value = {}

result = await _unwrap(list_gmail_drafts)(
service=mock_service,
user_google_email="user@example.com",
query="subject:invoice",
)

assert "matching 'subject:invoice'" in result
list_kwargs = (
mock_service.users.return_value.drafts.return_value.list.call_args.kwargs
)
assert list_kwargs["q"] == "subject:invoice"
assert list_kwargs["userId"] == "me"


@pytest.mark.asyncio
async def test_list_gmail_drafts_propagates_page_token():
mock_service = Mock()
mock_service.users().drafts().list().execute.return_value = {
"drafts": [{"id": "r1", "message": {"id": "msg_r1", "threadId": "t1"}}],
"nextPageToken": "token-next",
}
mock_service.users().drafts().get().execute.return_value = _draft_metadata(
"r1", "Subject", "to@example.com"
)

result = await _unwrap(list_gmail_drafts)(
service=mock_service,
user_google_email="user@example.com",
page_token="token-current",
)

list_kwargs = (
mock_service.users.return_value.drafts.return_value.list.call_args.kwargs
)
assert list_kwargs["pageToken"] == "token-current"
assert "page_token='token-next'" in result


@pytest.mark.asyncio
async def test_delete_gmail_draft_calls_api_and_confirms():
mock_service = Mock()
mock_service.users().drafts().delete().execute.return_value = None

result = await _unwrap(delete_gmail_draft)(
service=mock_service,
user_google_email="user@example.com",
draft_id="r_abc123",
)

assert "Draft r_abc123 deleted successfully." == result
delete_kwargs = (
mock_service.users.return_value.drafts.return_value.delete.call_args.kwargs
)
assert delete_kwargs["id"] == "r_abc123"
assert delete_kwargs["userId"] == "me"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Use httpretty + canned fixtures for Google API mocking in this module.

Current direct Mock chains don't follow the repository’s required test mocking strategy for Google APIs.

As per coding guidelines tests/**/*.py: "Mock Google APIs with httpretty and canned fixtures; do not hit live services in CI".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/gmail/test_list_and_delete_drafts.py` around lines 44 - 142, Tests in
this file currently use chained Mock objects instead of the repo-standard
httpretty + canned fixtures approach; replace the Mock-based Gmail API stubs in
tests referencing list_gmail_drafts, delete_gmail_draft (and helper
_draft_metadata) with httpretty HTTP mocks that load the canned JSON fixtures
for the Gmail endpoints (/gmail/v1/users/me/drafts list, get and delete) and
return the same payloads used now; ensure the httpretty responses assert/accept
query params (q, pageToken, userId) and return nextPageToken or empty bodies as
appropriate so the existing assertions (e.g., checking "matching
'subject:invoice'", page_token='token-next', and delete confirmation) continue
to pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant