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.
- universal_mcp/applications/google_docs/app.py +152 -18
- universal_mcp/applications/outlook/app.py +281 -199
- universal_mcp/applications/reddit/app.py +30 -47
- {universal_mcp_applications-0.1.24.dist-info → universal_mcp_applications-0.1.26.dist-info}/METADATA +1 -1
- {universal_mcp_applications-0.1.24.dist-info → universal_mcp_applications-0.1.26.dist-info}/RECORD +7 -7
- {universal_mcp_applications-0.1.24.dist-info → universal_mcp_applications-0.1.26.dist-info}/WHEEL +0 -0
- {universal_mcp_applications-0.1.24.dist-info → universal_mcp_applications-0.1.26.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
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
|
|
62
|
-
|
|
66
|
+
document_id (str): The unique ID of the Google Document to retrieve.
|
|
67
|
+
|
|
63
68
|
Returns:
|
|
64
|
-
A dictionary
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 (`` format)
|
|
78
|
+
- Equations (LaTeX-style `$...$`)
|
|
79
|
+
- Footnotes (`[^1]` references and notes at the end)
|
|
80
|
+
- Horizontal rules (`---`)
|
|
81
|
+
|
|
70
82
|
Tags:
|
|
71
|
-
|
|
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\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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
13
|
+
def reply_to_email(
|
|
14
14
|
self,
|
|
15
15
|
message_id: str,
|
|
16
|
+
comment: str,
|
|
16
17
|
user_id: str | None = None,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
) -> Any:
|
|
18
|
+
attachments: list[dict[str, Any]] | None = None,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
20
|
"""
|
|
21
|
-
Replies to
|
|
21
|
+
Replies to a specific email message.
|
|
22
22
|
|
|
23
23
|
Args:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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:
|
|
40
|
+
dict[str, Any]: A dictionary confirming the reply action.
|
|
31
41
|
|
|
32
42
|
Raises:
|
|
33
|
-
HTTPStatusError:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
62
|
+
|
|
58
63
|
response = self._post(
|
|
59
64
|
url,
|
|
60
65
|
data=request_body_data,
|
|
61
|
-
params=
|
|
66
|
+
params={},
|
|
62
67
|
content_type="application/json",
|
|
63
68
|
)
|
|
64
69
|
return self._handle_response(response)
|
|
65
70
|
|
|
66
|
-
def
|
|
71
|
+
def send_email(
|
|
67
72
|
self,
|
|
68
|
-
|
|
73
|
+
subject: str,
|
|
74
|
+
body: str,
|
|
75
|
+
to_recipients: list[str],
|
|
69
76
|
user_id: str | None = None,
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
84
|
+
Sends a new email.
|
|
74
85
|
|
|
75
86
|
Args:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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:
|
|
98
|
+
dict[str, Any]: A dictionary confirming the send action.
|
|
82
99
|
|
|
83
100
|
Raises:
|
|
84
|
-
HTTPStatusError:
|
|
101
|
+
HTTPStatusError: If the API request fails.
|
|
102
|
+
ValueError: If the user_id cannot be retrieved.
|
|
85
103
|
|
|
86
104
|
Tags:
|
|
87
|
-
|
|
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.
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"
|
|
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
|
-
|
|
126
|
+
"message": message,
|
|
127
|
+
"saveToSentItems": save_to_sent_items,
|
|
104
128
|
}
|
|
129
|
+
|
|
105
130
|
url = f"{self.base_url}/users/{user_id}/sendMail"
|
|
106
|
-
|
|
131
|
+
|
|
107
132
|
response = self._post(
|
|
108
133
|
url,
|
|
109
134
|
data=request_body_data,
|
|
110
|
-
params=
|
|
135
|
+
params={},
|
|
111
136
|
content_type="application/json",
|
|
112
137
|
)
|
|
113
138
|
return self._handle_response(response)
|
|
114
139
|
|
|
115
|
-
def
|
|
140
|
+
def get_email_folder(
|
|
116
141
|
self,
|
|
117
|
-
|
|
142
|
+
folder_id: str,
|
|
118
143
|
user_id: str | None = None,
|
|
119
|
-
|
|
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
|
|
149
|
+
Retrieves a specific email folder's metadata by its ID.
|
|
125
150
|
|
|
126
151
|
Args:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
select (
|
|
131
|
-
expand (
|
|
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:
|
|
159
|
+
dict[str, Any]: A dictionary containing the mail folder's metadata.
|
|
135
160
|
|
|
136
161
|
Raises:
|
|
137
|
-
HTTPStatusError:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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",
|
|
157
|
-
("$select",
|
|
158
|
-
("$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
|
|
191
|
+
def list_emails(
|
|
166
192
|
self,
|
|
167
193
|
user_id: str | None = None,
|
|
168
194
|
select: list[str] = ["bodyPreview"],
|
|
169
|
-
|
|
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
|
|
205
|
+
Retrieves a list of emails from a user's mailbox.
|
|
180
206
|
|
|
181
207
|
Args:
|
|
182
|
-
user_id (
|
|
183
|
-
select (list):
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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]:
|
|
220
|
+
dict[str, Any]: A dictionary containing a list of emails and pagination information.
|
|
202
221
|
|
|
203
222
|
Raises:
|
|
204
|
-
|
|
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
|
-
|
|
226
|
+
important
|
|
208
227
|
"""
|
|
209
|
-
|
|
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.
|
|
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",
|
|
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
|
|
266
|
+
def get_email(
|
|
245
267
|
self,
|
|
246
268
|
message_id: str,
|
|
247
269
|
user_id: str | None = None,
|
|
248
|
-
|
|
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
|
|
275
|
+
Retrieves a specific email by its ID.
|
|
254
276
|
|
|
255
277
|
Args:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
select (
|
|
260
|
-
expand (
|
|
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:
|
|
285
|
+
dict[str, Any]: A dictionary containing the email's details.
|
|
264
286
|
|
|
265
287
|
Raises:
|
|
266
|
-
HTTPStatusError:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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",
|
|
286
|
-
("$select",
|
|
287
|
-
("$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
|
|
317
|
+
def delete_email(self, message_id: str, user_id: str | None = None) -> dict[str, Any]:
|
|
295
318
|
"""
|
|
296
|
-
Permanently deletes a specific email
|
|
319
|
+
Permanently deletes a specific email by its ID.
|
|
297
320
|
|
|
298
321
|
Args:
|
|
299
|
-
|
|
300
|
-
|
|
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:
|
|
326
|
+
dict[str, Any]: A dictionary confirming the deletion.
|
|
304
327
|
|
|
305
328
|
Raises:
|
|
306
|
-
HTTPStatusError:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
360
|
+
Retrieves attachments for a specific email.
|
|
341
361
|
|
|
342
362
|
Args:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
top (
|
|
346
|
-
skip (
|
|
347
|
-
search (
|
|
348
|
-
filter (
|
|
349
|
-
count (
|
|
350
|
-
orderby (
|
|
351
|
-
select (
|
|
352
|
-
expand (
|
|
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]:
|
|
375
|
+
dict[str, Any]: A dictionary containing a list of attachments.
|
|
356
376
|
|
|
357
377
|
Raises:
|
|
358
|
-
|
|
359
|
-
|
|
378
|
+
ValueError: If incompatible parameters are used together.
|
|
379
|
+
HTTPStatusError: If the API request fails.
|
|
360
380
|
Tags:
|
|
361
|
-
|
|
381
|
+
important
|
|
362
382
|
"""
|
|
363
|
-
|
|
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.
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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",
|
|
383
|
-
("$select",
|
|
384
|
-
("$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
|
|
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
|
-
|
|
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]:
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
|
484
|
+
def get_next_page_results(self, url: str) -> dict[str, Any]:
|
|
415
485
|
"""
|
|
416
|
-
|
|
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
|
-
|
|
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.
|
|
434
|
-
self.
|
|
435
|
-
self.
|
|
436
|
-
self.
|
|
437
|
-
self.
|
|
438
|
-
self.
|
|
439
|
-
self.
|
|
440
|
-
self.
|
|
441
|
-
self.
|
|
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
|
|
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
|
-
|
|
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
|
|
91
|
-
limit: The maximum number of subreddits to return
|
|
92
|
-
sort: The order
|
|
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
|
|
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:
|
|
99
|
-
JSONDecodeError:
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
746
|
+
A dictionary containing the search results.
|
|
764
747
|
|
|
765
748
|
Tags:
|
|
766
749
|
search, reddit, posts, comments, users, read-only
|
{universal_mcp_applications-0.1.24.dist-info → universal_mcp_applications-0.1.26.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: universal-mcp-applications
|
|
3
|
-
Version: 0.1.
|
|
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
|
{universal_mcp_applications-0.1.24.dist-info → universal_mcp_applications-0.1.26.dist-info}/RECORD
RENAMED
|
@@ -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=
|
|
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=
|
|
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=
|
|
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.
|
|
280
|
-
universal_mcp_applications-0.1.
|
|
281
|
-
universal_mcp_applications-0.1.
|
|
282
|
-
universal_mcp_applications-0.1.
|
|
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,,
|
{universal_mcp_applications-0.1.24.dist-info → universal_mcp_applications-0.1.26.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|