universal-mcp-applications 0.1.23__py3-none-any.whl → 0.1.25__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.
@@ -49,7 +49,7 @@ class BrowserUseApp(APIApplication):
49
49
  llm=llm, task=task, max_steps=max_steps
50
50
  )
51
51
  result = created_task.complete()
52
- return result.to_dict()
52
+ return result.model_dump()
53
53
 
54
54
  async def get_browser_task_status(
55
55
  self,
@@ -65,7 +65,7 @@ class BrowserUseApp(APIApplication):
65
65
  dict: The current status and details of the task.
66
66
  """
67
67
  task = self.browser_client.tasks.get_task(task_id)
68
- return task.to_dict()
68
+ return task.model_dump()
69
69
 
70
70
  def list_tools(self):
71
71
  return [self.browser_task, self.get_browser_task_status]
@@ -55,37 +55,171 @@ class GoogleDocsApp(APIApplication):
55
55
 
56
56
  def get_document_content(self, document_id: str) -> dict[str, Any]:
57
57
  """
58
- Retrieves a document's raw data via `get_document`, then parses the complex JSON to extract and concatenate all plain text from its body. This function returns a simplified dictionary containing only the title and the clean, concatenated text content, distinct from `get_document`'s full metadata response.
59
-
58
+ Retrieves and converts a Google Docs document into Markdown-formatted content.
59
+
60
+ This method calls the Google Docs API via `get_document`, then parses the document structure
61
+ to extract paragraphs, headings, lists, tables, images, equations, footnotes, and horizontal rules.
62
+ The final result is a clean Markdown string that closely mirrors the layout and content of
63
+ the original document, including support for nested lists and multi-row tables.
64
+
60
65
  Args:
61
- document_id: The unique identifier of the Google Document to retrieve.
62
-
66
+ document_id (str): The unique ID of the Google Document to retrieve.
67
+
63
68
  Returns:
64
- A dictionary containing the document's title under the key 'title' and the concatenated plain text content under the key 'content'.
65
-
66
- Raises:
67
- KeyError: If the response structure from get_document is missing expected keys such as 'body' or 'content', a KeyError may be raised during extraction.
68
- Exception: Any exception raised by the underlying get_document call, such as network errors or API issues, will propagate.
69
-
69
+ dict[str, Any]: A dictionary with the following keys:
70
+ - 'title' (str): The document's title.
71
+ - 'content' (str): The document content converted to Markdown.
72
+ Markdown Output Supports:
73
+ - Paragraphs and line breaks
74
+ - Headings (Heading 1-6)
75
+ - Bulleted and numbered lists (with nesting)
76
+ - Tables (converted to Markdown tables)
77
+ - Inline images (`![](URL)` format)
78
+ - Equations (LaTeX-style `$...$`)
79
+ - Footnotes (`[^1]` references and notes at the end)
80
+ - Horizontal rules (`---`)
81
+
70
82
  Tags:
71
- retrieve, document, text-processing, parsing, important
83
+ google-docs, markdown, document-parsing, text-extraction, conversion, structured-data
72
84
  """
85
+ import re
86
+
73
87
  response = self.get_document(document_id)
74
88
  title = response.get("title", "")
75
- text_chunks: list[str] = []
76
89
  body_content = response.get("body", {}).get("content", [])
90
+ inline_objects = response.get("inlineObjects", {})
91
+ lists = response.get("lists", {})
92
+ footnotes_data = response.get("footnotes", {})
93
+
94
+ text_chunks: list[str] = []
95
+ footnotes: dict[str, str] = {}
96
+ footnote_index: dict[str, int] = {}
97
+ current_list_counters: dict[str, int] = {}
98
+
99
+ def extract_text_from_paragraph(paragraph: dict) -> str:
100
+ """Extracts paragraph text with inline formatting."""
101
+ text = ""
102
+ for elem in paragraph.get("elements", []):
103
+ if "textRun" in elem:
104
+ content = elem["textRun"].get("content", "")
105
+ text += content
106
+ elif "inlineObjectElement" in elem:
107
+ obj_id = elem["inlineObjectElement"]["inlineObjectId"]
108
+ obj = inline_objects.get(obj_id, {})
109
+ embed = obj.get("inlineObjectProperties", {}).get("embeddedObject", {})
110
+ image_source = embed.get("imageProperties", {}).get("contentUri", "")
111
+ if image_source:
112
+ text += f"\n\n![]({image_source})\n\n"
113
+ return text
114
+
115
+ def extract_table(table: dict) -> str:
116
+ rows = []
117
+ for i, row in enumerate(table.get("tableRows", [])):
118
+ cells = []
119
+ for cell in row.get("tableCells", []):
120
+ cell_text = ""
121
+ for content in cell.get("content", []):
122
+ if "paragraph" in content:
123
+ cell_text += extract_text_from_paragraph(content["paragraph"]).strip()
124
+ cells.append(cell_text)
125
+ row_line = "| " + " | ".join(cells) + " |"
126
+ rows.append(row_line)
127
+ if i == 0:
128
+ rows.insert(1, "| " + " | ".join(["---"] * len(cells)) + " |")
129
+ return "\n".join(rows)
130
+
131
+ def extract_heading_style(paragraph: dict) -> str:
132
+ """Returns appropriate Markdown heading level."""
133
+ style = paragraph.get("paragraphStyle", {})
134
+ heading = style.get("namedStyleType", "")
135
+ match = re.match(r"HEADING_(\d)", heading)
136
+ if match:
137
+ level = int(match.group(1))
138
+ return "#" * level
139
+ return ""
140
+
141
+ def extract_list_prefix(paragraph: dict) -> str:
142
+ """Generates proper list prefix (numbered or bullet)."""
143
+ list_id = paragraph.get("bullet", {}).get("listId")
144
+ if not list_id:
145
+ return ""
146
+ glyph = paragraph["bullet"].get("glyph", None)
147
+ nesting = paragraph["bullet"].get("nestingLevel", 0)
148
+ list_info = lists.get(list_id, {})
149
+ list_type = list_info.get("listProperties", {}).get("nestingLevels", [{}])[nesting].get("glyphType")
150
+
151
+ indent = " " * nesting
152
+ if list_type and "DECIMAL" in list_type:
153
+ current_list_counters[list_id] = current_list_counters.get(list_id, 1)
154
+ prefix = f"{current_list_counters[list_id]}. "
155
+ current_list_counters[list_id] += 1
156
+ else:
157
+ prefix = "- "
158
+ return indent + prefix
159
+
160
+ def extract_equation(paragraph: dict) -> str:
161
+ return "\n\n$" + paragraph.get("equation", {}).get("equation", "") + "$\n\n"
162
+
163
+ def extract_footnote_ref(footnote_id: str) -> str:
164
+ if footnote_id not in footnote_index:
165
+ footnote_index[footnote_id] = len(footnotes) + 1
166
+ fn_content = footnotes_data.get(footnote_id, {}).get("content", [])
167
+ fn_text = ""
168
+ for item in fn_content:
169
+ if "paragraph" in item:
170
+ fn_text += extract_text_from_paragraph(item["paragraph"]).strip()
171
+ footnotes[footnote_id] = fn_text
172
+ return f"[^{footnote_index[footnote_id]}]"
173
+
77
174
  for element in body_content:
78
175
  if "paragraph" in element:
79
- for para_elem in element["paragraph"].get("elements", []):
80
- text_run = para_elem.get("textRun")
81
- if text_run and "content" in text_run:
82
- text_chunks.append(text_run["content"])
83
- content = "".join(text_chunks).strip()
176
+ para = element["paragraph"]
177
+ if "equation" in para:
178
+ text_chunks.append(extract_equation(para))
179
+ continue
180
+
181
+ heading_md = extract_heading_style(para)
182
+ list_prefix = extract_list_prefix(para)
183
+
184
+ para_text = extract_text_from_paragraph(para).strip()
185
+ if para_text:
186
+ if heading_md:
187
+ text_chunks.append(f"{heading_md} {para_text}")
188
+ elif list_prefix:
189
+ text_chunks.append(f"{list_prefix}{para_text}")
190
+ else:
191
+ text_chunks.append(para_text)
192
+
193
+ elif "table" in element:
194
+ table_md = extract_table(element["table"])
195
+ text_chunks.append(table_md)
196
+
197
+ elif "horizontalRule" in element:
198
+ text_chunks.append("\n---\n")
199
+
200
+ elif "tableOfContents" in element:
201
+ text_chunks.append("<!-- Table of Contents -->")
202
+
203
+ # Handle footnote references (inline elements)
204
+ elif "footnoteReference" in element:
205
+ footnote_id = element["footnoteReference"]["footnoteId"]
206
+ ref = extract_footnote_ref(footnote_id)
207
+ text_chunks.append(ref)
208
+
209
+ # Append footnotes at the end
210
+ if footnotes:
211
+ text_chunks.append("\n## Footnotes\n")
212
+ for fid, index in sorted(footnote_index.items(), key=lambda x: x[1]):
213
+ text_chunks.append(f"[^{index}]: {footnotes[fid]}")
214
+
215
+ content = "\n\n".join(chunk.strip() for chunk in text_chunks if chunk.strip())
216
+
84
217
  return {
85
218
  "title": title,
86
- "content": content,
219
+ "content": content
87
220
  }
88
221
 
222
+
89
223
  def insert_text(
90
224
  self, document_id: str, content: str, index: int = 1
91
225
  ) -> dict[str, Any]:
@@ -21,8 +21,8 @@ class OutlookApp(APIApplication):
21
21
  Replies to an email using its message ID, with either a simple comment or a full message object including attachments. Unlike `send_mail`, which creates a new email, this function targets an existing message. It defaults to the current user if no user ID is specified.
22
22
 
23
23
  Args:
24
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
25
24
  message_id (string): message-id
25
+ user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
26
26
  comment (string): A comment to include in the reply. Example: 'Thank you for your email. Here is my reply.'.
27
27
  message (object): A message object to specify additional properties for the reply, such as attachments. Example: {'subject': 'RE: Project Update', 'body': {'contentType': 'Text', 'content': 'Thank you for the update. Looking forward to the next steps.'}, 'toRecipients': [{'emailAddress': {'address': 'alice@contoso.com'}}], 'attachments': [{'@odata.type': '#microsoft.graph.fileAttachment', 'name': 'agenda.pdf', 'contentType': 'application/pdf', 'contentBytes': 'SGVsbG8gV29ybGQh'}]}.
28
28
 
@@ -40,9 +40,7 @@ class OutlookApp(APIApplication):
40
40
  user_info = self.get_current_user_profile()
41
41
  user_id = user_info.get("userPrincipalName")
42
42
  if not user_id:
43
- raise ValueError(
44
- "Could not retrieve user ID from get_current_user_profile response."
45
- )
43
+ raise ValueError("Could not retrieve user ID from get_current_user_profile response.")
46
44
  if message_id is None:
47
45
  raise ValueError("Missing required parameter 'message-id'.")
48
46
  request_body_data = None
@@ -50,9 +48,7 @@ class OutlookApp(APIApplication):
50
48
  "comment": comment,
51
49
  "message": message,
52
50
  }
53
- request_body_data = {
54
- k: v for k, v in request_body_data.items() if v is not None
55
- }
51
+ request_body_data = {k: v for k, v in request_body_data.items() if v is not None}
56
52
  url = f"{self.base_url}/users/{user_id}/messages/{message_id}/reply"
57
53
  query_params = {}
58
54
  response = self._post(
@@ -73,8 +69,8 @@ class OutlookApp(APIApplication):
73
69
  Sends a new email on behalf of a specified or current user, using a dictionary for content like recipients and subject. Unlike `reply_to_message`, which replies to an existing message, this function composes and sends an entirely new email from scratch.
74
70
 
75
71
  Args:
76
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
77
72
  message (object): message Example: {'subject': 'Meet for lunch?', 'body': {'contentType': 'Text', 'content': 'The new cafeteria is open.'}, 'toRecipients': [{'emailAddress': {'address': 'frannis@contoso.com'}}], 'ccRecipients': [{'emailAddress': {'address': 'danas@contoso.com'}}], 'bccRecipients': [{'emailAddress': {'address': 'bccuser@contoso.com'}}], 'attachments': [{'@odata.type': '#microsoft.graph.fileAttachment', 'name': 'attachment.txt', 'contentType': 'text/plain', 'contentBytes': 'SGVsbG8gV29ybGQh'}]}.
73
+ user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
78
74
  saveToSentItems (boolean): saveToSentItems Example: 'False'.
79
75
 
80
76
  Returns:
@@ -91,17 +87,13 @@ class OutlookApp(APIApplication):
91
87
  user_info = self.get_current_user_profile()
92
88
  user_id = user_info.get("userPrincipalName")
93
89
  if not user_id:
94
- raise ValueError(
95
- "Could not retrieve user ID from get_current_user_profile response."
96
- )
90
+ raise ValueError("Could not retrieve user ID from get_current_user_profile response.")
97
91
  request_body_data = None
98
92
  request_body_data = {
99
93
  "message": message,
100
94
  "saveToSentItems": saveToSentItems,
101
95
  }
102
- request_body_data = {
103
- k: v for k, v in request_body_data.items() if v is not None
104
- }
96
+ request_body_data = {k: v for k, v in request_body_data.items() if v is not None}
105
97
  url = f"{self.base_url}/users/{user_id}/sendMail"
106
98
  query_params = {}
107
99
  response = self._post(
@@ -116,7 +108,7 @@ class OutlookApp(APIApplication):
116
108
  self,
117
109
  mailFolder_id: str,
118
110
  user_id: str | None = None,
119
- includeHiddenFolders: str | None = None,
111
+ includeHiddenFolders: bool | None = None,
120
112
  select: list[str] | None = None,
121
113
  expand: list[str] | None = None,
122
114
  ) -> Any:
@@ -124,9 +116,9 @@ class OutlookApp(APIApplication):
124
116
  Retrieves a specific mail folder's metadata by its ID for a given user. The response can be customized to include hidden folders or select specific properties. Unlike `list_user_messages`, this function fetches folder details, not the emails contained within it.
125
117
 
126
118
  Args:
127
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
128
119
  mailFolder_id (string): mailFolder-id
129
- includeHiddenFolders (string): Include Hidden Folders
120
+ user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
121
+ includeHiddenFolders (boolean): Include Hidden Folders
130
122
  select (array): Select properties to be returned
131
123
  expand (array): Expand related entities
132
124
 
@@ -144,18 +136,19 @@ class OutlookApp(APIApplication):
144
136
  user_info = self.get_current_user_profile()
145
137
  user_id = user_info.get("userPrincipalName")
146
138
  if not user_id:
147
- raise ValueError(
148
- "Could not retrieve user ID from get_current_user_profile response."
149
- )
139
+ raise ValueError("Could not retrieve user ID from get_current_user_profile response.")
150
140
  if mailFolder_id is None:
151
141
  raise ValueError("Missing required parameter 'mailFolder-id'.")
152
142
  url = f"{self.base_url}/users/{user_id}/mailFolders/{mailFolder_id}"
143
+ select_str = ",".join(select) if select else None
144
+ expand_str = ",".join(expand) if expand else None
145
+
153
146
  query_params = {
154
147
  k: v
155
148
  for k, v in [
156
149
  ("includeHiddenFolders", includeHiddenFolders),
157
- ("$select", select),
158
- ("$expand", expand),
150
+ ("$select", select_str),
151
+ ("$expand", expand_str),
159
152
  ]
160
153
  if v is not None
161
154
  }
@@ -178,6 +171,12 @@ class OutlookApp(APIApplication):
178
171
  """
179
172
  Retrieves a list of messages from a user's mailbox. This function supports powerful querying using optional parameters for filtering, searching, sorting, and pagination, unlike `get_user_message`, which fetches a single email by its ID.
180
173
 
174
+ IMPORTANT LIMITATIONS (Microsoft Graph API restrictions):
175
+ - `search` cannot be used with `filter`
176
+ - `search` cannot be used with `orderby`
177
+ - `search` cannot be used with `skip` (use pagination via @odata.nextLink and get_next_page instead)
178
+ - When using `search`, pagination uses $skiptoken instead of $skip
179
+
181
180
  Args:
182
181
  user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
183
182
  select (list): Select properties to be returned. Defaults to ['bodyPreview'].
@@ -190,30 +189,46 @@ class OutlookApp(APIApplication):
190
189
  ]
