universal-mcp-applications 0.1.24__py3-none-any.whl → 0.1.26__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.

Potentially problematic release.


This version of universal-mcp-applications might be problematic. Click here for more details.

@@ -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]:
@@ -10,163 +10,189 @@ class OutlookApp(APIApplication):
10
10
  super().__init__(name="outlook", integration=integration, **kwargs)
11
11
  self.base_url = "https://graph.microsoft.com/v1.0"
12
12
 
13
- def reply_to_message(
13
+ def reply_to_email(
14
14
  self,
15
15
  message_id: str,
16
+ comment: str,
16
17
  user_id: str | None = None,
17
- comment: str | None = None,
18
- message: dict[str, Any] | None = None,
19
- ) -> Any:
18
+ attachments: list[dict[str, Any]] | None = None,
19
+ ) -> dict[str, Any]:
20
20
  """
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.
21
+ Replies to a specific email message.
22
22
 
23
23
  Args:
24
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
25
- message_id (string): message-id
26
- comment (string): A comment to include in the reply. Example: 'Thank you for your email. Here is my reply.'.
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'}]}.
24
+ message_id (str): The ID of the email message to reply to.
25
+ comment (str): The body of the reply.
26
+ user_id (str, optional): The ID of the user to send the reply from. Defaults to the authenticated user.
27
+ attachments (list[dict[str, Any]], optional): A list of attachment objects to include in the reply.
28
+ Each attachment dictionary should conform to the Microsoft Graph API specification.
29
+ Example:
30
+ [
31
+ {
32
+ "@odata.type": "#microsoft.graph.fileAttachment",
33
+ "name": "attachment.txt",
34
+ "contentType": "text/plain",
35
+ "contentBytes": "SGVsbG8gV29ybGQh"
36
+ }
37
+ ]
28
38
 
29
39
  Returns:
30
- Any: Success
40
+ dict[str, Any]: A dictionary confirming the reply action.
31
41
 
32
42
  Raises:
33
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
43
+ HTTPStatusError: If the API request fails.
44
+ ValueError: If the user_id cannot be retrieved or message_id is missing.
34
45
 
35
46
  Tags:
36
- users.message, important
47
+ important
37
48
  """
38
- # If user_id is not provided, get it automatically
39
49
  if user_id is None:
40
- user_info = self.get_current_user_profile()
50
+ user_info = self.get_my_profile()
41
51
  user_id = user_info.get("userPrincipalName")
42
52
  if not user_id:
43
- raise ValueError(
44
- "Could not retrieve user ID from get_current_user_profile response."
45
- )
46
- if message_id is None:
47
- raise ValueError("Missing required parameter 'message-id'.")
48
- request_body_data = None
49
- request_body_data = {
50
- "comment": comment,
51
- "message": message,
52
- }
53
- request_body_data = {
54
- k: v for k, v in request_body_data.items() if v is not None
55
- }
53
+ raise ValueError("Could not retrieve user ID from get_my_profile response.")
54
+ if not message_id:
55
+ raise ValueError("Missing required parameter 'message_id'.")
56
+
57
+ request_body_data = {"comment": comment}
58
+ if attachments:
59
+ request_body_data["message"] = {"attachments": attachments}
60
+
56
61
  url = f"{self.base_url}/users/{user_id}/messages/{message_id}/reply"
57
- query_params = {}
62
+
58
63
  response = self._post(
59
64
  url,
60
65
  data=request_body_data,
61
- params=query_params,
66
+ params={},
62
67
  content_type="application/json",
63
68
  )
64
69
  return self._handle_response(response)
65
70
 
66
- def send_mail(
71
+ def send_email(
67
72
  self,
68
- message: dict[str, Any],
73
+ subject: str,
74
+ body: str,
75
+ to_recipients: list[str],
69
76
  user_id: str | None = None,
70
- saveToSentItems: bool | None = None,
71
- ) -> Any:
77
+ cc_recipients: list[str] | None = None,
78
+ bcc_recipients: list[str] | None = None,
79
+ attachments: list[dict[str, Any]] | None = None,
80
+ body_content_type: str = "Text",
81
+ save_to_sent_items: bool = True,
82
+ ) -> dict[str, Any]:
72
83
  """
73
- 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.
84
+ Sends a new email.
74
85
 
75
86
  Args:
76
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
77
- 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'}]}.
78
- saveToSentItems (boolean): saveToSentItems Example: 'False'.
87
+ subject (str): The subject of the email.
88
+ body (str): The body of the email.
89
+ to_recipients (list[str]): A list of email addresses for the 'To' recipients.
90
+ user_id (str, optional): The ID of the user to send the email from. Defaults to the authenticated user.
91
+ cc_recipients (list[str], optional): A list of email addresses for the 'Cc' recipients.
92
+ bcc_recipients (list[str], optional): A list of email addresses for the 'Bcc' recipients.
93
+ attachments (list[dict[str, Any]], optional): A list of attachment objects. See `reply_to_email` for an example.
94
+ body_content_type (str, optional): The content type of the email body, e.g., "Text" or "HTML". Defaults to "Text".
95
+ save_to_sent_items (bool, optional): Whether to save the email to the 'Sent Items' folder. Defaults to True.
79
96
 
80
97
  Returns:
81
- Any: Success
98
+ dict[str, Any]: A dictionary confirming the send action.
82
99
 
83
100
  Raises:
84
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
101
+ HTTPStatusError: If the API request fails.
102
+ ValueError: If the user_id cannot be retrieved.
85
103
 
86
104
  Tags:
87
- users.user.Actions, important
105
+ important
88
106
  """
