Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ mcp_server_debug.log
# ---- Local development files -------------------------------------------
/.credentials
/.claude
.serena/
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,8 +698,9 @@ cp .env.oauth21 .env
|------|------|-------------|
| `search_drive_files` | **Core** | Search files with query syntax |
| `get_drive_file_content` | **Core** | Read file content (Office formats) |
| `list_drive_items` | Extended | List folder contents |
| `create_drive_file` | **Core** | Create files or fetch from URLs |
| `list_drive_items` | Extended | List folder contents |
| `update_drive_file` | Extended | Update file metadata, move between folders |

</td>
</tr>
Expand Down
1 change: 1 addition & 0 deletions core/tool_tiers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ drive:
- create_drive_file
extended:
- list_drive_items
- update_drive_file
complete:
- get_drive_file_permissions
- check_drive_file_public_access
Expand Down
150 changes: 149 additions & 1 deletion gdrive/drive_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,4 +493,152 @@ async def check_drive_file_public_access(
"Fix: Drive → Share → 'Anyone with the link' → 'Viewer'"
])

return "\n".join(output_parts)
return "\n".join(output_parts)


@server.tool()
@handle_http_errors("update_drive_file", is_read_only=False, service_type="drive")
@require_google_service("drive", "drive_file")
async def update_drive_file(
service,
user_google_email: str,
file_id: str,
# File metadata updates
name: Optional[str] = None,
description: Optional[str] = None,
mime_type: Optional[str] = None,

# Folder organization
add_parents: Optional[str] = None, # Comma-separated folder IDs to add
remove_parents: Optional[str] = None, # Comma-separated folder IDs to remove

# File status
starred: Optional[bool] = None,
trashed: Optional[bool] = None,

# Sharing and permissions
writers_can_share: Optional[bool] = None,
copy_requires_writer_permission: Optional[bool] = None,

# Custom properties
properties: Optional[dict] = None, # User-visible custom properties
) -> str:
"""
Updates metadata and properties of a Google Drive file.

Args:
user_google_email (str): The user's Google email address. Required.
file_id (str): The ID of the file to update. Required.
name (Optional[str]): New name for the file.
description (Optional[str]): New description for the file.
mime_type (Optional[str]): New MIME type (note: changing type may require content upload).
add_parents (Optional[str]): Comma-separated folder IDs to add as parents.
remove_parents (Optional[str]): Comma-separated folder IDs to remove from parents.
starred (Optional[bool]): Whether to star/unstar the file.
trashed (Optional[bool]): Whether to move file to/from trash.
writers_can_share (Optional[bool]): Whether editors can share the file.
copy_requires_writer_permission (Optional[bool]): Whether copying requires writer permission.
properties (Optional[dict]): Custom key-value properties for the file.

Returns:
str: Confirmation message with details of the updates applied.
"""
logger.info(f"[update_drive_file] Updating file {file_id} for {user_google_email}")

try:
# First, get current file info for reference
current_file = await asyncio.to_thread(
service.files().get(
fileId=file_id,
fields="id, name, description, mimeType, parents, starred, trashed, webViewLink",
supportsAllDrives=True
).execute
)

# Build the update body with only specified fields
update_body = {}
if name is not None:
update_body['name'] = name
if description is not None:
update_body['description'] = description
if mime_type is not None:
update_body['mimeType'] = mime_type
if starred is not None:
update_body['starred'] = starred
if trashed is not None:
update_body['trashed'] = trashed
if writers_can_share is not None:
update_body['writersCanShare'] = writers_can_share
if copy_requires_writer_permission is not None:
update_body['copyRequiresWriterPermission'] = copy_requires_writer_permission
if properties is not None:
update_body['properties'] = properties

# Build query parameters for parent changes
query_params = {
'fileId': file_id,
'supportsAllDrives': True,
'fields': 'id, name, description, mimeType, parents, starred, trashed, webViewLink, writersCanShare, copyRequiresWriterPermission, properties'
}

if add_parents:
query_params['addParents'] = add_parents
if remove_parents:
query_params['removeParents'] = remove_parents

# Only include body if there are updates
if update_body:
query_params['body'] = update_body

# Perform the update
updated_file = await asyncio.to_thread(
service.files().update(**query_params).execute
)

Comment on lines +659 to +666
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

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

The body parameter should be passed directly to the update() method, not included in query_params. According to Google Drive API documentation, the body should be a separate parameter.