191
190
  includeHiddenMessages (boolean): Include Hidden Messages
192
191
  top (integer): Specify the number of items to be included in the result Example: '50'.
193
- skip (integer): Specify the number of items to skip in the result Example: '10'.
194
- search (string): Search items by search phrases
195
- filter (string): Filter items by property values
192
+ skip (integer): Specify the number of items to skip in the result. Cannot be used with 'search'. Example: '10'.
193
+ search (string): Search items by search phrases. Cannot be used with 'filter', 'orderby', or 'skip'.
194
+ filter (string): Filter items by property values. Cannot be used with 'search'.
196
195
  count (boolean): Include count of items
197
- orderby (array): Order items by property values
196
+ orderby (array): Order items by property values. Cannot be used with 'search'.
198
197
  expand (array): Expand related entities
199
198
 
200
199
  Returns:
201
200
  dict[str, Any]: Retrieved collection
202
201
 
203
202
  Raises:
203
+ ValueError: If incompatible parameters are used together (search with filter/orderby/skip).
204
204
  HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
205
205
 
206
206
  Tags:
207
207
  users.message, important
208
208
  """
209
+ if search:
210
+ if filter:
211
+ raise ValueError(
212
+ "The 'search' parameter cannot be used together with 'filter'. "
213
+ "This is a Microsoft Graph API restriction. Please use either search or filter, not both."
214
+ )
215
+ if orderby:
216
+ raise ValueError(
217
+ "The 'search' parameter cannot be used together with 'orderby'. "
218
+ "This is a Microsoft Graph API restriction. When using search, results are sorted by relevance."
219
+ )
220
+ if skip:
221
+ raise ValueError(
222
+ "The 'search' parameter cannot be used together with 'skip'. "
223
+ "When using search, use pagination via @odata.nextLink and the get_next_page function instead."
224
+ )
225
+
209
226
  # If user_id is not provided, get it automatically
210
227
  if user_id is None:
211
228
  user_info = self.get_current_user_profile()
212
229
  user_id = user_info.get("userPrincipalName")
213
230
  if not user_id:
214
- raise ValueError(
215
- "Could not retrieve user ID from get_current_user_profile response."
216
- )
231
+ raise ValueError("Could not retrieve user ID from get_current_user_profile response.")
217
232
 
218
233
  url = f"{self.base_url}/users/{user_id}/messages"
219
234
 
@@ -245,7 +260,7 @@ class OutlookApp(APIApplication):
245
260
  self,
246
261
  message_id: str,
247
262
  user_id: str | None = None,
248
- includeHiddenMessages: str | None = None,
263
+ includeHiddenMessages: bool | None = None,
249
264
  select: list[str] | None = None,
250
265
  expand: list[str] | None = None,
251
266
  ) -> Any:
@@ -253,9 +268,9 @@ class OutlookApp(APIApplication):
253
268
  Retrieves a specific email message by its ID for a given user, with options to select specific fields or expand related data. Unlike `list_user_messages`, which fetches a collection of emails with advanced filtering, this function is designed to retrieve a single, known message.
254
269
 
255
270
  Args:
256
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
257
271
  message_id (string): message-id
258
- includeHiddenMessages (string): Include Hidden Messages
272
+ user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
273
+ includeHiddenMessages (boolean): Include Hidden Messages
259
274
  select (array): Select properties to be returned
260
275
  expand (array): Expand related entities
261
276
 
@@ -273,18 +288,19 @@ class OutlookApp(APIApplication):
273
288
  user_info = self.get_current_user_profile()
274
289
  user_id = user_info.get("userPrincipalName")
275
290
  if not user_id:
276
- raise ValueError(
277
- "Could not retrieve user ID from get_current_user_profile response."
278
- )
291
+ raise ValueError("Could not retrieve user ID from get_current_user_profile response.")
279
292
  if message_id is None:
280
293
  raise ValueError("Missing required parameter 'message-id'.")
281
294
  url = f"{self.base_url}/users/{user_id}/messages/{message_id}"
295
+ select_str = ",".join(select) if select else None
296
+ expand_str = ",".join(expand) if expand else None
297
+
282
298
  query_params = {
283
299
  k: v
284
300
  for k, v in [
285
301
  ("includeHiddenMessages", includeHiddenMessages),
286
- ("$select", select),
287
- ("$expand", expand),
302
+ ("$select", select_str),
303
+ ("$expand", expand_str),
288
304
  ]
289
305
  if v is not None
290
306
  }
@@ -296,8 +312,8 @@ class OutlookApp(APIApplication):
296
312
  Permanently deletes a specific email, identified by `message_id`, from a user's mailbox. If `user_id` is not provided, it defaults to the current authenticated user. Unlike retrieval functions such as `get_user_message`, this performs a destructive action to remove the specified email from Outlook.
297
313
 
298
314
  Args:
299
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
300
315
  message_id (string): message-id
316
+ user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
301
317
 
302
318
  Returns:
303
319
  Any: Success
@@ -313,9 +329,7 @@ class OutlookApp(APIApplication):
313
329
  user_info = self.get_current_user_profile()
314
330
  user_id = user_info.get("userPrincipalName")
315
331
  if not user_id:
316
- raise ValueError(
317
- "Could not retrieve user ID from get_current_user_profile response."
318
- )
332
+ raise ValueError("Could not retrieve user ID from get_current_user_profile response.")
319
333
  if message_id is None:
320
334
  raise ValueError("Missing required parameter 'message-id'.")
321
335
  url = f"{self.base_url}/users/{user_id}/messages/{message_id}"
@@ -339,15 +353,20 @@ class OutlookApp(APIApplication):
339
353
  """