89
- # If user_id is not provided, get it automatically
90
107
  if user_id is None:
91
- user_info = self.get_current_user_profile()
108
+ user_info = self.get_my_profile()
92
109
  user_id = user_info.get("userPrincipalName")
93
110
  if not user_id:
94
- raise ValueError(
95
- "Could not retrieve user ID from get_current_user_profile response."
96
- )
97
- request_body_data = None
98
- request_body_data = {
99
- "message": message,
100
- "saveToSentItems": saveToSentItems,
111
+ raise ValueError("Could not retrieve user ID from get_my_profile response.")
112
+
113
+ message = {
114
+ "subject": subject,
115
+ "body": {"contentType": body_content_type, "content": body},
116
+ "toRecipients": [{"emailAddress": {"address": email}} for email in to_recipients],
101
117
  }
118
+ if cc_recipients:
119
+ message["ccRecipients"] = [{"emailAddress": {"address": email}} for email in cc_recipients]
120
+ if bcc_recipients:
121
+ message["bccRecipients"] = [{"emailAddress": {"address": email}} for email in bcc_recipients]
122
+ if attachments:
123
+ message["attachments"] = attachments
124
+
102
125
  request_body_data = {
103
- k: v for k, v in request_body_data.items() if v is not None
126
+ "message": message,
127
+ "saveToSentItems": save_to_sent_items,
104
128
  }
129
+
105
130
  url = f"{self.base_url}/users/{user_id}/sendMail"
106
- query_params = {}
131
+
107
132
  response = self._post(
108
133
  url,
109
134
  data=request_body_data,
110
- params=query_params,
135
+ params={},
111
136
  content_type="application/json",
112
137
  )
113
138
  return self._handle_response(response)
114
139
 
115
- def get_mail_folder(
140
+ def get_email_folder(
116
141
  self,
117
- mailFolder_id: str,
142
+ folder_id: str,
118
143
  user_id: str | None = None,
119
- includeHiddenFolders: str | None = None,
144
+ include_hidden: bool | None = None,
120
145
  select: list[str] | None = None,
121
146
  expand: list[str] | None = None,
122
- ) -> Any:
147
+ ) -> dict[str, Any]:
123
148
  """
124
- 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.
149
+ Retrieves a specific email folder's metadata by its ID.
125
150
 
126
151
  Args:
127
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
128
- mailFolder_id (string): mailFolder-id
129
- includeHiddenFolders (string): Include Hidden Folders
130
- select (array): Select properties to be returned
131
- expand (array): Expand related entities
152
+ folder_id (str): The unique identifier for the mail folder.
153
+ user_id (str, optional): The ID of the user who owns the folder. Defaults to the authenticated user.
154
+ include_hidden (bool, optional): If true, includes hidden folders in the results.
155
+ select (list[str], optional): A list of properties to return.
156
+ expand (list[str], optional): A list of related entities to expand.
132
157
 
133
158
  Returns:
134
- Any: Retrieved navigation property
159
+ dict[str, Any]: A dictionary containing the mail folder's metadata.
135
160
 
136
161
  Raises:
137
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
138
-
162
+ HTTPStatusError: If the API request fails.
163
+ ValueError: If user_id cannot be retrieved or folder_id is missing.
139
164
  Tags:
140
- users.mailFolder, important
165
+ important
141
166
  """
142
- # If user_id is not provided, get it automatically
143
167
  if user_id is None:
144
- user_info = self.get_current_user_profile()
168
+ user_info = self.get_my_profile()
145
169
  user_id = user_info.get("userPrincipalName")
146
170
  if not user_id:
147
- raise ValueError(
148
- "Could not retrieve user ID from get_current_user_profile response."
149
- )
150
- if mailFolder_id is None:
151
- raise ValueError("Missing required parameter 'mailFolder-id'.")
152
- url = f"{self.base_url}/users/{user_id}/mailFolders/{mailFolder_id}"
171
+ raise ValueError("Could not retrieve user ID from get_my_profile response.")
172
+ if not folder_id:
173
+ raise ValueError("Missing required parameter 'folder_id'.")
174
+
175
+ url = f"{self.base_url}/users/{user_id}/mailFolders/{folder_id}"
176
+ select_str = ",".join(select) if select else None
177
+ expand_str = ",".join(expand) if expand else None
178
+
153
179
  query_params = {
154
180
  k: v
155
181
  for k, v in [
156
- ("includeHiddenFolders", includeHiddenFolders),
157
- ("$select", select),
158
- ("$expand", expand),
182
+ ("includeHiddenFolders", include_hidden),
183
+ ("$select", select_str),
184
+ ("$expand", expand_str),
159
185
  ]
160
186
  if v is not None
161
187
  }
162
188
  response = self._get(url, params=query_params)
163
189
  return self._handle_response(response)
164
190
 
165
- def list_user_messages(
191
+ def list_emails(
166
192
  self,
167
193
  user_id: str | None = None,
168
194
  select: list[str] = ["bodyPreview"],
169
- includeHiddenMessages: bool | None = None,
195
+ include_hidden: bool | None = None,
170
196
  top: int | None = None,
171
197
  skip: int | None = None,
172
198
  search: str | None = None,
@@ -176,48 +202,44 @@ class OutlookApp(APIApplication):
176
202
  expand: list[str] | None = None,
177
203
  ) -> dict[str, Any]:
178
204
  """
179
- 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.
205
+ Retrieves a list of emails from a user's mailbox.
180
206
 
181
207
  Args:
182
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
183
- select (list): Select properties to be returned. Defaults to ['bodyPreview'].
184
- Example: [
185
- 'id', 'categories', 'receivedDateTime', 'sentDateTime', 'hasAttachments', 'internetMessageId',
186
- 'subject', 'body', 'bodyPreview', 'importance', 'parentFolderId', 'conversationId',
187
- 'conversationIndex', 'isDeliveryReceiptRequested', 'isReadReceiptRequested', 'isRead', 'isDraft',
188
- 'webLink', 'inferenceClassification', 'sender', 'from', 'toRecipients', 'ccRecipients',
189
- 'bccRecipients', 'replyTo', 'flag', 'attachments', 'extensions', 'mentions', 'uniqueBody'
190
- ]
191
- includeHiddenMessages (boolean): Include Hidden Messages
192
- 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
196
- count (boolean): Include count of items
197
- orderby (array): Order items by property values
198
- expand (array): Expand related entities
208
+ user_id (str, optional): The ID of the user. Defaults to the authenticated user.
209
+ select (list[str], optional): A list of properties to return for each email. Defaults to ['bodyPreview'].
210
+ include_hidden (bool, optional): If true, includes hidden messages.
211
+ top (int, optional): The maximum number of emails to return.
212
+ skip (int, optional): The number of emails to skip. Cannot be used with 'search'.
213
+ search (str, optional): A search query. Cannot be used with 'filter', 'orderby', or 'skip'.
214
+ filter (str, optional): A filter query. Cannot be used with 'search'.
215
+ count (bool, optional): If true, includes the total count of emails in the response.
216
+ orderby (list[str], optional): A list of properties to sort the results by. Cannot be used with 'search'.
217
+ expand (list[str], optional): A list of related entities to expand.
199
218
 
200
219
  Returns:
201
- dict[str, Any]: Retrieved collection
220
+ dict[str, Any]: A dictionary containing a list of emails and pagination information.
202
221
 
203
222
  Raises:
204
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
205
-
223
+ ValueError: If incompatible parameters are used together (e.g., 'search' with 'filter').
224
+ HTTPStatusError: If the API request fails.
206
225
  Tags:
207
- users.message, important
226
+ important
208
227
  """
209
- # If user_id is not provided, get it automatically
228
+ if search:
229
+ if filter:
230
+ raise ValueError("The 'search' parameter cannot be used with 'filter'.")
231
+ if orderby:
232
+ raise ValueError("The 'search' parameter cannot be used with 'orderby'.")
233
+ if skip:
234
+ raise ValueError("The 'search' parameter cannot be used with 'skip'. Use pagination via @odata.nextLink instead.")
235
+
210
236
  if user_id is None:
211
- user_info = self.get_current_user_profile()
237
+ user_info = self.get_my_profile()
212
238
  user_id = user_info.get("userPrincipalName")
213
239
  if not user_id:
214
- raise ValueError(
215
- "Could not retrieve user ID from get_current_user_profile response."
216
- )
240
+ raise ValueError("Could not retrieve user ID from get_my_profile response.")
217
241
 
218
242
  url = f"{self.base_url}/users/{user_id}/messages"
219
-
220
- # Handle list parameters by joining with commas
221
243
  select_str = ",".join(select) if select else None
222
244
  orderby_str = ",".join(orderby) if orderby else None
223
245
  expand_str = ",".join(expand) if expand else None
@@ -225,7 +247,7 @@ class OutlookApp(APIApplication):
225
247
  query_params = {
226
248
  k: v
227
249
  for k, v in [
228
- ("includeHiddenMessages", includeHiddenMessages),
250
+ ("includeHiddenMessages", include_hidden),
229
251
  ("$top", top),
230
252
  ("$skip", skip),
231
253
  ("$search", search),
@@ -241,89 +263,87 @@ class OutlookApp(APIApplication):
241
263
  response = self._get(url, params=query_params)
242
264
  return self._handle_response(response)
243
265
 
244
- def get_user_message(
266
+ def get_email(
245
267
  self,
246
268
  message_id: str,
247
269
  user_id: str | None = None,
248
- includeHiddenMessages: str | None = None,
270
+ include_hidden: bool | None = None,
249
271
  select: list[str] | None = None,
250
272
  expand: list[str] | None = None,
251
- ) -> Any:
273
+ ) -> dict[str, Any]:
252
274
  """
253
- 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.
275
+ Retrieves a specific email by its ID.
254
276
 
255
277
  Args:
256
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
257
- message_id (string): message-id
258
- includeHiddenMessages (string): Include Hidden Messages
259
- select (array): Select properties to be returned
260
- expand (array): Expand related entities
278
+ message_id (str): The unique identifier for the email.
279
+ user_id (str, optional): The ID of the user who owns the email. Defaults to the authenticated user.
280
+ include_hidden (bool, optional): If true, includes hidden messages.
281
+ select (list[str], optional): A list of properties to return.
282
+ expand (list[str], optional): A list of related entities to expand.
261
283
 
262
284
  Returns:
263
- Any: Retrieved navigation property
285
+ dict[str, Any]: A dictionary containing the email's details.
264
286
 
265
287
  Raises:
266
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
267
-
288
+ HTTPStatusError: If the API request fails.
289
+ ValueError: If user_id cannot be retrieved or message_id is missing.
268
290
  Tags:
269
- users.message, important
291
+ important
270
292
  """
271
- # If user_id is not provided, get it automatically
272
293
  if user_id is None:
273
- user_info = self.get_current_user_profile()
294
+ user_info = self.get_my_profile()
274
295
  user_id = user_info.get("userPrincipalName")
275
296
  if not user_id:
276
- raise ValueError(
277
- "Could not retrieve user ID from get_current_user_profile response."
278
- )
279
- if message_id is None:
280
- raise ValueError("Missing required parameter 'message-id'.")
297
+ raise ValueError("Could not retrieve user ID from get_my_profile response.")
298
+ if not message_id:
299
+ raise ValueError("Missing required parameter 'message_id'.")
300
+
281
301
  url = f"{self.base_url}/users/{user_id}/messages/{message_id}"
302
+ select_str = ",".join(select) if select else None
303
+ expand_str = ",".join(expand) if expand else None
304
+
282
305
  query_params = {
283
306
  k: v
284
307
  for k, v in [
285
- ("includeHiddenMessages", includeHiddenMessages),
286
- ("$select", select),
287
- ("$expand", expand),
308
+ ("includeHiddenMessages", include_hidden),
309
+ ("$select", select_str),
310
+ ("$expand", expand_str),
288
311
  ]
289
312
  if v is not None
290
313
  }
291
314
  response = self._get(url, params=query_params)
292
315
  return self._handle_response(response)
293
316
 
294
- def user_delete_message(self, message_id: str, user_id: str | None = None) -> Any:
317
+ def delete_email(self, message_id: str, user_id: str | None = None) -> dict[str, Any]:
295
318
  """
296
- 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.
319
+ Permanently deletes a specific email by its ID.
297
320
 
298
321
  Args:
299
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
300
- message_id (string): message-id
322
+ message_id (str): The unique identifier for the email to be deleted.
323
+ user_id (str, optional): The ID of the user who owns the email. Defaults to the authenticated user.
301
324
 
302
325
  Returns:
303
- Any: Success
326
+ dict[str, Any]: A dictionary confirming the deletion.
304
327
 
305
328
  Raises:
306
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
307
-
329
+ HTTPStatusError: If the API request fails.
330
+ ValueError: If user_id cannot be retrieved or message_id is missing.
308
331
  Tags:
309
- users.message, important
332
+ important
310
333
  """
311
- # If user_id is not provided, get it automatically
312
334
  if user_id is None:
313
- user_info = self.get_current_user_profile()
335
+ user_info = self.get_my_profile()
314
336
  user_id = user_info.get("userPrincipalName")
315
337
  if not user_id:
316
- raise ValueError(
317
- "Could not retrieve user ID from get_current_user_profile response."
318
- )
319
- if message_id is None:
320
- raise ValueError("Missing required parameter 'message-id'.")
338
+ raise ValueError("Could not retrieve user ID from get_my_profile response.")
339
+ if not message_id:
340
+ raise ValueError("Missing required parameter 'message_id'.")
341
+
321
342
  url = f"{self.base_url}/users/{user_id}/messages/{message_id}"
322
- query_params = {}
323
- response = self._delete(url, params=query_params)
343
+ response = self._delete(url, params={})
324
344
  return self._handle_response(response)
325
345
 
326
- def list_message_attachments(
346
+ def list_email_attachments(
327
347
  self,
328
348
  message_id: str,
329
349
  user_id: str | None = None,
@@ -337,40 +357,50 @@ class OutlookApp(APIApplication):
337
357
  expand: list[str] | None = None,
338
358
  ) -> dict[str, Any]:
339
359
  """
340
- 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.
360
+ Retrieves attachments for a specific email.
341
361
 
342
362
  Args:
343
- user_id (string, optional): user-id. If not provided, will automatically get the current user's ID.
344
- message_id (string): message-id
345
- 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
349
- count (boolean): Include count of items
350
- orderby (array): Order items by property values
351
- select (array): Select properties to be returned
352
- expand (array): Expand related entities
363
+ message_id (str): The unique identifier for the email.
364
+ user_id (str, optional): The ID of the user who owns the email. Defaults to the authenticated user.
365
+ top (int, optional): The maximum number of attachments to return.
366
+ skip (int, optional): The number of attachments to skip. Cannot be used with 'search'.
367
+ search (str, optional): A search query. Cannot be used with 'filter', 'orderby', or 'skip'.
368
+ filter (str, optional): A filter query. Cannot be used with 'search'.
369
+ count (bool, optional): If true, includes the total count of attachments.
370
+ orderby (list[str], optional): A list of properties to sort by. Cannot be used with 'search'.
371
+ select (list[str], optional): A list of properties to return.
372
+ expand (list[str], optional): A list of related entities to expand.
353
373
 
354
374
  Returns:
355
- dict[str, Any]: Retrieved collection
375
+ dict[str, Any]: A dictionary containing a list of attachments.
356
376
 
357
377
  Raises:
358
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
359
-
378
+ ValueError: If incompatible parameters are used together.
379
+ HTTPStatusError: If the API request fails.
360
380
  Tags:
361
- users.message, important
381
+ important
362
382
  """
363
- # If user_id is not provided, get it automatically
383
+ if search:
384
+ if filter:
385
+ raise ValueError("The 'search' parameter cannot be used with 'filter'.")
386
+ if orderby:
387
+ raise ValueError("The 'search' parameter cannot be used with 'orderby'.")
388
+ if skip:
389
+ raise ValueError("The 'search' parameter cannot be used with 'skip'. Use pagination via @odata.nextLink instead.")
390
+
364
391
  if user_id is None:
365
- user_info = self.get_current_user_profile()
392
+ user_info = self.get_my_profile()
366
393
  user_id = user_info.get("userPrincipalName")
367
394
  if not user_id:
368
- raise ValueError(
369
- "Could not retrieve user ID from get_current_user_profile response."
370
- )
371
- if message_id is None:
372
- raise ValueError("Missing required parameter 'message-id'.")
395
+ raise ValueError("Could not retrieve user ID from get_my_profile response.")
396
+ if not message_id:
397
+ raise ValueError("Missing required parameter 'message_id'.")
398
+
373
399
  url = f"{self.base_url}/users/{user_id}/messages/{message_id}/attachments"
400
+ orderby_str = ",".join(orderby) if orderby else None
401
+ select_str = ",".join(select) if select else None
402
+ expand_str = ",".join(expand) if expand else None
403
+
374
404
  query_params = {
375
405
  k: v
376
406
  for k, v in [
@@ -379,64 +409,116 @@ class OutlookApp(APIApplication):
379
409
  ("$search", search),
380
410
  ("$filter", filter),
381
411
  ("$count", count),
382
- ("$orderby", orderby),
383
- ("$select", select),
384
- ("$expand", expand),
412
+ ("$orderby", orderby_str),
413
+ ("$select", select_str),
414
+ ("$expand", expand_str),
385
415
  ]
386
416
  if v is not None
387
417
  }
388
418
  response = self._get(url, params=query_params)
389
419
  return self._handle_response(response)
390
420
 
391
- def get_current_user_profile(
421
+ def get_attachment(
392
422
  self,
423
+ message_id: str,
424
+ attachment_id: str,
425
+ user_id: str | None = None,
393
426
  ) -> dict[str, Any]:
394
427
  """
395
- Fetches the `userPrincipalName` for the currently authenticated user from the `/me` endpoint. This internal helper is used by other methods to automatically obtain the user's ID for API calls when a `user_id` is not explicitly provided.
428
+ Retrieves a specific attachment from an email message and formats it as a dictionary.
396
429
 
430
+ Args:
431
+ message_id (str): The ID of the email message.
432
+ attachment_id (str): The ID of the attachment.
433
+ user_id (str, optional): The ID of the user. Defaults to the authenticated user.
397
434
 
398
435
  Returns:
399
- dict[str, Any]: Current user information
436
+ dict[str, Any]: A dictionary containing the attachment details:
437
+ - 'type' (str): The general type of the attachment (e.g., "image", "audio", "video", "file").
438
+ - 'data' (str): The base64 encoded content of the attachment.
439
+ - 'mime_type' (str): The MIME type of the attachment.
440
+ - 'file_name' (str): The name of the attachment file.
441
+ Tags:
442
+ important
443
+ """
444
+ if user_id is None:
445
+ user_info = self.get_my_profile()
446
+ user_id = user_info.get("userPrincipalName")
447
+ if not user_id:
448
+ raise ValueError("Could not retrieve user ID.")
449
+ if not message_id or not attachment_id:
450
+ raise ValueError("Missing required parameter 'message_id' or 'attachment_id'.")
400
451
 
401
- Raises:
402
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body.
452
+ url = f"{self.base_url}/users/{user_id}/messages/{message_id}/attachments/{attachment_id}"
403
453
 
404
- Tags:
405
- me, important
454
+ response = self._get(url, params={})
455
+ attachment_data = self._handle_response(response)
456
+
457
+ content_type = attachment_data.get("contentType", "application/octet-stream")
458
+ attachment_type = content_type.split("/")[0] if "/" in content_type else "file"
459
+ if attachment_type not in ["image", "audio", "video", "text"]:
460
+ attachment_type = "file"
461
+
462
+ return {
463
+ "type": attachment_type,
464
+ "data": attachment_data.get("contentBytes"),
465
+ "mime_type": content_type,
466
+ "file_name": attachment_data.get("name"),
467
+ }
468
+
469
+ def get_my_profile(self) -> dict[str, Any]:
470
+ """
471
+ Fetches the userPrincipalName for the currently authenticated user.
472
+
473
+ Returns:
474
+ dict[str, Any]: A dictionary containing the user's principal name.
475
+
476
+ Raises:
477
+ HTTPStatusError: If the API request fails.
406
478
  """
407
479
  url = f"{self.base_url}/me"
408
- query_params = {
409
- "$select": "userPrincipalName",
410
- }
480
+ query_params = {"$select": "userPrincipalName"}
411
481
  response = self._get(url, params=query_params)
412
482
  return self._handle_response(response)
413
483
 
414
- def get_next_page(self, url: str) -> dict[str, Any]:
484
+ def get_next_page_results(self, url: str) -> dict[str, Any]:
415
485
  """
416
- Executes a GET request for a full URL, typically the `@odata.nextLink` from a previous paginated API response. It simplifies retrieving subsequent pages of data from list functions by handling URL validation and parsing before fetching the results for the next page.
486
+ Retrieves the next page of results from a paginated API response.
487
+
488
+ Args:
489
+ url (str): The full URL for the next page of results (@odata.nextLink).
490
+
491
+ Returns:
492
+ dict[str, Any]: A dictionary containing the next page of results.
493
+
494
+ Raises:
495
+ ValueError: If the URL is missing or invalid.
496
+ Tags:
497
+ important
417
498
  """
418
499
  if not url:
419
500
  raise ValueError("Missing required parameter 'url'.")
420
501
  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
- )
502
+ raise ValueError(f"The provided URL must start with '{self.base_url}'.")
503
+
424
504
  relative_part = url[len(self.base_url) :]
425
505
  parsed_relative = urlparse(relative_part)
426
506
  path_only = parsed_relative.path
427
507
  params = {k: v[0] for k, v in parse_qs(parsed_relative.query).items()}
508
+
428
509
  response = self._get(path_only, params=params)
429
510
  return self._handle_response(response)
430
511
 
431
512
  def list_tools(self):
432
513
  return [
433
- self.reply_to_message,
434
- self.send_mail,
435
- self.get_mail_folder,
436
- self.list_user_messages,
437
- self.get_user_message,
438
- self.user_delete_message,
439
- self.list_message_attachments,
440
- self.get_current_user_profile,
441
- self.get_next_page,
514
+ self.reply_to_email,
515
+ self.send_email,
516
+ self.get_email_folder,
517
+ self.list_emails,
518
+ self.get_email,
519
+ self.delete_email,
520
+ self.list_email_attachments,
521
+ self.get_attachment,
522
+ self.get_my_profile,
523
+ self.get_next_page_results,
442
524
  ]
@@ -44,9 +44,7 @@ class RedditApp(APIApplication):
44
44
  "User-Agent": "agentr-reddit-app/0.1 by AgentR",
45
45
  }
46
46
 
47
- def get_subreddit_posts(
48
- self, subreddit: str, limit: int = 5, timeframe: str = "day"
49
- ) -> dict[str, Any]:
47
+ def get_subreddit_posts(self, subreddit: str, limit: int = 5, timeframe: str = "day") -> dict[str, Any]:
50
48
  """
51
49
  Fetches a specified number of top-rated posts from a particular subreddit, allowing results to be filtered by a specific timeframe (e.g., 'day', 'week'). This is a simplified version compared to `get_subreddit_top_posts`, which uses more complex pagination parameters instead of a direct time filter.
52
50
 
@@ -56,7 +54,7 @@ class RedditApp(APIApplication):
56
54
  timeframe: The time period for top posts. Valid options: 'hour', 'day', 'week', 'month', 'year', 'all' (default: 'day')
57
55
 
58
56
  Returns:
59
- A formatted string containing a numbered list of top posts, including titles, authors, scores, and URLs, or an error message if the request fails
57
+ A dictionary containing a list of top posts with their details, or an error message if the request fails.
60
58
 
61
59
  Raises:
62
60
  RequestException: When the HTTP request to the Reddit API fails
@@ -69,34 +67,29 @@ class RedditApp(APIApplication):
69
67
  if timeframe not in valid_timeframes:
70
68
  return f"Error: Invalid timeframe '{timeframe}'. Please use one of: {', '.join(valid_timeframes)}"
71
69
  if not 1 <= limit <= 100:
72
- return (
73
- f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
74
- )
70
+ return f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
75
71
  url = f"{self.base_api_url}/r/{subreddit}/top"
76
72
  params = {"limit": limit, "t": timeframe}
77
- logger.info(
78
- f"Requesting top {limit} posts from r/{subreddit} for timeframe '{timeframe}'"
79
- )
73
+ logger.info(f"Requesting top {limit} posts from r/{subreddit} for timeframe '{timeframe}'")
80
74
  response = self._get(url, params=params)
81
75
  return self._handle_response(response)
82
76
 
83
- def search_subreddits(
84
- self, query: str, limit: int = 5, sort: str = "relevance"
85
- ) -> str:
77
+ def search_subreddits(self, query: str, limit: int = 5, sort: str = "relevance") -> dict[str, Any]:
86
78
  """
87
- Searches for subreddits by name and description using a query string, with results sortable by relevance or activity. Unlike the broader `search_reddit` function, this method exclusively discovers subreddits, not posts, comments, or users.
79
+ Finds subreddits based on a query string, searching their names and descriptions.
80
+ Results can be sorted by relevance or activity. This function is for discovering communities and does not search for posts or users, unlike the more general `search_reddit` function.
88
81
 
89
82
  Args:
90
- query: The text to search for in subreddit names and descriptions
91
- limit: The maximum number of subreddits to return, between 1 and 100 (default: 5)
92
- sort: The order of results, either 'relevance' or 'activity' (default: 'relevance')
83
+ query: The search query for subreddit names and descriptions.
84
+ limit: The maximum number of subreddits to return (1-100, default is 5).
85
+ sort: The sorting order for results. Can be 'relevance' or 'activity' (default is 'relevance').
93
86
 
94
87
  Returns:
95
- A formatted string containing a list of matching subreddits with their names, subscriber counts, and descriptions, or an error message if the search fails or parameters are invalid
88
+ A dictionary containing a list of matching subreddits, including their names, subscriber counts, and descriptions. Returns an error message on failure.
96
89
 
97
90
  Raises:
98
- RequestException: When the HTTP request to Reddit's API fails
99
- JSONDecodeError: When the API response contains invalid JSON
91
+ RequestException: If the API request to Reddit fails.
92
+ JSONDecodeError: If the API response is not valid JSON.
100
93
 
101
94
  Tags:
102
95
  search, important, reddit, api, query, format, list, validation
@@ -105,18 +98,14 @@ class RedditApp(APIApplication):
105
98
  if sort not in valid_sorts:
106
99
  return f"Error: Invalid sort option '{sort}'. Please use one of: {', '.join(valid_sorts)}"
107
100
  if not 1 <= limit <= 100:
108
- return (
109
- f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
110
- )
101
+ return f"Error: Invalid limit '{limit}'. Please use a value between 1 and 100."
111
102
  url = f"{self.base_api_url}/subreddits/search"
112
103
  params = {
113
104
  "q": query,
114
105
  "limit": limit,
115
106
  "sort": sort,
116
107
  }
117
- logger.info(
118
- f"Searching for subreddits matching '{query}' (limit: {limit}, sort: {sort})"
119
- )
108
+ logger.info(f"Searching for subreddits matching '{query}' (limit: {limit}, sort: {sort})")
120
109
  response = self._get(url, params=params)
121
110
  return self._handle_response(response)
122
111
 
@@ -193,16 +182,10 @@ class RedditApp(APIApplication):
193
182
  logger.info(f"Submitting a new post to r/{subreddit}")
194
183
  response = self._post(url_api, data=data)
195
184
  response_json = response.json()
196
- if (
197
- response_json
198
- and "json" in response_json
199
- and "errors" in response_json["json"]
200
- ):
185
+ if response_json and "json" in response_json and "errors" in response_json["json"]:
201
186
  errors = response_json["json"]["errors"]
202
187
  if errors:
203
- error_message = ", ".join(
204
- [f"{code}: {message}" for code, message in errors]
205
- )
188
+ error_message = ", ".join([f"{code}: {message}" for code, message in errors])
206
189
  return f"Reddit API error: {error_message}"
207
190
  return response_json
208
191
 
@@ -317,7 +300,7 @@ class RedditApp(APIApplication):
317
300
  Retrieves the full profile information for the currently authenticated user by making a GET request to the `/api/v1/me` Reddit API endpoint. This differs from `get_user_profile`, which requires a username, and `get_current_user_karma`, which specifically fetches karma data.
318
301
 
319
302
  Returns:
320
- Any: API response data.
303
+ A dictionary containing the authenticated user's profile information.
321
304
 
322
305
  Tags:
323
306
  users
@@ -333,7 +316,7 @@ class RedditApp(APIApplication):
333
316
  Fetches the karma breakdown for the authenticated user from the Reddit API. This function specifically targets the `/api/v1/me/karma` endpoint, returning karma statistics per subreddit, which is more specific than `get_current_user_info` that retrieves general profile information.
334
317
 
335
318
  Returns:
336
- Any: API response data.
319
+ A dictionary containing the authenticated user's karma breakdown by subreddit.
337
320
 
338
321
  Tags:
339
322
  account
@@ -349,10 +332,10 @@ class RedditApp(APIApplication):
349
332
  Fetches a specific Reddit post's details and its complete comment tree using the post's unique ID. This function returns the entire discussion, including the original post and all associated comments, providing broader context than `get_comment_by_id` which only retrieves a single comment.
350
333
 
351
334
  Args:
352
- post_id (string): The Reddit post ID ( e.g. '1m734tx' for https://www.reddit.com/r/mcp/comments/1m734tx/comment/n4occ77/)
335
+ post_id (str): The Reddit post ID ( e.g. '1m734tx' for https://www.reddit.com/r/mcp/comments/1m734tx/comment/n4occ77/)
353
336
 
354
337
  Returns:
355
- Any: API response data containing post details and comments.
338
+ A dictionary containing the post details and its comment tree.
356
339
 
357
340
  Tags:
358
341
  listings, comments, posts, important
@@ -384,7 +367,7 @@ class RedditApp(APIApplication):
384
367
  sr_detail: Optional. Expand subreddit details.
385
368
 
386
369
  Returns:
387
- Any: API response data containing a list of controversial posts.
370
+ A dictionary containing a listing of controversial posts.
388
371
 
389
372
  Tags:
390
373
  listings, posts, controversial, read-only
@@ -429,7 +412,7 @@ class RedditApp(APIApplication):
429
412
  sr_detail: Optional. Expand subreddit details.
430
413
 
431
414
  Returns:
432
- Any: API response data containing a list of hot posts.
415
+ A dictionary containing a listing of hot posts.
433
416
 
434
417
  Tags:
435
418
  listings, posts, hot, read-only
@@ -473,7 +456,7 @@ class RedditApp(APIApplication):
473
456
  sr_detail: Optional. Expand subreddit details.
474
457
 
475
458
  Returns:
476
- Any: API response data containing a list of new posts.
459
+ A dictionary containing a listing of new posts.
477
460
 
478
461
  Tags:
479
462
  listings, posts, new, read-only
@@ -520,7 +503,7 @@ class RedditApp(APIApplication):
520
503
  sr_detail: Optional. Expand subreddit details.
521
504
 
522
505
  Returns:
523
- Any: API response data containing a list of hot posts from the subreddit.
506
+ A dictionary containing a listing of hot posts from the specified subreddit.
524
507
 
525
508
  Tags:
526
509
  listings, posts, subreddit, hot, read-only
@@ -568,7 +551,7 @@ class RedditApp(APIApplication):
568
551
  sr_detail: Optional. Expand subreddit details.
569
552
 
570
553
  Returns:
571
- Any: API response data containing a list of new posts from the subreddit.
554
+ A dictionary containing a listing of new posts from the specified subreddit.
572
555
 
573
556
  Tags:
574
557
  listings, posts, subreddit, new, read-only
@@ -615,7 +598,7 @@ class RedditApp(APIApplication):
615
598
  sr_detail: Optional. Expand subreddit details.
616
599
 
617
600
  Returns:
618
- Any: API response data containing a list of top posts from the subreddit.
601
+ A dictionary containing a listing of top posts from the specified subreddit.
619
602
 
620
603
  Tags:
621
604
  listings, posts, subreddit, top, read-only
@@ -660,7 +643,7 @@ class RedditApp(APIApplication):
660
643
  sr_detail: Optional. Expand subreddit details.
661
644
 
662
645
  Returns:
663
- Any: API response data containing a list of rising posts.
646
+ A dictionary containing a listing of rising posts.
664
647
 
665
648
  Tags:
666
649
  listings, posts, rising, read-only
@@ -703,7 +686,7 @@ class RedditApp(APIApplication):
703
686
  sr_detail: Optional. Expand subreddit details.
704
687
 
705
688
  Returns:
706
- Any: API response data containing a list of top posts.
689
+ A dictionary containing a listing of top posts.
707
690
 
708
691
  Tags:
709
692
  listings, posts, top, read-only
@@ -760,7 +743,7 @@ class RedditApp(APIApplication):
760
743
  type: Optional. A comma-separated list of result types ('sr', 'link', 'user').
761
744
 
762
745
  Returns:
763
- Any: API response data containing search results.
746
+ A dictionary containing the search results.
764
747
 
765
748
  Tags:
766
749
  search, reddit, posts, comments, users, read-only
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp-applications
3
- Version: 0.1.24
3
+ Version: 0.1.26
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
@@ -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=3LvFRBKzlA_Gzkj07eyFiuCcKRBTd7-9WIhdrYpLOO8,20885
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
@@ -182,7 +182,7 @@ universal_mcp/applications/posthog/__init__.py,sha256=2j8vH09Xqz51BlfNehaEvY2A2L
182
182
  universal_mcp/applications/posthog/app.py,sha256=-T0hOSxPT3qQEVG8IrRY0MVi0YI8yHHeKPgl5jRexGY,275182
183
183
  universal_mcp/applications/reddit/README.md,sha256=RzGOX8sFJldmLsrjFG1FHmWzL7RddZTylb-m8XjSKQI,6558
184
184
  universal_mcp/applications/reddit/__init__.py,sha256=JHA_BMVnuQyoDMbQt5dRNxKPrAxyZZL6zzQyNbvyjHs,27
185
- universal_mcp/applications/reddit/app.py,sha256=jC5ha7sszKNypIRiX19ZYMEMafGxBe9nVZqiKKu8lO0,35841
185
+ universal_mcp/applications/reddit/app.py,sha256=HUqYRJ7U07uMg-U5c5VDrboSlSSB1_gD_dtDiwqDSDo,35648
186
186
  universal_mcp/applications/resend/README.md,sha256=zfKzut3KCw9JlX3crspSjRsLZyoN9uly_yZWjWA4LUU,8409
187
187
  universal_mcp/applications/resend/__init__.py,sha256=4me7YQFrYTNvqbJawBrsrJlzyFRwXZA6u4E09IPWjlk,27
188
188
  universal_mcp/applications/resend/app.py,sha256=bPW5wgl7wf2m4tgygSwdfRCaKKBEPwMrLWm3yhOP9kk,34879
@@ -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.24.dist-info/METADATA,sha256=WcvhSK6ZVRfoATJtf8wtM_kmcDhW0sr2c8NZ5iiNlpc,2956
280
- universal_mcp_applications-0.1.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
281
- universal_mcp_applications-0.1.24.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
282
- universal_mcp_applications-0.1.24.dist-info/RECORD,,
279
+ universal_mcp_applications-0.1.26.dist-info/METADATA,sha256=0Ae_g0zJq4xSsdekTvbRe5hTweEP3hMBVXMTYiDrmmM,2956
280
+ universal_mcp_applications-0.1.26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
281
+ universal_mcp_applications-0.1.26.dist-info/licenses/LICENSE,sha256=NweDZVPslBAZFzlgByF158b85GR0f5_tLQgq1NS48To,1063
282
+ universal_mcp_applications-0.1.26.dist-info/RECORD,,