Suggested change
if update_body:
query_params['body'] = update_body
# Perform the update
updated_file = await asyncio.to_thread(
service.files().update(**query_params).execute
)
# Perform the update
if update_body:
updated_file = await asyncio.to_thread(
service.files().update(body=update_body, **query_params).execute
)
else:
updated_file = await asyncio.to_thread(
service.files().update(**query_params).execute
)

Copilot uses AI. Check for mistakes.
# Build response message
output_parts = [f"✅ Successfully updated file: {updated_file.get('name', current_file['name'])}"]
output_parts.append(f" File ID: {file_id}")

# Report what changed
Comment on lines +659 to +671
Copy link

Copilot AI Oct 7, 2025

Choose a reason for hiding this comment

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

The API call structure is incorrect. The body parameter (if present in query_params) should be passed separately as the body argument to update(), not unpacked with other query parameters.

Suggested change
if update_body:
query_params['body'] = update_body
# Perform the update
updated_file = await asyncio.to_thread(
service.files().update(**query_params).execute
)
# Build response message
output_parts = [f"✅ Successfully updated file: {updated_file.get('name', current_file['name'])}"]
output_parts.append(f" File ID: {file_id}")
# Report what changed
# Perform the update
# Perform the update
if update_body:
updated_file = await asyncio.to_thread(
service.files().update(body=update_body, **query_params).execute
)
else:
updated_file = await asyncio.to_thread(
service.files().update(**query_params).execute
)
service.files().update(body=update_body, **query_params).execute
)
else:
updated_file = await asyncio.to_thread(
service.files().update(**query_params).execute
)

Copilot uses AI. Check for mistakes.
changes = []
if name is not None and name != current_file.get('name'):
changes.append(f" • Name: '{current_file.get('name')}' → '{name}'")
if description is not None:
old_desc = current_file.get('description', '(empty)')
new_desc = description if description else '(empty)'
if old_desc != new_desc:
changes.append(f" • Description: {old_desc} → {new_desc}")
if add_parents:
changes.append(f" • Added to folder(s): {add_parents}")
if remove_parents:
changes.append(f" • Removed from folder(s): {remove_parents}")
if starred is not None:
star_status = "starred" if starred else "unstarred"
changes.append(f" • File {star_status}")
if trashed is not None:
trash_status = "moved to trash" if trashed else "restored from trash"
changes.append(f" • File {trash_status}")
if writers_can_share is not None:
share_status = "can" if writers_can_share else "cannot"
changes.append(f" • Writers {share_status} share the file")
if copy_requires_writer_permission is not None:
copy_status = "requires" if copy_requires_writer_permission else "doesn't require"
changes.append(f" • Copying {copy_status} writer permission")
if properties:
changes.append(f" • Updated custom properties: {properties}")

if changes:
output_parts.append("")
output_parts.append("Changes applied:")
output_parts.extend(changes)
else:
output_parts.append(" (No changes were made)")

output_parts.append("")
output_parts.append(f"View file: {updated_file.get('webViewLink', '#')}")

return "\n".join(output_parts)

except Exception as e:
logger.error(f"Error updating file: {e}")
return f"❌ Error updating file: {e}"
46 changes: 46 additions & 0 deletions test_update_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python
"""
Direct test of update_drive_file function to verify OAuth scope fix
"""
import asyncio
from gdrive.drive_tools import update_drive_file
from auth.google_auth import get_google_service


async def test_update():
"""Test the update_drive_file function with all parameters"""

# Test parameters - replace with actual IDs
file_id = "YOUR_FILE_ID_HERE" # e.g., "1abc2def3ghi..."
folder_id = "YOUR_FOLDER_ID_HERE" # e.g., "1xyz2uvw3rst..."

# Get the service
service = await get_google_service(
"[email protected]",
"drive",
"drive_file" # Using correct scope
)

# Call update_drive_file directly
result = await update_drive_file(
service=service,
user_google_email="[email protected]",
file_id=file_id,
name="Updated Test Document - OAuth Fixed",
description="Testing update_drive_file with all parameters after fixing OAuth scope issue",
add_parents=folder_id,
starred=True,
writers_can_share=True,
copy_requires_writer_permission=False,
properties={
"test_key": "test_value",
"updated_by": "mcp_server",
"update_timestamp": "2025-01-29"
}
)

print(result)


if __name__ == "__main__":
asyncio.run(test_update())