datarobot-genai 0.2.37__py3-none-any.whl → 0.3.1__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.
Files changed (46) hide show
  1. datarobot_genai/core/agents/__init__.py +1 -1
  2. datarobot_genai/core/agents/base.py +5 -2
  3. datarobot_genai/core/chat/responses.py +6 -1
  4. datarobot_genai/core/utils/auth.py +188 -31
  5. datarobot_genai/crewai/__init__.py +1 -4
  6. datarobot_genai/crewai/agent.py +150 -17
  7. datarobot_genai/crewai/events.py +11 -4
  8. datarobot_genai/drmcp/__init__.py +4 -2
  9. datarobot_genai/drmcp/core/config.py +21 -1
  10. datarobot_genai/drmcp/core/mcp_instance.py +5 -49
  11. datarobot_genai/drmcp/core/routes.py +108 -13
  12. datarobot_genai/drmcp/core/tool_config.py +16 -0
  13. datarobot_genai/drmcp/core/utils.py +110 -0
  14. datarobot_genai/drmcp/test_utils/tool_base_ete.py +41 -26
  15. datarobot_genai/drmcp/tools/clients/gdrive.py +2 -0
  16. datarobot_genai/drmcp/tools/clients/microsoft_graph.py +141 -0
  17. datarobot_genai/drmcp/tools/clients/perplexity.py +173 -0
  18. datarobot_genai/drmcp/tools/clients/tavily.py +199 -0
  19. datarobot_genai/drmcp/tools/confluence/tools.py +43 -94
  20. datarobot_genai/drmcp/tools/gdrive/tools.py +44 -133
  21. datarobot_genai/drmcp/tools/jira/tools.py +19 -41
  22. datarobot_genai/drmcp/tools/microsoft_graph/tools.py +201 -32
  23. datarobot_genai/drmcp/tools/perplexity/__init__.py +0 -0
  24. datarobot_genai/drmcp/tools/perplexity/tools.py +117 -0
  25. datarobot_genai/drmcp/tools/predictive/data.py +1 -9
  26. datarobot_genai/drmcp/tools/predictive/deployment.py +0 -8
  27. datarobot_genai/drmcp/tools/predictive/deployment_info.py +91 -117
  28. datarobot_genai/drmcp/tools/predictive/model.py +0 -21
  29. datarobot_genai/drmcp/tools/predictive/predict_realtime.py +3 -0
  30. datarobot_genai/drmcp/tools/predictive/project.py +3 -19
  31. datarobot_genai/drmcp/tools/predictive/training.py +1 -19
  32. datarobot_genai/drmcp/tools/tavily/__init__.py +13 -0
  33. datarobot_genai/drmcp/tools/tavily/tools.py +141 -0
  34. datarobot_genai/langgraph/agent.py +10 -2
  35. datarobot_genai/llama_index/__init__.py +1 -1
  36. datarobot_genai/llama_index/agent.py +284 -5
  37. datarobot_genai/nat/agent.py +17 -6
  38. {datarobot_genai-0.2.37.dist-info → datarobot_genai-0.3.1.dist-info}/METADATA +3 -1
  39. {datarobot_genai-0.2.37.dist-info → datarobot_genai-0.3.1.dist-info}/RECORD +43 -40
  40. datarobot_genai/crewai/base.py +0 -159
  41. datarobot_genai/drmcp/core/tool_filter.py +0 -117
  42. datarobot_genai/llama_index/base.py +0 -299
  43. {datarobot_genai-0.2.37.dist-info → datarobot_genai-0.3.1.dist-info}/WHEEL +0 -0
  44. {datarobot_genai-0.2.37.dist-info → datarobot_genai-0.3.1.dist-info}/entry_points.txt +0 -0
  45. {datarobot_genai-0.2.37.dist-info → datarobot_genai-0.3.1.dist-info}/licenses/AUTHORS +0 -0
  46. {datarobot_genai-0.2.37.dist-info → datarobot_genai-0.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -22,21 +22,17 @@ from fastmcp.exceptions import ToolError
22
22
  from fastmcp.tools.tool import ToolResult
23
23
 
24
24
  from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
25
- from datarobot_genai.drmcp.tools.clients.gdrive import GOOGLE_DRIVE_FOLDER_MIME
26
25
  from datarobot_genai.drmcp.tools.clients.gdrive import LIMIT
27
26
  from datarobot_genai.drmcp.tools.clients.gdrive import MAX_PAGE_SIZE
28
27
  from datarobot_genai.drmcp.tools.clients.gdrive import SUPPORTED_FIELDS
29
28
  from datarobot_genai.drmcp.tools.clients.gdrive import SUPPORTED_FIELDS_STR
30
29
  from datarobot_genai.drmcp.tools.clients.gdrive import GoogleDriveClient
31
- from datarobot_genai.drmcp.tools.clients.gdrive import GoogleDriveError
32
30
  from datarobot_genai.drmcp.tools.clients.gdrive import get_gdrive_access_token
33
31
 
34
32
  logger = logging.getLogger(__name__)
35
33
 
36
34
 
37
- @dr_mcp_tool(
38
- tags={"google", "gdrive", "list", "search", "files", "find", "contents"}, enabled=False
39
- )
35
+ @dr_mcp_tool(tags={"google", "gdrive", "list", "search", "files", "find", "contents"})
40
36
  async def gdrive_find_contents(
41
37
  *,
42
38
  page_size: Annotated[
@@ -67,44 +63,34 @@ async def gdrive_find_contents(
67
63
  ) -> ToolResult:
68
64
  """
69
65
  Search or list files in the user's Google Drive with pagination and filtering support.
70
- Use this tool to discover file names and IDs for use with other tools.
66
+ Use this tool to discover GDrive file names and IDs for use with other tools.
71
67
 
72
68
  Limit must be bigger than or equal to page size and it must be multiplication of page size.
73
- Ex.
74
- page size = 10 limit = 50
75
- page size = 3 limit = 3
76
- page size = 12 limit = 36
69
+
70
+ Examples
71
+ --------
72
+ - page size = 10 limit = 50
73
+ - page size = 3 limit = 3
74
+ - page size = 12 limit = 36
77
75
  """
78
76
  access_token = await get_gdrive_access_token()
79
77
  if isinstance(access_token, ToolError):
80
78
  raise access_token
81
79
 
82
- try:
83
- async with GoogleDriveClient(access_token) as client:
84
- data = await client.list_files(
85
- page_size=page_size,
86
- page_token=page_token,
87
- query=query,
88
- limit=limit,
89
- folder_id=folder_id,
90
- recursive=recursive,
91
- )
92
- except GoogleDriveError as e:
93
- logger.error(f"Google Drive error listing files: {e}")
94
- raise ToolError(str(e))
95
- except Exception as e:
96
- logger.error(f"Unexpected error listing Google Drive files: {e}")
97
- raise ToolError(f"An unexpected error occurred while listing Google Drive files: {str(e)}")
80
+ async with GoogleDriveClient(access_token) as client:
81
+ data = await client.list_files(
82
+ page_size=page_size,
83
+ page_token=page_token,
84
+ query=query,
85
+ limit=limit,
86
+ folder_id=folder_id,
87
+ recursive=recursive,
88
+ )
98
89
 
99
90
  filtered_fields = set(fields).intersection(SUPPORTED_FIELDS) if fields else SUPPORTED_FIELDS
100
91
  number_of_files = len(data.files)
101
- next_page_info = (
102
- f"Next page token needed to fetch more data: {data.next_page_token}"
103
- if data.next_page_token
104
- else "There're no more pages."
105
- )
92
+
106
93
  return ToolResult(
107
- content=f"Successfully listed {number_of_files} files. {next_page_info}",
108
94
  structured_content={
109
95
  "files": [
110
96
  file.model_dump(by_alias=True, include=filtered_fields) for file in data.files
@@ -127,8 +113,7 @@ async def gdrive_read_content(
127
113
  ] = None,
128
114
  ) -> ToolResult:
129
115
  """
130
- Retrieve the content of a specific file by its ID. Google Workspace files are
131
- automatically exported to LLM-readable formats (Push-Down).
116
+ Retrieve the content of a specific Google drive file by its ID.
132
117
 
133
118
  Usage:
134
119
  - Basic: gdrive_read_content(file_id="1ABC123def456")
@@ -155,27 +140,10 @@ async def gdrive_read_content(
155
140
  if isinstance(access_token, ToolError):
156
141
  raise access_token
157
142
 
158
- try:
159
- async with GoogleDriveClient(access_token) as client:
160
- file_content = await client.read_file_content(file_id, target_format)
161
- except GoogleDriveError as e:
162
- logger.error(f"Google Drive error reading file content: {e}")
163
- raise ToolError(str(e))
164
- except Exception as e:
165
- logger.error(f"Unexpected error reading Google Drive file content: {e}")
166
- raise ToolError(
167
- f"An unexpected error occurred while reading Google Drive file content: {str(e)}"
168
- )
169
-
170
- export_info = ""
171
- if file_content.was_exported:
172
- export_info = f" (exported from {file_content.original_mime_type})"
143
+ async with GoogleDriveClient(access_token) as client:
144
+ file_content = await client.read_file_content(file_id, target_format)
173
145
 
174
146
  return ToolResult(
175
- content=(
176
- f"Successfully retrieved content of '{file_content.name}' "
177
- f"({file_content.mime_type}){export_info}."
178
- ),
179
147
  structured_content=file_content.as_flat_dict(),
180
148
  )
181
149
 
@@ -239,28 +207,15 @@ async def gdrive_create_file(
239
207
  if isinstance(access_token, ToolError):
240
208
  raise access_token
241
209
 
242
- try:
243
- async with GoogleDriveClient(access_token) as client:
244
- created_file = await client.create_file(
245
- name=name,
246
- mime_type=mime_type,
247
- parent_id=parent_id,
248
- initial_content=initial_content,
249
- )
250
- except GoogleDriveError as e:
251
- logger.error(f"Google Drive error creating file: {e}")
252
- raise ToolError(str(e))
253
- except Exception as e:
254
- logger.error(f"Unexpected error creating Google Drive file: {e}")
255
- raise ToolError(f"An unexpected error occurred while creating Google Drive file: {str(e)}")
256
-
257
- file_type = "folder" if mime_type == GOOGLE_DRIVE_FOLDER_MIME else "file"
258
- content_info = ""
259
- if initial_content and mime_type != GOOGLE_DRIVE_FOLDER_MIME:
260
- content_info = " with initial content"
210
+ async with GoogleDriveClient(access_token) as client:
211
+ created_file = await client.create_file(
212
+ name=name,
213
+ mime_type=mime_type,
214
+ parent_id=parent_id,
215
+ initial_content=initial_content,
216
+ )
261
217
 
262
218
  return ToolResult(
263
- content=f"Successfully created {file_type} '{created_file.name}'{content_info}.",
264
219
  structured_content=created_file.as_flat_dict(),
265
220
  )
266
221
 
@@ -313,44 +268,20 @@ async def gdrive_update_metadata(
313
268
  if isinstance(access_token, ToolError):
314
269
  raise access_token
315
270
 
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)}"
271
+ async with GoogleDriveClient(access_token) as client:
272
+ updated_file = await client.update_file_metadata(
273
+ file_id=file_id,
274
+ new_name=new_name,
275
+ starred=starred,
276
+ trashed=trash,
331
277
  )
332
278
 
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
279
  return ToolResult(
348
- content=f"Successfully updated file '{updated_file.name}': {changes_description}.",
349
280
  structured_content=updated_file.as_flat_dict(),
350
281
  )
351
282
 
352
283
 
353
- @dr_mcp_tool(tags={"google", "gdrive", "manage", "access", "acl"})
284
+ @dr_mcp_tool(tags={"google", "gdrive", "manage", "access", "acl"}, enabled=False)
354
285
  async def gdrive_manage_access(
355
286
  *,
356
287
  file_id: Annotated[str, "The ID of the file or folder."],
@@ -408,39 +339,19 @@ async def gdrive_manage_access(
408
339
  if isinstance(access_token, ToolError):
409
340
  raise access_token
410
341
 
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)}"
342
+ async with GoogleDriveClient(access_token) as client:
343
+ permission_id = await client.manage_access(
344
+ file_id=file_id,
345
+ action=action,
346
+ role=role,
347
+ email_address=email_address,
348
+ permission_id=permission_id,
349
+ transfer_ownership=transfer_ownership,
428
350
  )
429
351
 
430
352
  # Build response
431
353
  structured_content = {"affectedFileId": file_id}
432
354
  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
355
  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
356
 
446
- return ToolResult(content=content, structured_content=structured_content)
357
+ return ToolResult(structured_content=structured_content)
@@ -53,10 +53,11 @@ async def jira_search_issues(
53
53
  async with JiraClient(access_token) as client:
54
54
  issues = await client.search_jira_issues(jql_query=jql_query, max_results=max_results)
55
55
 
56
- n = len(issues)
57
56
  return ToolResult(
58
- content=f"Successfully executed JQL query and retrieved {n} issue(s).",
59
- structured_content={"data": [issue.as_flat_dict() for issue in issues], "count": n},
57
+ structured_content={
58
+ "data": [issue.as_flat_dict() for issue in issues],
59
+ "count": len(issues),
60
+ },
60
61
  )
61
62
 
62
63
 
@@ -72,17 +73,10 @@ async def jira_get_issue(
72
73
  if isinstance(access_token, ToolError):
73
74
  raise access_token
74
75
 
75
- try:
76
- async with JiraClient(access_token) as client:
77
- issue = await client.get_jira_issue(issue_key)
78
- except Exception as e:
79
- logger.error(f"Unexpected error while getting Jira issue: {e}")
80
- raise ToolError(
81
- f"An unexpected error occurred while getting Jira issue '{issue_key}': {str(e)}"
82
- )
76
+ async with JiraClient(access_token) as client:
77
+ issue = await client.get_jira_issue(issue_key)
83
78
 
84
79
  return ToolResult(
85
- content=f"Successfully retrieved details for issue '{issue_key}'.",
86
80
  structured_content=issue.as_flat_dict(),
87
81
  )
88
82
 
@@ -118,20 +112,15 @@ async def jira_create_issue(
118
112
  f"Unexpected issue type `{issue_type}`. Possible values are {possible_issue_types}."
119
113
  )
120
114
 
121
- try:
122
- async with JiraClient(access_token) as client:
123
- issue_key = await client.create_jira_issue(
124
- project_key=project_key,
125
- summary=summary,
126
- issue_type_id=issue_type_id,
127
- description=description,
128
- )
129
- except Exception as e:
130
- logger.error(f"Unexpected error while creating Jira issue: {e}")
131
- raise ToolError(f"An unexpected error occurred while creating Jira issue: {str(e)}")
115
+ async with JiraClient(access_token) as client:
116
+ issue_key = await client.create_jira_issue(
117
+ project_key=project_key,
118
+ summary=summary,
119
+ issue_type_id=issue_type_id,
120
+ description=description,
121
+ )
132
122
 
133
123
  return ToolResult(
134
- content=f"Successfully created issue '{issue_key}'.",
135
124
  structured_content={"newIssueKey": issue_key, "projectKey": project_key},
136
125
  )
137
126
 
@@ -179,18 +168,12 @@ async def jira_update_issue(
179
168
  if isinstance(access_token, ToolError):
180
169
  raise access_token
181
170
 
182
- try:
183
- async with JiraClient(access_token) as client:
184
- updated_fields = await client.update_jira_issue(
185
- issue_key=issue_key, fields=fields_to_update
186
- )
187
- except Exception as e:
188
- logger.error(f"Unexpected error while updating Jira issue: {e}")
189
- raise ToolError(f"An unexpected error occurred while updating Jira issue: {str(e)}")
171
+ async with JiraClient(access_token) as client:
172
+ updated_fields = await client.update_jira_issue(
173
+ issue_key=issue_key, fields=fields_to_update
174
+ )
190
175
 
191
- updated_fields_str = ",".join(updated_fields)
192
176
  return ToolResult(
193
- content=f"Successfully updated issue '{issue_key}'. Fields modified: {updated_fields_str}.",
194
177
  structured_content={"updatedIssueKey": issue_key, "fields": updated_fields},
195
178
  )
196
179
 
@@ -226,15 +209,10 @@ async def jira_transition_issue(
226
209
  f"Possible values are {available_transitions_str}."
227
210
  )
228
211
 
229
- try:
230
- async with JiraClient(access_token) as client:
231
- await client.transition_jira_issue(issue_key=issue_key, transition_id=transition_id)
232
- except Exception as e:
233
- logger.error(f"Unexpected error while transitioning Jira issue: {e}")
234
- raise ToolError(f"An unexpected error occurred while transitioning Jira issue: {str(e)}")
212
+ async with JiraClient(access_token) as client:
213
+ await client.transition_jira_issue(issue_key=issue_key, transition_id=transition_id)
235
214
 
236
215
  return ToolResult(
237
- content=f"Successfully transitioned issue '{issue_key}' to status '{transition_name}'.",
238
216
  structured_content={
239
217
  "transitionedIssueKey": issue_key,
240
218
  "newStatusName": transition_name,
@@ -16,13 +16,14 @@
16
16
 
17
17
  import logging
18
18
  from typing import Annotated
19
+ from typing import Any
20
+ from typing import Literal
19
21
 
20
22
  from fastmcp.exceptions import ToolError
21
23
  from fastmcp.tools.tool import ToolResult
22
24
 
23
25
  from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
24
26
  from datarobot_genai.drmcp.tools.clients.microsoft_graph import MicrosoftGraphClient
25
- from datarobot_genai.drmcp.tools.clients.microsoft_graph import MicrosoftGraphError
26
27
  from datarobot_genai.drmcp.tools.clients.microsoft_graph import get_microsoft_graph_access_token
27
28
  from datarobot_genai.drmcp.tools.clients.microsoft_graph import validate_site_url
28
29
 
@@ -124,10 +125,6 @@ async def microsoft_graph_search_content(
124
125
  - Documentation: https://learn.microsoft.com/en-us/graph/api/search-query
125
126
  - Search concepts: https://learn.microsoft.com/en-us/graph/search-concept-files
126
127
 
127
- Permissions:
128
- - Requires Sites.Read.All or Sites.Search.All permission
129
- - include_hidden_content only works with delegated permissions
130
- - region parameter is required for application permissions in multi-region environments
131
128
  """
132
129
  if not search_query:
133
130
  raise ToolError("Argument validation error: 'search_query' cannot be empty.")
@@ -142,25 +139,16 @@ async def microsoft_graph_search_content(
142
139
  if isinstance(access_token, ToolError):
143
140
  raise access_token
144
141
 
145
- try:
146
- async with MicrosoftGraphClient(access_token=access_token, site_url=site_url) as client:
147
- items = await client.search_content(
148
- search_query=search_query,
149
- site_id=site_id,
150
- from_offset=from_offset,
151
- size=size,
152
- entity_types=entity_types,
153
- filters=filters,
154
- include_hidden_content=include_hidden_content,
155
- region=region,
156
- )
157
- except MicrosoftGraphError as e:
158
- logger.error(f"Microsoft Graph error searching content: {e}")
159
- raise ToolError(str(e))
160
- except Exception as e:
161
- logger.error(f"Unexpected error searching Microsoft Graph content: {e}", exc_info=True)
162
- raise ToolError(
163
- f"An unexpected error occurred while searching Microsoft Graph content: {str(e)}"
142
+ async with MicrosoftGraphClient(access_token=access_token, site_url=site_url) as client:
143
+ items = await client.search_content(
144
+ search_query=search_query,
145
+ site_id=site_id,
146
+ from_offset=from_offset,
147
+ size=size,
148
+ entity_types=entity_types,
149
+ filters=filters,
150
+ include_hidden_content=include_hidden_content,
151
+ region=region,
164
152
  )
165
153
 
166
154
  results = []
@@ -180,12 +168,7 @@ async def microsoft_graph_search_content(
180
168
  }
181
169
  results.append(result_dict)
182
170
 
183
- n = len(results)
184
171
  return ToolResult(
185
- content=(
186
- f"Successfully searched Microsoft Graph and retrieved {n} result(s) for "
187
- f"'{search_query}' (from={from_offset}, size={size})."
188
- ),
189
172
  structured_content={
190
173
  "query": search_query,
191
174
  "siteUrl": site_url,
@@ -193,7 +176,63 @@ async def microsoft_graph_search_content(
193
176
  "from": from_offset,
194
177
  "size": size,
195
178
  "results": results,
196
- "count": n,
179
+ "count": len(results),
180
+ },
181
+ )
182
+
183
+
184
+ @dr_mcp_tool(tags={"microsoft", "graph api", "sharepoint", "onedrive", "share"}, enabled=False)
185
+ async def microsoft_graph_share_item(
186
+ *,
187
+ file_id: Annotated[str, "The ID of the file or folder to share."],
188
+ document_library_id: Annotated[str, "The ID of the document library containing the item."],
189
+ recipient_emails: Annotated[list[str], "A list of email addresses to invite."],
190
+ role: Annotated[Literal["read", "write"], "The role to assign: 'read' or 'write'."] = "read",
191
+ send_invitation: Annotated[
192
+ bool, "Flag determining if recipients should be notified. Default False"
193
+ ] = False,
194
+ ) -> ToolResult | ToolError:
195
+ """
196
+ Share a SharePoint or Onedrive file or folder with one or more users.
197
+ It works with internal users or existing guest users in the
198
+ tenant. It does NOT create new guest accounts and does NOT use the tenant-level
199
+ /invitations endpoint.
200
+
201
+ Microsoft Graph API is treating OneDrive and SharePoint resources as driveItem.
202
+
203
+ API Reference:
204
+ - DriveItem Resource Type: https://learn.microsoft.com/en-us/graph/api/resources/driveitem
205
+ - API Documentation: https://learn.microsoft.com/en-us/graph/api/driveitem-invite
206
+ """
207
+ if not file_id or not file_id.strip():
208
+ raise ToolError("Argument validation error: 'file_id' cannot be empty.")
209
+
210
+ if not document_library_id or not document_library_id.strip():
211
+ raise ToolError("Argument validation error: 'document_library_id' cannot be empty.")
212
+
213
+ if not recipient_emails:
214
+ raise ToolError("Argument validation error: you must provide at least one 'recipient'.")
215
+
216
+ access_token = await get_microsoft_graph_access_token()
217
+ if isinstance(access_token, ToolError):
218
+ raise access_token
219
+
220
+ async with MicrosoftGraphClient(access_token=access_token) as client:
221
+ await client.share_item(
222
+ file_id=file_id,
223
+ document_library_id=document_library_id,
224
+ recipient_emails=recipient_emails,
225
+ role=role,
226
+ send_invitation=send_invitation,
227
+ )
228
+
229
+ return ToolResult(
230
+ structured_content={
231
+ "fileId": file_id,
232
+ "documentLibraryId": document_library_id,
233
+ "recipientEmails": recipient_emails,
234
+ "n": len(recipient_emails),
235
+ "role": role,
197
236
  },
198
237
  )
199
238
 
@@ -208,7 +247,8 @@ async def microsoft_graph_search_content(
208
247
  "create",
209
248
  "file",
210
249
  "write",
211
- }
250
+ },
251
+ enabled=False,
212
252
  )
213
253
  async def microsoft_create_file(
214
254
  *,
@@ -265,7 +305,6 @@ async def microsoft_create_file(
265
305
  )
266
306
 
267
307
  return ToolResult(
268
- content=f"File '{created_file.name}' created successfully.",
269
308
  structured_content={
270
309
  "file_name": created_file.name,
271
310
  "destination": "onedrive" if is_personal_onedrive else "sharepoint",
@@ -275,3 +314,133 @@ async def microsoft_create_file(
275
314
  "parentFolderId": created_file.parent_folder_id,
276
315
  },
277
316
  )
317
+
318
+
319
+ @dr_mcp_tool(
320
+ tags={
321
+ "microsoft",
322
+ "graph api",
323
+ "sharepoint",
324
+ "onedrive",
325
+ "metadata",
326
+ "update",
327
+ "fields",
328
+ "compliance",
329
+ },
330
+ enabled=False,
331
+ )
332
+ async def microsoft_update_metadata(
333
+ *,
334
+ item_id: Annotated[str, "The ID of the file or list item to update."],
335
+ fields_to_update: Annotated[
336
+ dict[str, Any],
337
+ "Key-value pairs of metadata fields to modify. "
338
+ "For SharePoint list items: any custom column values. "
339
+ "For drive items: 'name' and/or 'description'.",
340
+ ],
341
+ site_id: Annotated[
342
+ str | None,
343
+ "The site ID (required for SharePoint list items, along with list_id).",
344
+ ] = None,
345
+ list_id: Annotated[
346
+ str | None,
347
+ "The list ID (required for SharePoint list items, along with site_id).",
348
+ ] = None,
349
+ document_library_id: Annotated[
350
+ str | None,
351
+ "The drive ID (required for OneDrive/drive item updates). "
352
+ "Cannot be used together with site_id and list_id.",
353
+ ] = None,
354
+ ) -> ToolResult | ToolError:
355
+ """
356
+ Update metadata on a SharePoint list item or OneDrive/SharePoint drive item.
357
+
358
+ **SharePoint List Items:** Provide site_id and list_id to update custom
359
+ column values on a list item. All custom columns can be updated.
360
+
361
+ **OneDrive/Drive Items:** Provide document_library_id to update drive item
362
+ properties. Only 'name' and 'description' fields can be updated.
363
+
364
+ **Context Requirements:**
365
+ - For SharePoint list items: Both site_id AND list_id are required
366
+ - For OneDrive/drive items: document_library_id is required
367
+ - Cannot specify both contexts simultaneously
368
+
369
+ **Examples:**
370
+ - SharePoint list item: Update a 'Status' column to 'Approved'
371
+ - Drive item: Rename a file or update its description
372
+
373
+ """
374
+ if not item_id or not item_id.strip():
375
+ raise ToolError("Error: item_id is required.")
376
+ if not fields_to_update:
377
+ raise ToolError("Error: fields_to_update is required and cannot be empty.")
378
+
379
+ # Validate context parameters
380
+ has_sharepoint_context = site_id is not None and list_id is not None
381
+ has_partial_sharepoint_context = (site_id is not None) != (list_id is not None)
382
+ has_drive_context = document_library_id is not None
383
+
384
+ if has_partial_sharepoint_context:
385
+ raise ToolError(
386
+ "Error: For SharePoint list items, both site_id and list_id must be provided."
387
+ )
388
+
389
+ if has_sharepoint_context and has_drive_context:
390
+ raise ToolError(
391
+ "Error: Cannot specify both SharePoint (site_id + list_id) and OneDrive "
392
+ "(document_library_id) context. Choose one."
393
+ )
394
+
395
+ if not has_sharepoint_context and not has_drive_context:
396
+ raise ToolError(
397
+ "Error: Must specify either SharePoint context (site_id + list_id) or "
398
+ "OneDrive context (document_library_id)."
399
+ )
400
+
401
+ access_token = await get_microsoft_graph_access_token()
402
+ if isinstance(access_token, ToolError):
403
+ raise access_token
404
+
405
+ async with MicrosoftGraphClient(access_token=access_token) as client:
406
+ result = await client.update_item_metadata(
407
+ item_id=item_id.strip(),
408
+ fields_to_update=fields_to_update,
409
+ site_id=site_id,
410
+ list_id=list_id,
411
+ drive_id=document_library_id,
412
+ )
413
+
414
+ context_type = "sharepoint_list_item" if has_sharepoint_context else "drive_item"
415
+ structured: dict[str, Any] = {
416
+ "item_id": item_id,
417
+ "context_type": context_type,
418
+ "fields_updated": list(fields_to_update.keys()),
419
+ }
420
+
421
+ # Add context-specific IDs for traceability
422
+ if has_sharepoint_context:
423
+ structured["site_id"] = site_id
424
+ structured["list_id"] = list_id
425
+ else:
426
+ structured["document_library_id"] = document_library_id
427
+
428
+ # Include relevant response data
429
+ if isinstance(result, dict):
430
+ # For drive items, include key properties if present
431
+ if has_drive_context:
432
+ if "id" in result:
433
+ structured["id"] = result["id"]
434
+ if "name" in result:
435
+ structured["name"] = result["name"]
436
+ if "webUrl" in result:
437
+ structured["webUrl"] = result["webUrl"]
438
+ if "description" in result:
439
+ structured["description"] = result.get("description")
440
+ # For list items, the response is the fields object itself
441
+ else:
442
+ structured["updated_fields"] = result
443
+
444
+ return ToolResult(
445
+ structured_content=structured,
446
+ )
File without changes