datarobot-genai 0.2.29__py3-none-any.whl → 0.2.37__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datarobot_genai/core/cli/agent_kernel.py +4 -1
- datarobot_genai/drmcp/__init__.py +2 -2
- datarobot_genai/drmcp/core/exceptions.py +0 -4
- datarobot_genai/drmcp/core/logging.py +2 -2
- datarobot_genai/drmcp/test_utils/clients/__init__.py +0 -0
- datarobot_genai/drmcp/test_utils/clients/anthropic.py +68 -0
- datarobot_genai/drmcp/test_utils/{openai_llm_mcp_client.py → clients/base.py} +38 -40
- datarobot_genai/drmcp/test_utils/clients/dr_gateway.py +58 -0
- datarobot_genai/drmcp/test_utils/clients/openai.py +68 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +20 -0
- datarobot_genai/drmcp/test_utils/test_interactive.py +16 -16
- datarobot_genai/drmcp/test_utils/tool_base_ete.py +1 -1
- datarobot_genai/drmcp/test_utils/utils.py +1 -1
- datarobot_genai/drmcp/tools/clients/gdrive.py +187 -1
- datarobot_genai/drmcp/tools/clients/microsoft_graph.py +126 -0
- datarobot_genai/drmcp/tools/gdrive/tools.py +186 -10
- datarobot_genai/drmcp/tools/microsoft_graph/tools.py +79 -0
- datarobot_genai/drmcp/tools/predictive/data.py +5 -5
- datarobot_genai/drmcp/tools/predictive/deployment.py +52 -46
- datarobot_genai/drmcp/tools/predictive/model.py +87 -52
- datarobot_genai/drmcp/tools/predictive/project.py +2 -2
- datarobot_genai/drmcp/tools/predictive/training.py +52 -24
- {datarobot_genai-0.2.29.dist-info → datarobot_genai-0.2.37.dist-info}/METADATA +1 -1
- {datarobot_genai-0.2.29.dist-info → datarobot_genai-0.2.37.dist-info}/RECORD +28 -24
- {datarobot_genai-0.2.29.dist-info → datarobot_genai-0.2.37.dist-info}/WHEEL +0 -0
- {datarobot_genai-0.2.29.dist-info → datarobot_genai-0.2.37.dist-info}/entry_points.txt +0 -0
- {datarobot_genai-0.2.29.dist-info → datarobot_genai-0.2.37.dist-info}/licenses/AUTHORS +0 -0
- {datarobot_genai-0.2.29.dist-info → datarobot_genai-0.2.37.dist-info}/licenses/LICENSE +0 -0
|
@@ -20,6 +20,7 @@ import logging
|
|
|
20
20
|
import uuid
|
|
21
21
|
from typing import Annotated
|
|
22
22
|
from typing import Any
|
|
23
|
+
from typing import Literal
|
|
23
24
|
|
|
24
25
|
import httpx
|
|
25
26
|
from datarobot.auth.datarobot.exceptions import OAuthServiceClientErr
|
|
@@ -33,7 +34,17 @@ from datarobot_genai.drmcp.core.auth import get_access_token
|
|
|
33
34
|
|
|
34
35
|
logger = logging.getLogger(__name__)
|
|
35
36
|
|
|
36
|
-
SUPPORTED_FIELDS = {
|
|
37
|
+
SUPPORTED_FIELDS = {
|
|
38
|
+
"id",
|
|
39
|
+
"name",
|
|
40
|
+
"size",
|
|
41
|
+
"mimeType",
|
|
42
|
+
"webViewLink",
|
|
43
|
+
"createdTime",
|
|
44
|
+
"modifiedTime",
|
|
45
|
+
"starred",
|
|
46
|
+
"trashed",
|
|
47
|
+
}
|
|
37
48
|
SUPPORTED_FIELDS_STR = ",".join(SUPPORTED_FIELDS)
|
|
38
49
|
DEFAULT_FIELDS = f"nextPageToken,files({SUPPORTED_FIELDS_STR})"
|
|
39
50
|
GOOGLE_DRIVE_FOLDER_MIME = "application/vnd.google-apps.folder"
|
|
@@ -119,6 +130,8 @@ class GoogleDriveFile(BaseModel):
|
|
|
119
130
|
web_view_link: Annotated[str | None, Field(alias="webViewLink")] = None
|
|
120
131
|
created_time: Annotated[str | None, Field(alias="createdTime")] = None
|
|
121
132
|
modified_time: Annotated[str | None, Field(alias="modifiedTime")] = None
|
|
133
|
+
starred: bool | None = None
|
|
134
|
+
trashed: bool | None = None
|
|
122
135
|
|
|
123
136
|
model_config = ConfigDict(populate_by_name=True)
|
|
124
137
|
|
|
@@ -133,8 +146,31 @@ class GoogleDriveFile(BaseModel):
|
|
|
133
146
|
web_view_link=data.get("webViewLink"),
|
|
134
147
|
created_time=data.get("createdTime"),
|
|
135
148
|
modified_time=data.get("modifiedTime"),
|
|
149
|
+
starred=data.get("starred"),
|
|
150
|
+
trashed=data.get("trashed"),
|
|
136
151
|
)
|
|
137
152
|
|
|
153
|
+
def as_flat_dict(self) -> dict[str, Any]:
|
|
154
|
+
"""Return a flat dictionary representation of the file."""
|
|
155
|
+
result: dict[str, Any] = {
|
|
156
|
+
"id": self.id,
|
|
157
|
+
"name": self.name,
|
|
158
|
+
"mimeType": self.mime_type,
|
|
159
|
+
}
|
|
160
|
+
if self.size is not None:
|
|
161
|
+
result["size"] = self.size
|
|
162
|
+
if self.web_view_link is not None:
|
|
163
|
+
result["webViewLink"] = self.web_view_link
|
|
164
|
+
if self.created_time is not None:
|
|
165
|
+
result["createdTime"] = self.created_time
|
|
166
|
+
if self.modified_time is not None:
|
|
167
|
+
result["modifiedTime"] = self.modified_time
|
|
168
|
+
if self.starred is not None:
|
|
169
|
+
result["starred"] = self.starred
|
|
170
|
+
if self.trashed is not None:
|
|
171
|
+
result["trashed"] = self.trashed
|
|
172
|
+
return result
|
|
173
|
+
|
|
138
174
|
|
|
139
175
|
class PaginatedResult(BaseModel):
|
|
140
176
|
"""Result of a paginated API call."""
|
|
@@ -440,6 +476,66 @@ class GoogleDriveClient:
|
|
|
440
476
|
response.raise_for_status()
|
|
441
477
|
return GoogleDriveFile.from_api_response(response.json())
|
|
442
478
|
|
|
479
|
+
async def update_file_metadata(
|
|
480
|
+
self,
|
|
481
|
+
file_id: str,
|
|
482
|
+
new_name: str | None = None,
|
|
483
|
+
starred: bool | None = None,
|
|
484
|
+
trashed: bool | None = None,
|
|
485
|
+
) -> GoogleDriveFile:
|
|
486
|
+
"""Update file metadata in Google Drive.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
file_id: The ID of the file to update.
|
|
490
|
+
new_name: A new name to rename the file. Must not be empty or whitespace.
|
|
491
|
+
starred: Set to True to star the file or False to unstar it.
|
|
492
|
+
trashed: Set to True to trash the file or False to restore it.
|
|
493
|
+
|
|
494
|
+
Returns
|
|
495
|
+
-------
|
|
496
|
+
GoogleDriveFile with updated metadata.
|
|
497
|
+
|
|
498
|
+
Raises
|
|
499
|
+
------
|
|
500
|
+
GoogleDriveError: If no update fields are provided, file is not found,
|
|
501
|
+
access is denied, or the request is invalid.
|
|
502
|
+
"""
|
|
503
|
+
if new_name is None and starred is None and trashed is None:
|
|
504
|
+
raise GoogleDriveError(
|
|
505
|
+
"At least one of new_name, starred, or trashed must be provided."
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if new_name is not None and not new_name.strip():
|
|
509
|
+
raise GoogleDriveError("new_name cannot be empty or whitespace.")
|
|
510
|
+
|
|
511
|
+
body: dict[str, Any] = {}
|
|
512
|
+
if new_name is not None:
|
|
513
|
+
body["name"] = new_name
|
|
514
|
+
if starred is not None:
|
|
515
|
+
body["starred"] = starred
|
|
516
|
+
if trashed is not None:
|
|
517
|
+
body["trashed"] = trashed
|
|
518
|
+
|
|
519
|
+
response = await self._client.patch(
|
|
520
|
+
f"/{file_id}",
|
|
521
|
+
json=body,
|
|
522
|
+
params={"fields": SUPPORTED_FIELDS_STR, "supportsAllDrives": "true"},
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
if response.status_code == 404:
|
|
526
|
+
raise GoogleDriveError(f"File with ID '{file_id}' not found.")
|
|
527
|
+
if response.status_code == 403:
|
|
528
|
+
raise GoogleDriveError(
|
|
529
|
+
f"Permission denied: you don't have permission to update file '{file_id}'."
|
|
530
|
+
)
|
|
531
|
+
if response.status_code == 400:
|
|
532
|
+
raise GoogleDriveError("Bad request: invalid parameters for file update.")
|
|
533
|
+
if response.status_code == 429:
|
|
534
|
+
raise GoogleDriveError("Rate limit exceeded. Please try again later.")
|
|
535
|
+
|
|
536
|
+
response.raise_for_status()
|
|
537
|
+
return GoogleDriveFile.from_api_response(response.json())
|
|
538
|
+
|
|
443
539
|
async def _export_workspace_file(self, file_id: str, export_mime_type: str) -> str:
|
|
444
540
|
"""Export a Google Workspace file to the specified format.
|
|
445
541
|
|
|
@@ -726,6 +822,96 @@ class GoogleDriveClient:
|
|
|
726
822
|
headers={"Content-Type": f"multipart/related; boundary={boundary}"},
|
|
727
823
|
)
|
|
728
824
|
|
|
825
|
+
async def manage_access(
|
|
826
|
+
self,
|
|
827
|
+
*,
|
|
828
|
+
file_id: str,
|
|
829
|
+
action: Literal["add", "update", "remove"],
|
|
830
|
+
role: Literal["reader", "commenter", "writer", "fileOrganizer", "organizer", "owner"]
|
|
831
|
+
| None = None,
|
|
832
|
+
email_address: str | None = None,
|
|
833
|
+
permission_id: str | None = None,
|
|
834
|
+
transfer_ownership: bool = False,
|
|
835
|
+
) -> str:
|
|
836
|
+
"""Manage access permissions for a Google Drive file or folder.
|
|
837
|
+
|
|
838
|
+
Adds, updates, or removes sharing permissions on an existing Google Drive
|
|
839
|
+
file or folder using the Google Drive Permissions API.
|
|
840
|
+
|
|
841
|
+
This method supports granting access to users or groups, changing access
|
|
842
|
+
roles, and revoking permissions. Ownership transfer is supported for files
|
|
843
|
+
in "My Drive" when explicitly requested.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
file_id: The ID of the Google Drive file or folder whose permissions
|
|
847
|
+
are being managed.
|
|
848
|
+
action: The permission operation to perform.
|
|
849
|
+
role: The access role to assign or update. Valid values include
|
|
850
|
+
Required for "add" and "update" actions.
|
|
851
|
+
email_address: The email address of the user or group to grant access to.
|
|
852
|
+
Required for the "add" action.
|
|
853
|
+
permission_id: The ID of the permission to update or remove.
|
|
854
|
+
Required for "update" and "remove" actions.
|
|
855
|
+
transfer_ownership: Whether to transfer ownership of the file.
|
|
856
|
+
Only applicable when action="update" and role="owner".
|
|
857
|
+
|
|
858
|
+
Returns
|
|
859
|
+
-------
|
|
860
|
+
Permission id.
|
|
861
|
+
For "add" its newly added permission.
|
|
862
|
+
For "update"/"remove" its previous permission.
|
|
863
|
+
|
|
864
|
+
Raises
|
|
865
|
+
------
|
|
866
|
+
GoogleDriveError: If the permission operation fails (invalid arguments,
|
|
867
|
+
insufficient permissions, resource not found, ownership transfer
|
|
868
|
+
not allowed, rate limited, etc.).
|
|
869
|
+
"""
|
|
870
|
+
if not file_id.strip():
|
|
871
|
+
raise GoogleDriveError("Argument validation error: 'file_id' cannot be empty.")
|
|
872
|
+
|
|
873
|
+
if action == "add" and not email_address:
|
|
874
|
+
raise GoogleDriveError("'email_address' is required for action 'add'.")
|
|
875
|
+
|
|
876
|
+
if action in ("update", "remove") and not permission_id:
|
|
877
|
+
raise GoogleDriveError("'permission_id' is required for action 'update' or 'remove'.")
|
|
878
|
+
|
|
879
|
+
if action != "remove" and not role:
|
|
880
|
+
raise GoogleDriveError("'role' is required for action 'add' or 'update'.")
|
|
881
|
+
|
|
882
|
+
if action == "add":
|
|
883
|
+
response = await self._client.post(
|
|
884
|
+
url=f"/{file_id}/permissions",
|
|
885
|
+
json={
|
|
886
|
+
"type": "user",
|
|
887
|
+
"role": role,
|
|
888
|
+
"emailAddress": email_address,
|
|
889
|
+
},
|
|
890
|
+
params={"sendNotificationEmail": False, "supportsAllDrives": True},
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
elif action == "update":
|
|
894
|
+
response = await self._client.patch(
|
|
895
|
+
url=f"/{file_id}/permissions/{permission_id}",
|
|
896
|
+
json={"role": role},
|
|
897
|
+
params={"transferOwnership": transfer_ownership, "supportsAllDrives": True},
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
elif action == "remove":
|
|
901
|
+
response = await self._client.delete(url=f"/{file_id}/permissions/{permission_id}")
|
|
902
|
+
|
|
903
|
+
else:
|
|
904
|
+
raise GoogleDriveError(f"Invalid action '{action}'")
|
|
905
|
+
|
|
906
|
+
if response.status_code not in (200, 201, 204):
|
|
907
|
+
raise GoogleDriveError(f"Drive API error {response.status_code}: {response.text}")
|
|
908
|
+
|
|
909
|
+
if action == "add":
|
|
910
|
+
return response.json()["id"]
|
|
911
|
+
|
|
912
|
+
# Cannot be null here because of above validators
|
|
913
|
+
return permission_id # type: ignore
|
|
914
|
+
|
|
729
915
|
async def __aenter__(self) -> "GoogleDriveClient":
|
|
730
916
|
"""Async context manager entry."""
|
|
731
917
|
return self
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
18
|
from typing import Any
|
|
19
|
+
from urllib.parse import quote
|
|
19
20
|
|
|
20
21
|
import httpx
|
|
21
22
|
from datarobot.auth.datarobot.exceptions import OAuthServiceClientErr
|
|
@@ -415,6 +416,131 @@ class MicrosoftGraphClient:
|
|
|
415
416
|
|
|
416
417
|
return base_resource
|
|
417
418
|
|
|
419
|
+
async def get_personal_drive_id(self) -> str:
|
|
420
|
+
"""Get the current user's personal OneDrive drive ID.
|
|
421
|
+
|
|
422
|
+
Returns
|
|
423
|
+
-------
|
|
424
|
+
The drive ID string for the user's personal OneDrive.
|
|
425
|
+
|
|
426
|
+
Raises
|
|
427
|
+
------
|
|
428
|
+
MicrosoftGraphError: If the drive cannot be retrieved.
|
|
429
|
+
"""
|
|
430
|
+
try:
|
|
431
|
+
response = await self._client.get(f"{GRAPH_API_BASE}/me/drive")
|
|
432
|
+
response.raise_for_status()
|
|
433
|
+
data = response.json()
|
|
434
|
+
return data["id"]
|
|
435
|
+
except httpx.HTTPStatusError as e:
|
|
436
|
+
status_code = e.response.status_code
|
|
437
|
+
if status_code == 401:
|
|
438
|
+
raise MicrosoftGraphError(
|
|
439
|
+
"Authentication failed. Access token may be expired or invalid."
|
|
440
|
+
) from e
|
|
441
|
+
if status_code == 403:
|
|
442
|
+
raise MicrosoftGraphError(
|
|
443
|
+
"Permission denied: cannot access personal OneDrive. "
|
|
444
|
+
"Requires Files.Read or Files.ReadWrite permission."
|
|
445
|
+
) from e
|
|
446
|
+
raise MicrosoftGraphError(f"Failed to get personal OneDrive: HTTP {status_code}") from e
|
|
447
|
+
|
|
448
|
+
async def create_file(
|
|
449
|
+
self,
|
|
450
|
+
drive_id: str,
|
|
451
|
+
file_name: str,
|
|
452
|
+
content: str,
|
|
453
|
+
parent_folder_id: str = "root",
|
|
454
|
+
conflict_behavior: str = "rename",
|
|
455
|
+
) -> MicrosoftGraphItem:
|
|
456
|
+
"""Create a text file in a drive (SharePoint document library or OneDrive).
|
|
457
|
+
|
|
458
|
+
Uses Microsoft Graph's simple upload endpoint for files < 4MB.
|
|
459
|
+
Files are created as text/plain content.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
drive_id: The ID of the drive (document library) where the file will be created.
|
|
463
|
+
file_name: The name of the file to create (e.g., 'report.txt').
|
|
464
|
+
content: The text content to store in the file.
|
|
465
|
+
parent_folder_id: ID of the parent folder. Defaults to "root" (drive root folder).
|
|
466
|
+
conflict_behavior: How to handle name conflicts. Options:
|
|
467
|
+
- "rename" (default): Auto-renames to 'filename (1).txt', etc.
|
|
468
|
+
- "fail": Returns 409 Conflict error
|
|
469
|
+
- "replace": Overwrites existing file
|
|
470
|
+
|
|
471
|
+
Returns
|
|
472
|
+
-------
|
|
473
|
+
MicrosoftGraphItem representing the created file.
|
|
474
|
+
|
|
475
|
+
Raises
|
|
476
|
+
------
|
|
477
|
+
MicrosoftGraphError: If file creation fails.
|
|
478
|
+
"""
|
|
479
|
+
if not drive_id or not drive_id.strip():
|
|
480
|
+
raise MicrosoftGraphError("drive_id cannot be empty")
|
|
481
|
+
if not file_name or not file_name.strip():
|
|
482
|
+
raise MicrosoftGraphError("file_name cannot be empty")
|
|
483
|
+
|
|
484
|
+
# URL encode the filename for path-based addressing
|
|
485
|
+
encoded_name = quote(file_name, safe="")
|
|
486
|
+
|
|
487
|
+
# Simple upload endpoint for files < 4MB
|
|
488
|
+
# Reference: https://learn.microsoft.com/en-us/graph/api/driveitem-put-content
|
|
489
|
+
upload_url = (
|
|
490
|
+
f"{GRAPH_API_BASE}/drives/{drive_id}/items/{parent_folder_id}:/{encoded_name}:/content"
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
response = await self._client.put(
|
|
495
|
+
upload_url,
|
|
496
|
+
content=content.encode("utf-8"),
|
|
497
|
+
headers={"Content-Type": "text/plain"},
|
|
498
|
+
params={"@microsoft.graph.conflictBehavior": conflict_behavior},
|
|
499
|
+
)
|
|
500
|
+
response.raise_for_status()
|
|
501
|
+
except httpx.HTTPStatusError as e:
|
|
502
|
+
raise self._handle_create_file_error(e, drive_id, file_name, parent_folder_id) from e
|
|
503
|
+
|
|
504
|
+
return MicrosoftGraphItem.from_api_response(response.json())
|
|
505
|
+
|
|
506
|
+
def _handle_create_file_error(
|
|
507
|
+
self,
|
|
508
|
+
error: httpx.HTTPStatusError,
|
|
509
|
+
drive_id: str,
|
|
510
|
+
file_name: str,
|
|
511
|
+
parent_folder_id: str,
|
|
512
|
+
) -> MicrosoftGraphError:
|
|
513
|
+
"""Handle HTTP errors for file creation and return appropriate MicrosoftGraphError."""
|
|
514
|
+
status_code = error.response.status_code
|
|
515
|
+
error_msg = f"Failed to create file: HTTP {status_code}"
|
|
516
|
+
|
|
517
|
+
if status_code == 400:
|
|
518
|
+
try:
|
|
519
|
+
error_data = error.response.json()
|
|
520
|
+
api_message = error_data.get("error", {}).get("message", "Invalid request")
|
|
521
|
+
error_msg = f"Bad request creating file: {api_message}"
|
|
522
|
+
except Exception:
|
|
523
|
+
error_msg = "Bad request: invalid parameters for file creation."
|
|
524
|
+
elif status_code == 401:
|
|
525
|
+
error_msg = "Authentication failed. Access token may be expired or invalid."
|
|
526
|
+
elif status_code == 403:
|
|
527
|
+
error_msg = (
|
|
528
|
+
f"Permission denied: you don't have permission to create files in drive "
|
|
529
|
+
f"'{drive_id}'. Requires Files.ReadWrite.All permission."
|
|
530
|
+
)
|
|
531
|
+
elif status_code == 404:
|
|
532
|
+
error_msg = (
|
|
533
|
+
f"Parent folder '{parent_folder_id}' not found in drive '{drive_id}'."
|
|
534
|
+
if parent_folder_id != "root"
|
|
535
|
+
else f"Drive '{drive_id}' not found."
|
|
536
|
+
)
|
|
537
|
+
elif status_code == 409:
|
|
538
|
+
error_msg = f"File '{file_name}' already exists and conflict behavior is set to 'fail'."
|
|
539
|
+
elif status_code == 429:
|
|
540
|
+
error_msg = "Rate limit exceeded. Please try again later."
|
|
541
|
+
|
|
542
|
+
return MicrosoftGraphError(error_msg)
|
|
543
|
+
|
|
418
544
|
async def __aenter__(self) -> "MicrosoftGraphClient":
|
|
419
545
|
"""Async context manager entry."""
|
|
420
546
|
return self
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
18
|
from typing import Annotated
|
|
19
|
+
from typing import Literal
|
|
19
20
|
|
|
20
21
|
from fastmcp.exceptions import ToolError
|
|
21
22
|
from fastmcp.tools.tool import ToolResult
|
|
@@ -33,7 +34,9 @@ from datarobot_genai.drmcp.tools.clients.gdrive import get_gdrive_access_token
|
|
|
33
34
|
logger = logging.getLogger(__name__)
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
@dr_mcp_tool(
|
|
37
|
+
@dr_mcp_tool(
|
|
38
|
+
tags={"google", "gdrive", "list", "search", "files", "find", "contents"}, enabled=False
|
|
39
|
+
)
|
|
37
40
|
async def gdrive_find_contents(
|
|
38
41
|
*,
|
|
39
42
|
page_size: Annotated[
|
|
@@ -164,7 +167,6 @@ async def gdrive_read_content(
|
|
|
164
167
|
f"An unexpected error occurred while reading Google Drive file content: {str(e)}"
|
|
165
168
|
)
|
|
166
169
|
|
|
167
|
-
# Provide helpful context about the conversion
|
|
168
170
|
export_info = ""
|
|
169
171
|
if file_content.was_exported:
|
|
170
172
|
export_info = f" (exported from {file_content.original_mime_type})"
|
|
@@ -252,7 +254,6 @@ async def gdrive_create_file(
|
|
|
252
254
|
logger.error(f"Unexpected error creating Google Drive file: {e}")
|
|
253
255
|
raise ToolError(f"An unexpected error occurred while creating Google Drive file: {str(e)}")
|
|
254
256
|
|
|
255
|
-
# Build response message
|
|
256
257
|
file_type = "folder" if mime_type == GOOGLE_DRIVE_FOLDER_MIME else "file"
|
|
257
258
|
content_info = ""
|
|
258
259
|
if initial_content and mime_type != GOOGLE_DRIVE_FOLDER_MIME:
|
|
@@ -260,11 +261,186 @@ async def gdrive_create_file(
|
|
|
260
261
|
|
|
261
262
|
return ToolResult(
|
|
262
263
|
content=f"Successfully created {file_type} '{created_file.name}'{content_info}.",
|
|
263
|
-
structured_content=
|
|
264
|
-
"id": created_file.id,
|
|
265
|
-
"name": created_file.name,
|
|
266
|
-
"mimeType": created_file.mime_type,
|
|
267
|
-
"webViewLink": created_file.web_view_link,
|
|
268
|
-
"createdTime": created_file.created_time,
|
|
269
|
-
},
|
|
264
|
+
structured_content=created_file.as_flat_dict(),
|
|
270
265
|
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dr_mcp_tool(
|
|
269
|
+
tags={"google", "gdrive", "update", "metadata", "rename", "star", "trash"}, enabled=False
|
|
270
|
+
)
|
|
271
|
+
async def gdrive_update_metadata(
|
|
272
|
+
*,
|
|
273
|
+
file_id: Annotated[str, "The ID of the file or folder to update."],
|
|
274
|
+
new_name: Annotated[str | None, "A new name to rename the file."] = None,
|
|
275
|
+
starred: Annotated[bool | None, "Set to True to star the file or False to unstar it."] = None,
|
|
276
|
+
trash: Annotated[bool | None, "Set to True to trash the file or False to restore it."] = None,
|
|
277
|
+
) -> ToolResult:
|
|
278
|
+
"""
|
|
279
|
+
Update non-content metadata fields of a Google Drive file or folder.
|
|
280
|
+
|
|
281
|
+
This tool allows you to:
|
|
282
|
+
- Rename files and folders by setting new_name
|
|
283
|
+
- Star or unstar files (per-user preference) with starred
|
|
284
|
+
- Move files to trash or restore them with trash
|
|
285
|
+
|
|
286
|
+
Usage:
|
|
287
|
+
- Rename: gdrive_update_metadata(file_id="1ABC...", new_name="New Name.txt")
|
|
288
|
+
- Star: gdrive_update_metadata(file_id="1ABC...", starred=True)
|
|
289
|
+
- Unstar: gdrive_update_metadata(file_id="1ABC...", starred=False)
|
|
290
|
+
- Trash: gdrive_update_metadata(file_id="1ABC...", trash=True)
|
|
291
|
+
- Restore: gdrive_update_metadata(file_id="1ABC...", trash=False)
|
|
292
|
+
- Multiple: gdrive_update_metadata(file_id="1ABC...", new_name="New.txt", starred=True)
|
|
293
|
+
|
|
294
|
+
Note:
|
|
295
|
+
- At least one of new_name, starred, or trash must be provided.
|
|
296
|
+
- Starring is per-user: starring a shared file only affects your view.
|
|
297
|
+
- Trashing a folder trashes all contents recursively.
|
|
298
|
+
- Trashing requires permissions (owner for My Drive, organizer for Shared Drives).
|
|
299
|
+
"""
|
|
300
|
+
if not file_id or not file_id.strip():
|
|
301
|
+
raise ToolError("Argument validation error: 'file_id' cannot be empty.")
|
|
302
|
+
|
|
303
|
+
if new_name is None and starred is None and trash is None:
|
|
304
|
+
raise ToolError(
|
|
305
|
+
"Argument validation error: at least one of 'new_name', 'starred', or 'trash' "
|
|
306
|
+
"must be provided."
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if new_name is not None and not new_name.strip():
|
|
310
|
+
raise ToolError("Argument validation error: 'new_name' cannot be empty or whitespace.")
|
|
311
|
+
|
|
312
|
+
access_token = await get_gdrive_access_token()
|
|
313
|
+
if isinstance(access_token, ToolError):
|
|
314
|
+
raise access_token
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
async with GoogleDriveClient(access_token) as client:
|
|
318
|
+
updated_file = await client.update_file_metadata(
|
|
319
|
+
file_id=file_id,
|
|
320
|
+
new_name=new_name,
|
|
321
|
+
starred=starred,
|
|
322
|
+
trashed=trash,
|
|
323
|
+
)
|
|
324
|
+
except GoogleDriveError as e:
|
|
325
|
+
logger.error(f"Google Drive error updating file metadata: {e}")
|
|
326
|
+
raise ToolError(str(e))
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.error(f"Unexpected error updating Google Drive file metadata: {e}")
|
|
329
|
+
raise ToolError(
|
|
330
|
+
f"An unexpected error occurred while updating Google Drive file metadata: {str(e)}"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
changes: list[str] = []
|
|
334
|
+
if new_name is not None:
|
|
335
|
+
changes.append(f"renamed to '{new_name}'")
|
|
336
|
+
if starred is True:
|
|
337
|
+
changes.append("starred")
|
|
338
|
+
elif starred is False:
|
|
339
|
+
changes.append("unstarred")
|
|
340
|
+
if trash is True:
|
|
341
|
+
changes.append("moved to trash")
|
|
342
|
+
elif trash is False:
|
|
343
|
+
changes.append("restored from trash")
|
|
344
|
+
|
|
345
|
+
changes_description = ", ".join(changes)
|
|
346
|
+
|
|
347
|
+
return ToolResult(
|
|
348
|
+
content=f"Successfully updated file '{updated_file.name}': {changes_description}.",
|
|
349
|
+
structured_content=updated_file.as_flat_dict(),
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@dr_mcp_tool(tags={"google", "gdrive", "manage", "access", "acl"})
|
|
354
|
+
async def gdrive_manage_access(
|
|
355
|
+
*,
|
|
356
|
+
file_id: Annotated[str, "The ID of the file or folder."],
|
|
357
|
+
action: Annotated[Literal["add", "update", "remove"], "The operation to perform."],
|
|
358
|
+
role: Annotated[
|
|
359
|
+
Literal["reader", "commenter", "writer", "fileOrganizer", "organizer", "owner"] | None,
|
|
360
|
+
"The access level.",
|
|
361
|
+
] = None,
|
|
362
|
+
email_address: Annotated[
|
|
363
|
+
str | None, "The email of the user or group (required for 'add')."
|
|
364
|
+
] = None,
|
|
365
|
+
permission_id: Annotated[
|
|
366
|
+
str | None, "The specific permission ID (required for 'update' or 'remove')."
|
|
367
|
+
] = None,
|
|
368
|
+
transfer_ownership: Annotated[
|
|
369
|
+
bool, "Whether to transfer ownership (only for 'update' to 'owner' role)."
|
|
370
|
+
] = False,
|
|
371
|
+
) -> ToolResult:
|
|
372
|
+
"""
|
|
373
|
+
Consolidated tool for sharing files and managing permissions.
|
|
374
|
+
Pushes all logic to the Google Drive API permissions resource (create, update, delete).
|
|
375
|
+
|
|
376
|
+
Usage:
|
|
377
|
+
- Add role: gdrive_manage_access(
|
|
378
|
+
file_id="SomeFileId",
|
|
379
|
+
action="add",
|
|
380
|
+
role="reader",
|
|
381
|
+
email_address="dummy@user.com"
|
|
382
|
+
)
|
|
383
|
+
- Update role: gdrive_manage_access(
|
|
384
|
+
file_id="SomeFileId",
|
|
385
|
+
action="update",
|
|
386
|
+
role="reader",
|
|
387
|
+
permission_id="SomePermissionId"
|
|
388
|
+
)
|
|
389
|
+
- Remove permission: gdrive_manage_access(
|
|
390
|
+
file_id="SomeFileId",
|
|
391
|
+
action="remove",
|
|
392
|
+
permission_id="SomePermissionId"
|
|
393
|
+
)
|
|
394
|
+
"""
|
|
395
|
+
if not file_id or not file_id.strip():
|
|
396
|
+
raise ToolError("Argument validation error: 'file_id' cannot be empty.")
|
|
397
|
+
|
|
398
|
+
if action == "add" and not email_address:
|
|
399
|
+
raise ToolError("'email_address' is required for action 'add'.")
|
|
400
|
+
|
|
401
|
+
if action in ("update", "remove") and not permission_id:
|
|
402
|
+
raise ToolError("'permission_id' is required for action 'update' or 'remove'.")
|
|
403
|
+
|
|
404
|
+
if action != "remove" and not role:
|
|
405
|
+
raise ToolError("'role' is required for action 'add' or 'update'.")
|
|
406
|
+
|
|
407
|
+
access_token = await get_gdrive_access_token()
|
|
408
|
+
if isinstance(access_token, ToolError):
|
|
409
|
+
raise access_token
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
async with GoogleDriveClient(access_token) as client:
|
|
413
|
+
permission_id = await client.manage_access(
|
|
414
|
+
file_id=file_id,
|
|
415
|
+
action=action,
|
|
416
|
+
role=role,
|
|
417
|
+
email_address=email_address,
|
|
418
|
+
permission_id=permission_id,
|
|
419
|
+
transfer_ownership=transfer_ownership,
|
|
420
|
+
)
|
|
421
|
+
except GoogleDriveError as e:
|
|
422
|
+
logger.error(f"Google Drive permission operation failed: {e}")
|
|
423
|
+
raise ToolError(str(e))
|
|
424
|
+
except Exception as e:
|
|
425
|
+
logger.error(f"Unexpected error changing permissions for Google Drive file {file_id}: {e}")
|
|
426
|
+
raise ToolError(
|
|
427
|
+
f"Unexpected error changing permissions for Google Drive file {file_id}: {str(e)}"
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Build response
|
|
431
|
+
structured_content = {"affectedFileId": file_id}
|
|
432
|
+
if action == "add":
|
|
433
|
+
content = (
|
|
434
|
+
f"Successfully added role '{role}' for '{email_address}' for gdrive file '{file_id}'. "
|
|
435
|
+
f"New permission id '{permission_id}'."
|
|
436
|
+
)
|
|
437
|
+
structured_content["newPermissionId"] = permission_id
|
|
438
|
+
elif action == "update":
|
|
439
|
+
content = (
|
|
440
|
+
f"Successfully updated role '{role}' (permission '{permission_id}') "
|
|
441
|
+
f"for gdrive file '{file_id}'."
|
|
442
|
+
)
|
|
443
|
+
else: # action == "remove":
|
|
444
|
+
content = f"Successfully removed permission '{permission_id}' for gdrive file '{file_id}'."
|
|
445
|
+
|
|
446
|
+
return ToolResult(content=content, structured_content=structured_content)
|
|
@@ -196,3 +196,82 @@ async def microsoft_graph_search_content(
|
|
|
196
196
|
"count": n,
|
|
197
197
|
},
|
|
198
198
|
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dr_mcp_tool(
|
|
202
|
+
tags={
|
|
203
|
+
"microsoft",
|
|
204
|
+
"graph api",
|
|
205
|
+
"sharepoint",
|
|
206
|
+
"onedrive",
|
|
207
|
+
"document library",
|
|
208
|
+
"create",
|
|
209
|
+
"file",
|
|
210
|
+
"write",
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
async def microsoft_create_file(
|
|
214
|
+
*,
|
|
215
|
+
file_name: Annotated[str, "The name of the file to create (e.g., 'report.txt')."],
|
|
216
|
+
content_text: Annotated[str, "The raw text content to be stored in the file."],
|
|
217
|
+
document_library_id: Annotated[
|
|
218
|
+
str | None,
|
|
219
|
+
"The ID of the document library (Drive). If not provided, saves to personal OneDrive.",
|
|
220
|
+
] = None,
|
|
221
|
+
parent_folder_id: Annotated[
|
|
222
|
+
str | None,
|
|
223
|
+
"ID of the parent folder. Defaults to 'root' (root of the drive).",
|
|
224
|
+
] = "root",
|
|
225
|
+
) -> ToolResult | ToolError:
|
|
226
|
+
"""
|
|
227
|
+
Create a new text file in SharePoint or OneDrive.
|
|
228
|
+
|
|
229
|
+
**Personal OneDrive:** Just provide file_name and content_text.
|
|
230
|
+
The file saves to your personal OneDrive root folder.
|
|
231
|
+
|
|
232
|
+
**SharePoint:** Provide document_library_id to save to a specific
|
|
233
|
+
SharePoint site. Get the ID from microsoft_graph_search_content
|
|
234
|
+
results ('documentLibraryId' field).
|
|
235
|
+
|
|
236
|
+
**Conflict Resolution:** If a file with the same name exists,
|
|
237
|
+
it will be automatically renamed (e.g., 'report (1).txt').
|
|
238
|
+
"""
|
|
239
|
+
if not file_name or not file_name.strip():
|
|
240
|
+
raise ToolError("Error: file_name is required.")
|
|
241
|
+
if not content_text:
|
|
242
|
+
raise ToolError("Error: content_text is required.")
|
|
243
|
+
|
|
244
|
+
access_token = await get_microsoft_graph_access_token()
|
|
245
|
+
if isinstance(access_token, ToolError):
|
|
246
|
+
raise access_token
|
|
247
|
+
|
|
248
|
+
folder_id = parent_folder_id if parent_folder_id else "root"
|
|
249
|
+
|
|
250
|
+
async with MicrosoftGraphClient(access_token=access_token) as client:
|
|
251
|
+
# Auto-fetch personal OneDrive if no library specified
|
|
252
|
+
if document_library_id is None:
|
|
253
|
+
drive_id = await client.get_personal_drive_id()
|
|
254
|
+
is_personal_onedrive = True
|
|
255
|
+
else:
|
|
256
|
+
drive_id = document_library_id
|
|
257
|
+
is_personal_onedrive = False
|
|
258
|
+
|
|
259
|
+
created_file = await client.create_file(
|
|
260
|
+
drive_id=drive_id,
|
|
261
|
+
file_name=file_name.strip(),
|
|
262
|
+
content=content_text,
|
|
263
|
+
parent_folder_id=folder_id,
|
|
264
|
+
conflict_behavior="rename",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return ToolResult(
|
|
268
|
+
content=f"File '{created_file.name}' created successfully.",
|
|
269
|
+
structured_content={
|
|
270
|
+
"file_name": created_file.name,
|
|
271
|
+
"destination": "onedrive" if is_personal_onedrive else "sharepoint",
|
|
272
|
+
"driveId": drive_id,
|
|
273
|
+
"id": created_file.id,
|
|
274
|
+
"webUrl": created_file.web_url,
|
|
275
|
+
"parentFolderId": created_file.parent_folder_id,
|
|
276
|
+
},
|
|
277
|
+
)
|