340
354
  Retrieves attachments for a specific email message, identified by its ID. Supports advanced querying for filtering, sorting, and pagination, allowing users to select specific fields to return in the result set, focusing only on attachments rather than the full message content.
341
355
 
356
+ IMPORTANT LIMITATIONS (Microsoft Graph API restrictions):
357
+ - `search` cannot be used with `filter`
358
+ - `search` cannot be used with `orderby`
359
+ - `search` cannot be used with `skip` (use pagination via @odata.nextLink and get_next_page instead)
360
+
342
361
  Args:
343
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
344
362
  message_id (string): message-id
363
+ user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
345
364
  top (integer): Show only the first n items Example: '50'.
346
- skip (integer): Skip the first n items
347
- search (string): Search items by search phrases
348
- filter (string): Filter items by property values
365
+ skip (integer): Skip the first n items. Cannot be used with 'search'.
366
+ search (string): Search items by search phrases. Cannot be used with 'filter', 'orderby', or 'skip'.
367
+ filter (string): Filter items by property values. Cannot be used with 'search'.
349
368
  count (boolean): Include count of items
350
- orderby (array): Order items by property values
369
+ orderby (array): Order items by property values. Cannot be used with 'search'.
351
370
  select (array): Select properties to be returned
352
371
  expand (array): Expand related entities
