diff --git a/gdrive/drive_tools.py b/gdrive/drive_tools.py index 81f33f80..5934f9dc 100644 --- a/gdrive/drive_tools.py +++ b/gdrive/drive_tools.py @@ -2169,3 +2169,48 @@ async def transfer_drive_ownership( moved_to_new_owners_root=move_to_new_owners_root, ) return create_tool_result(text=text_output, data=result) + + +# ── MCP Resource Templates ────────────────────────────────────────────── + + +@server.resource( + "gdrive://files/{file_id}", + name="drive_file", + description=( + "Raw file content from Google Drive, returned as base64-encoded bytes. " + "Google Docs/Sheets/Slides are exported as PDF (Sheets as XLSX)." + ), + mime_type="application/octet-stream", +) +@require_google_service("drive", "drive_read") +async def drive_file_resource( + service, + file_id: str, +) -> bytes: + """Fetch raw file bytes from Google Drive.""" + resolved_file_id, file_metadata = await resolve_drive_item(service, file_id) + mime_type = file_metadata.get("mimeType", "") + + export_mime = { + "application/vnd.google-apps.document": "application/pdf", + "application/vnd.google-apps.spreadsheet": ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ), + "application/vnd.google-apps.presentation": "application/pdf", + }.get(mime_type) + + request_obj = ( + service.files().export_media(fileId=resolved_file_id, mimeType=export_mime) + if export_mime + else service.files().get_media(fileId=resolved_file_id) + ) + + fh = io.BytesIO() + downloader = MediaIoBaseDownload(fh, request_obj) + loop = asyncio.get_event_loop() + done = False + while not done: + _, done = await loop.run_in_executor(None, downloader.next_chunk) + + return fh.getvalue() diff --git a/gmail/gmail_tools.py b/gmail/gmail_tools.py index 076cae19..1337ba28 100644 --- a/gmail/gmail_tools.py +++ b/gmail/gmail_tools.py @@ -1968,3 +1968,30 @@ async def batch_modify_gmail_message_labels( actions.append(f"Removed labels: {', '.join(remove_label_ids)}") return f"Labels updated for {len(message_ids)} messages: {'; '.join(actions)}" + + +# ── MCP Resource Templates ────────────────────────────────────────────── + + +@server.resource( + "gmail://messages/{message_id}/attachments/{attachment_id}", + name="gmail_attachment", + description="Raw attachment content from a Gmail message, returned as base64-encoded bytes.", + mime_type="application/octet-stream", +) +@require_google_service("gmail", "gmail_read") +async def gmail_attachment_resource( + service, + message_id: str, + attachment_id: str, +) -> bytes: + """Fetch raw attachment bytes from a Gmail message.""" + attachment = await asyncio.to_thread( + service.users() + .messages() + .attachments() + .get(userId="me", messageId=message_id, id=attachment_id) + .execute + ) + base64_data = attachment.get("data", "") + return base64.urlsafe_b64decode(base64_data) diff --git a/schemas/drive.json b/schemas/drive.json index e2fe575f..368f0794 100644 --- a/schemas/drive.json +++ b/schemas/drive.json @@ -1560,5 +1560,18 @@ } } } + ], + "resourceTemplates": [ + { + "name": "drive_file", + "uriTemplate": "gdrive://files/{file_id}", + "description": "Raw file content from Google Drive, returned as base64-encoded bytes. Google Docs/Sheets/Slides are exported as PDF (Sheets as XLSX).", + "mimeType": "application/octet-stream", + "_meta": { + "_fastmcp": { + "tags": [] + } + } + } ] } diff --git a/schemas/gmail.json b/schemas/gmail.json index 148a8515..decf1466 100644 --- a/schemas/gmail.json +++ b/schemas/gmail.json @@ -1017,5 +1017,18 @@ } } } + ], + "resourceTemplates": [ + { + "name": "gmail_attachment", + "uriTemplate": "gmail://messages/{message_id}/attachments/{attachment_id}", + "description": "Raw attachment content from a Gmail message, returned as base64-encoded bytes.", + "mimeType": "application/octet-stream", + "_meta": { + "_fastmcp": { + "tags": [] + } + } + } ] }