
Google Drive
- 771 installs
- 364 repo stars
- Updated July 9, 2026
- sanjay3290/ai-skills
google-drive is an agent skill that searches, lists, uploads, and downloads Google Drive files via OAuth scripts for developers who need Workspace file automation without deploying a Google Drive MCP server.
About
google-drive is an Apache-2.0 agent skill (version 1.0) in sanjay3290/ai-skills providing lightweight Google Drive integration with standalone OAuth—no MCP server required. It requires a Google Workspace account; personal Gmail accounts are not supported. Operations run through scripts/drive.py for search, find-folder, list, download, upload, create folders, move, copy, rename, and trash, with first-time auth via scripts/auth.py login. Search accepts full-text queries, title filters, Drive URLs, folder IDs, and native mimeType pass-through syntax. Google Docs, Sheets, and Slides cannot be downloaded directly; binary files use long Drive file IDs. Developers reach for google-drive when agents must fetch spec PDFs, upload build artifacts, or organize project folders in Workspace Drives during automation or internal tooling work.
- google-drive
Google Drive by the numbers
- 771 all-time installs (skills.sh)
- +7 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #500 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/sanjay3290/ai-skills --skill google-driveAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 771 |
|---|---|
| repo stars | ★ 364 |
| Last updated | July 9, 2026 |
| Repository | sanjay3290/ai-skills ↗ |
How do agents integrate Google Drive file operations?
Use google-drive for development tasks
Who is it for?
Developers automating Google Workspace Drive search, upload, and folder management inside agent sessions without running a dedicated MCP server.
Skip if: Developers on personal Gmail accounts or anyone needing native Google Docs/Sheets/Slides export beyond binary download limitations.
When should I use this skill?
The user asks to search Google Drive, list or download files, upload documents, create folders, or organize Drive content from an agent.
What you get
Authenticated Drive file listings, search results, uploaded files, and local downloads via scripts/drive.py commands.
- Drive search results
- uploaded or downloaded files
- organized folder listings
By the numbers
- Apache-2.0 skill version 1.0
- Supports 5 search query formats including full-text, title, URL, folder ID, and mimeType
Files
Google Drive
Lightweight Google Drive integration with standalone OAuth authentication. No MCP server required. Full read/write access.
Requires Google Workspace account. Personal Gmail accounts are not supported.
First-Time Setup
Authenticate with Google (opens browser):
python scripts/auth.py loginCheck authentication status:
python scripts/auth.py statusLogout when needed:
python scripts/auth.py logoutRead Commands
All operations via scripts/drive.py. Auto-authenticates on first use if not logged in.
# Search for files (full-text search)
python scripts/drive.py search "quarterly report"
# Search by title only
python scripts/drive.py search "title:budget"
# Search using Google Drive URL (extracts ID automatically)
python scripts/drive.py search "https://drive.google.com/drive/folders/1ABC123..."
# Search files shared with you
python scripts/drive.py search --shared-with-me
# Search with pagination
python scripts/drive.py search "report" --limit 5 --page-token "..."
# Find a folder by exact name
python scripts/drive.py find-folder "Project Documents"
# List files in root Drive
python scripts/drive.py list
# List files in a specific folder
python scripts/drive.py list 1ABC123xyz --limit 20
# Download a file
python scripts/drive.py download 1ABC123xyz ./downloads/report.pdfWrite Commands
# Upload a file to Drive root
python scripts/drive.py upload ~/Documents/report.pdf
# Upload to a specific folder
python scripts/drive.py upload ~/Documents/report.pdf --folder 1ABC123xyz
# Upload with a custom name
python scripts/drive.py upload ~/Documents/report.pdf --name "Q4 Report.pdf"
# Create a new folder
python scripts/drive.py create-folder "Project Documents"
# Create a folder inside another folder
python scripts/drive.py create-folder "Attachments" --parent 1ABC123xyz
# Move a file to a different folder
python scripts/drive.py move FILE_ID DESTINATION_FOLDER_ID
# Copy a file
python scripts/drive.py copy FILE_ID
python scripts/drive.py copy FILE_ID --name "Report Copy" --folder 1ABC123xyz
# Rename a file or folder
python scripts/drive.py rename FILE_ID "New Name.pdf"
# Move a file to trash
python scripts/drive.py trash FILE_IDSearch Query Formats
The search command supports multiple query formats:
| Format | Example | Description |
|---|---|---|
| Full-text | "quarterly report" | Searches file contents and names |
| Title | "title:budget" | Searches file names only |
| URL | https://drive.google.com/... | Extracts and uses file/folder ID |
| Folder ID | 1ABC123... | Lists folder contents (25+ char IDs) |
| Native query | mimeType='application/pdf' | Pass-through Drive query syntax |
File ID Format
Google Drive uses long IDs like 1ABC123xyz_-abc123. Get IDs from:
searchresultsfind-folderresultslistresults- Google Drive URLs
Download Limitations
- Regular files (PDFs, images, etc.) download directly
- Google Docs/Sheets/Slides cannot be downloaded via this tool
- For Google Workspace files, use export or dedicated tools
Token Management
Tokens stored securely using the system keyring:
- macOS: Keychain
- Windows: Windows Credential Locker
- Linux: Secret Service API (GNOME Keyring, KDE Wallet, etc.)
Service name: google-drive-skill-oauth
Automatically refreshes expired tokens using Google's cloud function.
Google Drive Skill
An AI agent skill for interacting with Google Drive - search files, find folders, list contents, and download files. Works with Claude Code, Gemini CLI, Cursor, OpenAI Codex, Goose, and other AI clients supporting the Agent Skills Standard.
Features
- Search - Full-text search across files
- Find Folders - Locate folders by name
- List Contents - Browse folder contents
- Download - Download files locally
Read-only access. Lightweight alternative to the full Google Workspace MCP server.
⚠️ Requires Google Workspace account. Personal Gmail accounts are not supported.
Quick Start
1. Install dependencies
pip install keyring2. Authenticate
python scripts/auth.py loginThis opens a browser for Google OAuth. Tokens are stored securely in your system keyring.
3. Test connection
python scripts/auth.py statusUsage Examples
# Search for files (full-text search)
python scripts/drive.py search "quarterly report"
# Search by title only
python scripts/drive.py search "title:budget"
# Search using Google Drive URL
python scripts/drive.py search "https://drive.google.com/drive/folders/1ABC123..."
# Search files shared with you
python scripts/drive.py search --shared-with-me
# Search with pagination
python scripts/drive.py search "report" --limit 5 --page-token "..."
# Find a folder by exact name
python scripts/drive.py find-folder "Project Documents"
# List files in root Drive
python scripts/drive.py list
# List files in a specific folder
python scripts/drive.py list 1ABC123xyz --limit 20
# Download a file
python scripts/drive.py download 1ABC123xyz ./downloads/report.pdfCommand Reference
| Command | Description | Arguments |
|---|---|---|
search [query] | Search files | query, --shared-with-me, --limit |
find-folder <name> | Find folder by name | folder name |
list [folder_id] | List folder contents | optional folder ID, --limit |
download <id> <path> | Download file | file ID, local path |
Search Query Formats
| Format | Example | Description |
|---|---|---|
| Full-text | "quarterly report" | Searches file contents and names |
| Title | "title:budget" | Searches file names only |
| URL | https://drive.google.com/... | Extracts and uses file/folder ID |
| Folder ID | 1ABC123... | Lists folder contents |
| Native query | mimeType='application/pdf' | Drive query syntax |
Download Limitations
- Regular files (PDFs, images, etc.) download directly
- Google Docs/Sheets/Slides cannot be downloaded via this tool
- For Google Workspace files, use export or dedicated tools
Token Management
Tokens stored securely using the system keyring:
- macOS: Keychain
- Windows: Windows Credential Locker
- Linux: Secret Service API (GNOME Keyring, KDE Wallet)
Service name: google-drive-skill-oauth
Troubleshooting
"Failed to get access token"
Run python scripts/auth.py login to authenticate.
"File not found"
Check the file ID and your access permissions.
"Cannot download Google Workspace file"
Use the dedicated skill (google-docs, google-sheets, google-slides) for Google Workspace files.
License
Apache 2.0
keyring>=24.0.0
#!/usr/bin/env python3
"""
OAuth token management for Google Drive API.
Standalone authentication - does not require the MCP server.
Cross-platform support using keyring library.
"""
import http.server
import json
import secrets
import socket
import sys
import time
import webbrowser
from base64 import b64encode
from dataclasses import dataclass
from typing import Optional
from urllib.parse import urlencode, parse_qs, urlparse
import urllib.request
import urllib.error
import keyring
# OAuth Configuration - uses same cloud function as MCP server
CLIENT_ID = "338689075775-o75k922vn5fdl18qergr96rp8g63e4d7.apps.googleusercontent.com"
CLOUD_FUNCTION_URL = "https://google-workspace-extension.geminicli.com"
REFRESH_ENDPOINT = f"{CLOUD_FUNCTION_URL}/refreshToken"
# Keyring configuration
KEYCHAIN_SERVICE = "google-drive-skill-oauth"
KEYCHAIN_ACCOUNT = "main-account"
# Google Drive requires these scopes (read/write)
SCOPES = [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/userinfo.profile",
]
TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000 # 5 minutes
@dataclass
class TokenInfo:
access_token: str
refresh_token: Optional[str]
expires_at: Optional[int]
scope: Optional[str]
# ==============================================================================
# Cross-platform token storage using keyring
# ==============================================================================
def get_tokens_from_keychain() -> Optional[TokenInfo]:
"""Read OAuth tokens from keyring."""
try:
data_str = keyring.get_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)
if not data_str:
return None
data = json.loads(data_str)
token = data.get("token", {})
return TokenInfo(
access_token=token.get("accessToken", ""),
refresh_token=token.get("refreshToken"),
expires_at=token.get("expiresAt"),
scope=token.get("scope")
)
except (json.JSONDecodeError, keyring.errors.KeyringError):
return None
def save_tokens_to_keychain(token_info: TokenInfo) -> bool:
"""Save OAuth tokens to keyring."""
data = {
"serverName": KEYCHAIN_ACCOUNT,
"token": {
"accessToken": token_info.access_token,
"refreshToken": token_info.refresh_token,
"tokenType": "Bearer",
"scope": token_info.scope,
"expiresAt": token_info.expires_at
},
"updatedAt": int(time.time() * 1000)
}
try:
keyring.set_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, json.dumps(data))
return True
except keyring.errors.KeyringError:
return False
def clear_tokens_from_keychain() -> bool:
"""Clear OAuth tokens from keyring."""
try:
keyring.delete_password(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT)
return True
except (keyring.errors.PasswordDeleteError, keyring.errors.KeyringError):
return False
# ==============================================================================
# Utility functions
# ==============================================================================
def get_available_port() -> int:
"""Find an available port for the OAuth callback server."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('localhost', 0))
return s.getsockname()[1]
def is_token_expired(token_info: TokenInfo) -> bool:
"""Check if the access token is expired or expiring soon."""
if not token_info.expires_at:
return False
current_time_ms = int(time.time() * 1000)
return token_info.expires_at < (current_time_ms + TOKEN_EXPIRY_BUFFER_MS)
def refresh_access_token(refresh_token: str) -> Optional[dict]:
"""Refresh access token using the cloud function."""
try:
data = json.dumps({"refresh_token": refresh_token}).encode('utf-8')
req = urllib.request.Request(
REFRESH_ENDPOINT,
data=data,
headers={"Content-Type": "application/json"},
method="POST"
)
with urllib.request.urlopen(req, timeout=30) as response:
return json.loads(response.read().decode('utf-8'))
except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError) as e:
print(f"Error refreshing token: {e}", file=sys.stderr)
return None
class OAuthCallbackHandler(http.server.BaseHTTPRequestHandler):
"""HTTP request handler for OAuth callback."""
token_info: Optional[TokenInfo] = None
csrf_token: str = ""
error: Optional[str] = None
def log_message(self, format: str, *args) -> None: # noqa: A002
"""Suppress default logging."""
_ = format, args # Unused
def do_GET(self):
"""Handle the OAuth callback."""
parsed = urlparse(self.path)
if not parsed.path.startswith("/oauth2callback"):
self.send_error(404)
return
params = parse_qs(parsed.query)
# Validate CSRF token
state = params.get("state", [""])[0]
if state != OAuthCallbackHandler.csrf_token:
OAuthCallbackHandler.error = "State mismatch - possible CSRF attack"
self.send_response(400)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"<html><body><h1>Error: State mismatch</h1></body></html>")
return
# Check for errors
if "error" in params:
OAuthCallbackHandler.error = params.get("error_description", params["error"])[0]
self.send_response(400)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(f"<html><body><h1>Error: {OAuthCallbackHandler.error}</h1></body></html>".encode())
return
# Extract tokens
access_token = params.get("access_token", [""])[0]
refresh_token = params.get("refresh_token", [""])[0]
expiry_date = params.get("expiry_date", [""])[0]
scope = params.get("scope", [""])[0]
if access_token:
OAuthCallbackHandler.token_info = TokenInfo(
access_token=access_token,
refresh_token=refresh_token or None,
expires_at=int(expiry_date) if expiry_date else None,
scope=scope or None
)
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"""<html><body>
<h1>Authentication successful!</h1>
<p>You can close this window and return to the terminal.</p>
<script>window.close();</script>
</body></html>""")
else:
OAuthCallbackHandler.error = "No access token received"
self.send_response(400)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"<html><body><h1>Error: No access token received</h1></body></html>")
def perform_oauth_flow() -> Optional[TokenInfo]:
"""Perform the full OAuth flow with browser-based authentication."""
port = get_available_port()
redirect_uri = f"http://localhost:{port}/oauth2callback"
csrf_token = secrets.token_hex(32)
# Build state payload for cloud function
state_payload = {
"uri": redirect_uri,
"manual": False,
"csrf": csrf_token
}
state = b64encode(json.dumps(state_payload).encode()).decode()
# Build auth URL
auth_params = {
"client_id": CLIENT_ID,
"redirect_uri": CLOUD_FUNCTION_URL, # Cloud function handles the secret
"response_type": "code",
"scope": " ".join(SCOPES),
"access_type": "offline",
"prompt": "consent",
"state": state
}
auth_url = f"https://accounts.google.com/o/oauth2/v2/auth?{urlencode(auth_params)}"
# Set up callback handler
OAuthCallbackHandler.csrf_token = csrf_token
OAuthCallbackHandler.token_info = None
OAuthCallbackHandler.error = None
# Start local server
server = http.server.HTTPServer(('localhost', port), OAuthCallbackHandler)
server.timeout = 300 # 5 minute timeout
print(f"Opening browser for authentication...", file=sys.stderr)
print(f"If browser doesn't open, visit: {auth_url}", file=sys.stderr)
webbrowser.open(auth_url)
# Wait for callback
print("Waiting for authentication...", file=sys.stderr)
while OAuthCallbackHandler.token_info is None and OAuthCallbackHandler.error is None:
server.handle_request()
server.server_close()
if OAuthCallbackHandler.error:
print(f"Authentication failed: {OAuthCallbackHandler.error}", file=sys.stderr)
return None
return OAuthCallbackHandler.token_info
def get_valid_access_token(interactive: bool = True) -> Optional[str]:
"""
Get a valid access token.
Args:
interactive: If True, will prompt for OAuth flow if no tokens exist.
If False, will return None if no valid tokens.
"""
token_info = get_tokens_from_keychain()
# No tokens - need to authenticate
if not token_info or not token_info.access_token:
if not interactive:
return None
print("No OAuth tokens found. Starting authentication...", file=sys.stderr)
token_info = perform_oauth_flow()
if not token_info:
return None
save_tokens_to_keychain(token_info)
print("Authentication successful!", file=sys.stderr)
return token_info.access_token
# Check if token needs refresh
if is_token_expired(token_info) and token_info.refresh_token:
print("Access token expired, refreshing...", file=sys.stderr)
new_tokens = refresh_access_token(token_info.refresh_token)
if new_tokens:
token_info.access_token = new_tokens.get("access_token", token_info.access_token)
token_info.expires_at = new_tokens.get("expiry_date", token_info.expires_at)
save_tokens_to_keychain(token_info)
print("Token refreshed successfully.", file=sys.stderr)
else:
# Refresh failed - try re-authenticating if interactive
if interactive:
print("Token refresh failed. Re-authenticating...", file=sys.stderr)
token_info = perform_oauth_flow()
if token_info:
save_tokens_to_keychain(token_info)
else:
return None
else:
print("Warning: Failed to refresh token.", file=sys.stderr)
return token_info.access_token if token_info else None
def logout() -> bool:
"""Clear stored OAuth tokens."""
return clear_tokens_from_keychain()
def main():
"""CLI for auth operations."""
import argparse
parser = argparse.ArgumentParser(description="Google Drive OAuth management")
subparsers = parser.add_subparsers(dest="command")
subparsers.add_parser("login", help="Authenticate with Google")
subparsers.add_parser("logout", help="Clear stored tokens")
subparsers.add_parser("token", help="Print current access token")
subparsers.add_parser("status", help="Check authentication status")
args = parser.parse_args()
if args.command == "login":
token_info = perform_oauth_flow()
if token_info:
save_tokens_to_keychain(token_info)
print("Login successful!")
else:
print("Login failed.")
sys.exit(1)
elif args.command == "logout":
if logout():
print("Logged out successfully.")
else:
print("No tokens to clear.")
elif args.command == "token":
token = get_valid_access_token(interactive=False)
if token:
print(token)
else:
print("Not authenticated. Run: python auth.py login", file=sys.stderr)
sys.exit(1)
elif args.command == "status":
token_info = get_tokens_from_keychain()
if token_info and token_info.access_token:
expired = is_token_expired(token_info)
print(f"Status: Authenticated")
print(f"Token expired: {expired}")
if token_info.expires_at:
expires_in = (token_info.expires_at - int(time.time() * 1000)) / 1000 / 60
print(f"Expires in: {expires_in:.1f} minutes")
else:
print("Status: Not authenticated")
print("Run: python auth.py login")
sys.exit(1)
else:
parser.print_help()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Google Drive API operations.
Lightweight alternative to the full Google Workspace MCP server.
"""
import argparse
import json
import os
import re
import sys
import urllib.request
import urllib.error
import urllib.parse
from typing import Optional
from auth import get_valid_access_token
DRIVE_API_BASE = "https://www.googleapis.com/drive/v3"
# Google Drive URL patterns for extracting file/folder IDs
URL_PATTERNS = [
(r'/folders/([a-zA-Z0-9-_]+)', 'folder'),
(r'/file/d/([a-zA-Z0-9-_]+)', 'file'),
(r'/document/d/([a-zA-Z0-9-_]+)', 'file'),
(r'/spreadsheets/d/([a-zA-Z0-9-_]+)', 'file'),
(r'/presentation/d/([a-zA-Z0-9-_]+)', 'file'),
(r'/forms/d/([a-zA-Z0-9-_]+)', 'file'),
(r'[?&]id=([a-zA-Z0-9-_]+)', 'unknown'),
]
MIN_DRIVE_ID_LENGTH = 25
def escape_query_string(s: str) -> str:
"""Escape special characters in Drive query strings."""
return s.replace("\\", "\\\\").replace("'", "\\'")
def api_request(method: str, endpoint: str, params: Optional[dict] = None,
stream: bool = False) -> dict | bytes:
"""Make an authenticated request to the Google Drive API."""
token = get_valid_access_token()
if not token:
return {"error": "Failed to get access token"}
url = f"{DRIVE_API_BASE}/{endpoint}"
if params:
url += "?" + urllib.parse.urlencode(params)
headers = {
"Authorization": f"Bearer {token}",
}
try:
req = urllib.request.Request(url, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=60) as response:
if stream:
return response.read()
return json.loads(response.read().decode('utf-8'))
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8') if e.fp else str(e)
return {"error": f"HTTP {e.code}: {error_body}"}
except urllib.error.URLError as e:
return {"error": f"Request failed: {e.reason}"}
except json.JSONDecodeError:
return {"error": "Invalid JSON response"}
def extract_id_from_url(url: str) -> tuple[Optional[str], str]:
"""Extract file/folder ID from a Google Drive URL."""
for pattern, id_type in URL_PATTERNS:
match = re.search(pattern, url)
if match:
return match.group(1), id_type
return None, 'unknown'
def is_drive_url(query: str) -> bool:
"""Check if query looks like a Google Drive URL."""
return 'drive.google.com' in query or 'docs.google.com' in query
def is_drive_id(query: str) -> bool:
"""Check if query looks like a Drive file/folder ID."""
if len(query) < MIN_DRIVE_ID_LENGTH:
return False
return bool(re.match(r'^[a-zA-Z0-9-_]+$', query)) and ' ' not in query
def search(query: Optional[str] = None, page_size: int = 10,
page_token: Optional[str] = None, shared_with_me: bool = False) -> dict:
"""
Search for files and folders in Google Drive.
Supports:
- Full-text search: "quarterly report"
- Title search: "title:budget"
- Google Drive URLs: extracts file/folder ID automatically
- Folder ID: lists contents if query looks like an ID
- Native query syntax: "mimeType='application/pdf'"
"""
q = None
# Handle Google Drive URLs
if query and is_drive_url(query):
file_id, id_type = extract_id_from_url(query)
if not file_id:
return {"error": "Could not extract file/folder ID from URL"}
# Determine if it's a folder
if id_type == 'folder':
q = f"'{file_id}' in parents"
elif id_type == 'unknown':
# Check the file type via API
result = api_request("GET", f"files/{file_id}", {"fields": "mimeType"})
if isinstance(result, dict) and "error" not in result:
if result.get("mimeType") == "application/vnd.google-apps.folder":
q = f"'{file_id}' in parents"
else:
# Return just this file
file_result = api_request("GET", f"files/{file_id}", {
"fields": "id, name, modifiedTime, viewedByMeTime, mimeType, parents"
})
if isinstance(file_result, dict) and "error" not in file_result:
return {"files": [file_result], "nextPageToken": None}
return file_result
else:
# It's a file, get its details
file_result = api_request("GET", f"files/{file_id}", {
"fields": "id, name, modifiedTime, viewedByMeTime, mimeType, parents"
})
if isinstance(file_result, dict) and "error" not in file_result:
return {"files": [file_result], "nextPageToken": None}
return file_result
# Handle other query types
elif query:
query = query.strip()
# Title search prefix
if query.lower().startswith("title:"):
search_term = query[6:].strip()
# Remove quotes if present
if (search_term.startswith("'") and search_term.endswith("'")) or \
(search_term.startswith('"') and search_term.endswith('"')):
search_term = search_term[1:-1]
q = f"name contains '{escape_query_string(search_term)}'"
# Check if it's a Drive ID (list folder contents)
elif is_drive_id(query):
q = f"'{query}' in parents"
# Check if it's already a Drive query syntax
elif re.search(r'( and | or | not | contains | in |=)', query):
q = query
# Default to full-text search
else:
q = f"fullText contains '{escape_query_string(query)}'"
# Add sharedWithMe filter
if shared_with_me:
if q:
q += " and sharedWithMe"
else:
q = "sharedWithMe"
params = {
"pageSize": page_size,
"fields": "nextPageToken, files(id, name, modifiedTime, viewedByMeTime, mimeType, parents)"
}
if q:
params["q"] = q
if page_token:
params["pageToken"] = page_token
result = api_request("GET", "files", params)
if isinstance(result, dict) and "error" not in result:
return {
"files": result.get("files", []),
"nextPageToken": result.get("nextPageToken")
}
return result
def find_folder(folder_name: str) -> dict:
"""Find a folder by its exact name."""
escaped_name = escape_query_string(folder_name)
q = f"mimeType='application/vnd.google-apps.folder' and name = '{escaped_name}'"
params = {
"q": q,
"fields": "files(id, name)",
"spaces": "drive"
}
result = api_request("GET", "files", params)
if isinstance(result, dict) and "error" not in result:
return {"folders": result.get("files", [])}
return result
def list_files(folder_id: Optional[str] = None, page_size: int = 10,
page_token: Optional[str] = None) -> dict:
"""List files in a folder or root."""
params = {
"pageSize": page_size,
"fields": "nextPageToken, files(id, name, modifiedTime, mimeType, parents)"
}
if folder_id:
params["q"] = f"'{folder_id}' in parents"
if page_token:
params["pageToken"] = page_token
result = api_request("GET", "files", params)
if isinstance(result, dict) and "error" not in result:
return {
"files": result.get("files", []),
"nextPageToken": result.get("nextPageToken")
}
return result
def download(file_id: str, local_path: str) -> dict:
"""
Download a file from Google Drive.
Note: Google Workspace files (Docs, Sheets, Slides) cannot be downloaded
directly. Use the appropriate export format or getText tools instead.
"""
# First get file metadata to check type
metadata = api_request("GET", f"files/{file_id}", {"fields": "id, name, mimeType"})
if isinstance(metadata, dict) and "error" in metadata:
return metadata
mime_type = metadata.get("mimeType", "")
file_name = metadata.get("name", "unknown")
# Handle Google Workspace files
google_workspace_types = {
"application/vnd.google-apps.document": ("Google Doc", "docs.getText"),
"application/vnd.google-apps.spreadsheet": ("Google Sheet", "sheets.getText"),
"application/vnd.google-apps.presentation": ("Google Slides", "slides.getText"),
}
if mime_type in google_workspace_types:
file_type, tool = google_workspace_types[mime_type]
return {
"error": f"This is a {file_type}. Direct download not supported.",
"suggestion": f"Use the '{tool}' tool with ID: {file_id}",
"fileId": file_id,
"fileName": file_name
}
if mime_type.startswith("application/vnd.google-apps."):
return {
"error": f"Google Workspace file type ({mime_type}) cannot be downloaded directly.",
"suggestion": "Export the file manually or use specific tools.",
"fileId": file_id,
"fileName": file_name
}
# Download the file
file_content = api_request("GET", f"files/{file_id}", {"alt": "media"}, stream=True)
if isinstance(file_content, dict) and "error" in file_content:
return file_content
# Resolve path
abs_path = os.path.abspath(os.path.expanduser(local_path))
# Create directory if needed
dir_path = os.path.dirname(abs_path)
if dir_path:
os.makedirs(dir_path, exist_ok=True)
# Write file
try:
with open(abs_path, 'wb') as f:
f.write(file_content)
return {
"success": True,
"message": f"Downloaded '{file_name}' to {abs_path}",
"localPath": abs_path,
"fileName": file_name,
"fileId": file_id
}
except IOError as e:
return {"error": f"Failed to write file: {e}"}
def api_write_request(method: str, endpoint: str, data: Optional[dict] = None,
params: Optional[dict] = None) -> dict:
"""Make an authenticated write request (POST/PATCH) to the Google Drive API."""
token = get_valid_access_token()
if not token:
return {"error": "Failed to get access token"}
url = f"{DRIVE_API_BASE}/{endpoint}"
if params:
url += "?" + urllib.parse.urlencode(params)
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
body = json.dumps(data).encode('utf-8') if data else None
try:
req = urllib.request.Request(url, data=body, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=60) as response:
response_data = response.read().decode('utf-8')
return json.loads(response_data) if response_data else {"success": True}
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8') if e.fp else str(e)
return {"error": f"HTTP {e.code}: {error_body}"}
except urllib.error.URLError as e:
return {"error": f"Request failed: {e.reason}"}
except json.JSONDecodeError:
return {"success": True}
def upload(local_path: str, folder_id: Optional[str] = None, name: Optional[str] = None) -> dict:
"""
Upload a file to Google Drive.
Args:
local_path: Path to the local file to upload
folder_id: Target folder ID (uploads to root if not specified)
name: Name for the file in Drive (uses local filename if not specified)
"""
import mimetypes
abs_path = os.path.abspath(os.path.expanduser(local_path))
if not os.path.exists(abs_path):
return {"error": f"File not found: {abs_path}"}
file_name = name or os.path.basename(abs_path)
mime_type = mimetypes.guess_type(abs_path)[0] or "application/octet-stream"
# Read file content
try:
with open(abs_path, 'rb') as f:
file_content = f.read()
except IOError as e:
return {"error": f"Failed to read file: {e}"}
# Build metadata
metadata = {"name": file_name}
if folder_id:
metadata["parents"] = [folder_id]
# Use multipart upload
token = get_valid_access_token()
if not token:
return {"error": "Failed to get access token"}
boundary = "skill_upload_boundary"
body = (
f"--{boundary}\r\n"
f"Content-Type: application/json; charset=UTF-8\r\n\r\n"
f"{json.dumps(metadata)}\r\n"
f"--{boundary}\r\n"
f"Content-Type: {mime_type}\r\n\r\n"
).encode('utf-8') + file_content + f"\r\n--{boundary}--".encode('utf-8')
url = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": f"multipart/related; boundary={boundary}",
}
try:
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=120) as response:
result = json.loads(response.read().decode('utf-8'))
return {
"success": True,
"fileId": result.get("id"),
"name": result.get("name"),
"mimeType": result.get("mimeType")
}
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8') if e.fp else str(e)
return {"error": f"HTTP {e.code}: {error_body}"}
except urllib.error.URLError as e:
return {"error": f"Upload failed: {e.reason}"}
def create_folder(folder_name: str, parent_id: Optional[str] = None) -> dict:
"""
Create a new folder in Google Drive.
Args:
folder_name: Name of the new folder
parent_id: Parent folder ID (creates in root if not specified)
"""
metadata = {
"name": folder_name,
"mimeType": "application/vnd.google-apps.folder"
}
if parent_id:
metadata["parents"] = [parent_id]
return api_write_request("POST", "files", data=metadata)
def move_file(file_id: str, new_parent_id: str) -> dict:
"""
Move a file to a different folder.
Args:
file_id: ID of the file to move
new_parent_id: ID of the destination folder
"""
# Get current parents
current = api_request("GET", f"files/{file_id}", {"fields": "parents"})
if isinstance(current, dict) and "error" in current:
return current
old_parents = ",".join(current.get("parents", []))
params = {
"addParents": new_parent_id,
"removeParents": old_parents,
"fields": "id, name, parents"
}
return api_write_request("PATCH", f"files/{file_id}", params=params)
def copy_file(file_id: str, new_name: Optional[str] = None, folder_id: Optional[str] = None) -> dict:
"""
Copy a file.
Args:
file_id: ID of the file to copy
new_name: Name for the copy (defaults to "Copy of <original>")
folder_id: Destination folder ID (same folder if not specified)
"""
data = {}
if new_name:
data["name"] = new_name
if folder_id:
data["parents"] = [folder_id]
return api_write_request("POST", f"files/{file_id}/copy", data=data)
def rename_file(file_id: str, new_name: str) -> dict:
"""
Rename a file or folder.
Args:
file_id: ID of the file/folder to rename
new_name: New name
"""
return api_write_request("PATCH", f"files/{file_id}", data={"name": new_name},
params={"fields": "id, name"})
def trash_file(file_id: str) -> dict:
"""
Move a file or folder to trash.
Args:
file_id: ID of the file/folder to trash
"""
return api_write_request("PATCH", f"files/{file_id}", data={"trashed": True},
params={"fields": "id, name, trashed"})
def main():
parser = argparse.ArgumentParser(description="Google Drive API operations")
subparsers = parser.add_subparsers(dest="command", required=True)
# search
search_parser = subparsers.add_parser("search", help="Search for files and folders")
search_parser.add_argument("query", nargs="?", help="Search query, URL, or folder ID")
search_parser.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
search_parser.add_argument("--page-token", help="Pagination token")
search_parser.add_argument("--shared-with-me", action="store_true", help="Only shared files")
# find-folder
find_folder_parser = subparsers.add_parser("find-folder", help="Find folder by name")
find_folder_parser.add_argument("name", help="Folder name to find")
# list
list_parser = subparsers.add_parser("list", help="List files in a folder")
list_parser.add_argument("folder_id", nargs="?", help="Folder ID (root if not specified)")
list_parser.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
list_parser.add_argument("--page-token", help="Pagination token")
# download
download_parser = subparsers.add_parser("download", help="Download a file")
download_parser.add_argument("file_id", help="File ID to download")
download_parser.add_argument("local_path", help="Local path to save file")
# upload
upload_parser = subparsers.add_parser("upload", help="Upload a file to Drive")
upload_parser.add_argument("local_path", help="Path to local file")
upload_parser.add_argument("--folder", help="Target folder ID")
upload_parser.add_argument("--name", help="Name for file in Drive (default: local filename)")
# create-folder
create_folder_parser = subparsers.add_parser("create-folder", help="Create a new folder")
create_folder_parser.add_argument("name", help="Folder name")
create_folder_parser.add_argument("--parent", help="Parent folder ID")
# move
move_parser = subparsers.add_parser("move", help="Move a file to another folder")
move_parser.add_argument("file_id", help="File ID to move")
move_parser.add_argument("folder_id", help="Destination folder ID")
# copy
copy_parser = subparsers.add_parser("copy", help="Copy a file")
copy_parser.add_argument("file_id", help="File ID to copy")
copy_parser.add_argument("--name", help="Name for the copy")
copy_parser.add_argument("--folder", help="Destination folder ID")
# rename
rename_parser = subparsers.add_parser("rename", help="Rename a file or folder")
rename_parser.add_argument("file_id", help="File/folder ID")
rename_parser.add_argument("new_name", help="New name")
# trash
trash_parser = subparsers.add_parser("trash", help="Move a file to trash")
trash_parser.add_argument("file_id", help="File/folder ID to trash")
args = parser.parse_args()
if args.command == "search":
result = search(args.query, args.limit, args.page_token, args.shared_with_me)
elif args.command == "find-folder":
result = find_folder(args.name)
elif args.command == "list":
result = list_files(args.folder_id, args.limit, args.page_token)
elif args.command == "download":
result = download(args.file_id, args.local_path)
elif args.command == "upload":
result = upload(args.local_path, args.folder, args.name)
elif args.command == "create-folder":
result = create_folder(args.name, args.parent)
elif args.command == "move":
result = move_file(args.file_id, args.folder_id)
elif args.command == "copy":
result = copy_file(args.file_id, args.name, args.folder)
elif args.command == "rename":
result = rename_file(args.file_id, args.new_name)
elif args.command == "trash":
result = trash_file(args.file_id)
else:
result = {"error": f"Unknown command: {args.command}"}
print(json.dumps(result, indent=2))
if isinstance(result, dict) and "error" in result:
sys.exit(1)
if __name__ == "__main__":
main()
keyring>=24.0.0
Related skills
Forks & variants (1)
Google Drive has 1 known copy in the catalog totaling 35 installs. They canonicalize to this original listing.
- sanjay3290 - 35 installs
How it compares
Pick google-drive for scriptable Workspace Drive CRUD without MCP overhead; use a full Workspace MCP when you need multi-product Google APIs beyond Drive files.
FAQ
Does google-drive require an MCP server?
google-drive does not require an MCP server. It uses standalone OAuth through scripts/auth.py and file operations through scripts/drive.py with full read/write Drive access after login.
Which Google accounts does google-drive support?
google-drive requires a Google Workspace account. Personal Gmail accounts are not supported. Google Docs, Sheets, and Slides cannot be downloaded directly—use export workflows for those file types.