353
372
 
@@ -355,22 +374,43 @@ class OutlookApp(APIApplication):
355
374
  dict[str, Any]: Retrieved collection
356
375
 
357
376
  Raises:
377
+ ValueError: If incompatible parameters are used together (search with filter/orderby/skip).
358
378
  HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
359
379
 
360
380
  Tags:
361
381
  users.message, important
362
382
  """
383
+ if search:
384
+ if filter:
385
+ raise ValueError(
386
+ "The 'search' parameter cannot be used together with 'filter'. "
387
+ "This is a Microsoft Graph API restriction. Please use either search or filter, not both."
388
+ )
389
+ if orderby:
390
+ raise ValueError(
391
+ "The 'search' parameter cannot be used together with 'orderby'. "
392
+ "This is a Microsoft Graph API restriction. When using search, results are sorted by relevance."
393
+ )
394
+ if skip:
395
+ raise ValueError(
396
+ "The 'search' parameter cannot be used together with 'skip'. "
397
+ "When using search, use pagination via @odata.nextLink and the get_next_page function instead."
398
+ )
399
+
363
400
  # If user_id is not provided, get it automatically
364
401
  if user_id is None:
365
402
  user_info = self.get_current_user_profile()
366
403
  user_id = user_info.get("userPrincipalName")
367
404
  if not user_id:
368
- raise ValueError(
369
- "Could not retrieve user ID from get_current_user_profile response."
370
- )
405
+ raise ValueError("Could not retrieve user ID from get_current_user_profile response.")
371
406
  if message_id is None:
372
407
  raise ValueError("Missing required parameter 'message-id'.")
408
+
373
409
  url = f"{self.base_url}/users/{user_id}/messages/{message_id}/attachments"
410
+ orderby_str = ",".join(orderby) if orderby else None
411
+ select_str = ",".join(select) if select else None
412
+ expand_str = ",".join(expand) if expand else None
413
+
374
414
  query_params = {
375
415
  k: v
376
416
  for k, v in [
@@ -379,9 +419,9 @@ class OutlookApp(APIApplication):
379
419
  ("$search", search),
380
420
  ("$filter", filter),
381
421
  ("$count", count),
382
- ("$orderby", orderby),
383
- ("$select", select),
384
- ("$expand", expand),
422
+ ("$orderby", orderby_str),
423
+ ("$select", select_str),
424
+ ("$expand", expand_str),
385
425
  ]
386
426
  if v is not None
387
427
  }
@@ -418,9 +458,7 @@ class OutlookApp(APIApplication):
418
458
  if not url:
419
459
  raise ValueError("Missing required parameter 'url'.")
420
460
  if not url.startswith(self.base_url):
421
- raise ValueError(
422
- f"The provided URL '{url}' does not start with the expected base URL '{self.base_url}'."
423
- )
461
+ raise ValueError(f"The provided URL '{url}' does not start with the expected base URL '{self.base_url}'.")
424
462
  relative_part = url[len(self.base_url) :]
425
463
  parsed_relative = urlparse(relative_part)
426
464
  path_only = parsed_relative.path
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp-applications
3
- Version: 0.1.23
3
+ Version: 0.1.25
4
4
  Summary: A Universal MCP Application: universal_mcp_applications
5
5
  Project-URL: Homepage, https://github.com/universal-mcp/applications
6
6
  Project-URL: Repository, https://github.com/universal-mcp/applications
@@ -25,7 +25,7 @@ universal_mcp/applications/braze/__init__.py,sha256=9HSB9jg_Ws55dLcTh2qEtLNHMFnR
25
25
  universal_mcp/applications/braze/app.py,sha256=dJSivDLvc002IPdusKNvLWqIz035j3bRQw6FFXve2Ns,175855
26
26
  universal_mcp/applications/browser_use/README.md,sha256=xLlNi7R94GfykFnUrxR9WN4G4cyG-VxHKEeTnGFdk10,13
27
27
  universal_mcp/applications/browser_use/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- universal_mcp/applications/browser_use/app.py,sha256=Md_k4bau_lpfkYRzNEU07sXo2OInhqA8mGQypLQVKyA,2581
28
+ universal_mcp/applications/browser_use/app.py,sha256=n4K598QcQTF2e8ukme8kXh63lS8mIwNKqTZS1AVb554,2587
29
29
  universal_mcp/applications/cal_com_v2/README.md,sha256=V7M5cx_Rhbj54mCj4LudO1g61UYlA5Ur65wWG1d6n2U,21047
30
30
  universal_mcp/applications/cal_com_v2/__init__.py,sha256=OOTUXThnSL6bsOfTs4B0h-zKDo6b3LxL98JQlpZifCE,29
31
31
  universal_mcp/applications/cal_com_v2/app.py,sha256=d38htmRGXJ_mNJdMbGQIwvkyAV0t8uiacvaKt7ymD_k,220974
@@ -105,7 +105,7 @@ universal_mcp/applications/google_calendar/__init__.py,sha256=qxVxf_Q5lOdxXRHzmE
105
105
  universal_mcp/applications/google_calendar/app.py,sha256=FZptXBLsRo4Rp2kRrVJO_dM3Wr8G0XyMXLHWfPya80Q,25884
106
106
  universal_mcp/applications/google_docs/README.md,sha256=KDy_X4SRELegE5sEdixAP0YeXZOXdADTX2D-tAUlCJM,4512
107
107
  universal_mcp/applications/google_docs/__init__.py,sha256=U0pWagxnj0VD-AcKNd8eS0orzaMmlUOgvW9vkYBNH40,31
108
- universal_mcp/applications/google_docs/app.py,sha256=lPmvTzLvc-o90EKQ6kynY84O-HEZiZEScyWIyexTnxY,34129
108
+ universal_mcp/applications/google_docs/app.py,sha256=V3Ys3D6Al-fYb_8CRj5I5BqcZ0V5ovtHN3eiOHPf07Y,39737
109
109
  universal_mcp/applications/google_drive/README.md,sha256=Kmg7LLaDW-7bnsgdVimwxc5SdUf2uA9Fv8zIMXVa-Uc,15393
110
110
  universal_mcp/applications/google_drive/__init__.py,sha256=DTyed4ADcCmALSyPT8whjXoosPXl3m-i8JrilPJ3ijU,32
111
111
  universal_mcp/applications/google_drive/app.py,sha256=J81m8OBjE0552GGWsIfgM4idFjjZfPEOsjk0ZVeJzgM,257259
@@ -170,7 +170,7 @@ universal_mcp/applications/openai/__init__.py,sha256=7h2xDZdb1pRh7ZDLtFK9BNDEbRH
170
170
  universal_mcp/applications/openai/app.py,sha256=5pW85lC5SsojpQNPTZO4QkGkDAcLk8M3nvl1m1Bi0Vk,34123
171
171
  universal_mcp/applications/outlook/README.md,sha256=gEdQckOduZHCjLYACZtN3ApFC41y1YdcT0uFCiNvuvk,2830
172
172
  universal_mcp/applications/outlook/__init__.py,sha256=yExFpo0apWqcpP7l4qCjAYwMyzomUaEeNDhY7Gi-We0,28
173
- universal_mcp/applications/outlook/app.py,sha256=pGQRkF_vfWB6yFUVgisFfnQ_q_XpHPSDPQlvCqQd2JU,19560
173
+ universal_mcp/applications/outlook/app.py,sha256=RBlncTgS9eLrb1SAY7Rw7g0AYnWSqHsfthxEyWgNk4E,22470
174
174
  universal_mcp/applications/perplexity/README.md,sha256=3DTRkkaGKmAyZz3Rpdo5OUyEXogWqzZ228gxrixO2pI,554
175
175
  universal_mcp/applications/perplexity/__init__.py,sha256=f4uz8qlw-uek6AlyWxoWxukubaHb6YV4riTcvQhfvO0,31
176
176
  universal_mcp/applications/perplexity/app.py,sha256=M47xUTRJbnd7zDxVG1R3nkX46aI7GZ-aVPphNQjMkvA,2815
@@ -276,7 +276,7 @@ universal_mcp/applications/youtube/app.py,sha256=eqgqe0b53W9Mj0FZGW3ZqY3xkGF4NbO
276
276
  universal_mcp/applications/zenquotes/README.md,sha256=FJyoTGRCaZjF_bsCBqg1CrYcvIfuUG_Qk616G1wjhF8,512
277
277
  universal_mcp/applications/zenquotes/__init__.py,sha256=C5nEHZ3Xy6nYUarq0BqQbbJnHs0UtSlqhk0DqmvWiHk,58
278
278
  universal_mcp/applications/zenquotes/app.py,sha256=7xIEnSZWAGYu5583Be2ZjSCtLUAfMWRzucSpp7hw_h4,1299
279
- universal_mcp_applications-0.1.23.dist-info/METADATA,sha256=t6rJqUsQ7zqQC2KzL5Zsuvr9besu7eToU2zuT0u7Z4o,2956
280
- universal_mcp_applications-0.1.23.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
281
- universal_mcp_applications-0.1.23.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
282
- universal_mcp_applications-0.1.23.dist-info/RECORD,,
279
+ universal_mcp_applications-0.1.25.dist-info/METADATA,sha256=yI7SU0Crz1RSAHM8JMGShgf-SJqMYzMwE4AQFm9rk4c,2956
280
+ universal_mcp_applications-0.1.25.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
281
+ universal_mcp_applications-0.1.25.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
282
+ universal_mcp_applications-0.1.25.dist-info/RECORD,,