universal-mcp 0.1.2rc1__py3-none-any.whl → 0.1.3rc1__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.
- universal_mcp/applications/firecrawl/app.py +74 -190
- universal_mcp/applications/markitdown/app.py +17 -6
- universal_mcp/applications/notion/README.md +55 -0
- universal_mcp/applications/notion/__init__.py +0 -0
- universal_mcp/applications/notion/app.py +407 -0
- universal_mcp/applications/perplexity/app.py +79 -0
- universal_mcp/cli.py +21 -14
- universal_mcp/integrations/integration.py +9 -3
- universal_mcp/logger.py +74 -0
- universal_mcp/servers/server.py +28 -21
- universal_mcp/utils/docgen.py +2 -2
- universal_mcp/utils/installation.py +15 -0
- universal_mcp/utils/openapi.py +48 -48
- universal_mcp-0.1.3rc1.dist-info/METADATA +252 -0
- {universal_mcp-0.1.2rc1.dist-info → universal_mcp-0.1.3rc1.dist-info}/RECORD +17 -12
- universal_mcp-0.1.2rc1.dist-info/METADATA +0 -207
- {universal_mcp-0.1.2rc1.dist-info → universal_mcp-0.1.3rc1.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.2rc1.dist-info → universal_mcp-0.1.3rc1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,407 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from universal_mcp.applications import APIApplication
|
4
|
+
from universal_mcp.integrations import Integration
|
5
|
+
|
6
|
+
|
7
|
+
class NotionApp(APIApplication):
|
8
|
+
def __init__(self, integration: Integration = None, **kwargs) -> None:
|
9
|
+
"""
|
10
|
+
Initializes a new instance of the Notion API app with a specified integration and additional parameters.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
integration: Optional; an Integration object containing credentials for authenticating with the Notion API. Defaults to None.
|
14
|
+
kwargs: Additional keyword arguments that are passed to the parent class initializer.
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
None
|
18
|
+
"""
|
19
|
+
super().__init__(name='notion', integration=integration, **kwargs)
|
20
|
+
self.base_url = "https://api.notion.com"
|
21
|
+
|
22
|
+
def _get_headers(self):
|
23
|
+
if not self.integration:
|
24
|
+
raise ValueError("Integration not configured for NotionApp")
|
25
|
+
credentials = self.integration.get_credentials()
|
26
|
+
if "headers" in credentials:
|
27
|
+
return credentials["headers"]
|
28
|
+
return {
|
29
|
+
"Authorization": f"Bearer {credentials['access_token']}",
|
30
|
+
"Accept": "application/json",
|
31
|
+
"Notion-Version": "2022-06-28",
|
32
|
+
}
|
33
|
+
|
34
|
+
def retrieve_a_user(self, id, request_body=None) -> dict[str, Any]:
|
35
|
+
"""
|
36
|
+
Retrieves user details from the server using the specified user ID.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
self: Instance of the class containing the method.
|
40
|
+
id: The unique identifier of the user to retrieve.
|
41
|
+
request_body: Optional request body data, provided when needed. Default is None.
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
A dictionary containing user details, as retrieved from the server response.
|
45
|
+
"""
|
46
|
+
if id is None:
|
47
|
+
raise ValueError("Missing required parameter 'id'")
|
48
|
+
url = f"{self.base_url}/v1/users/{id}"
|
49
|
+
query_params = {}
|
50
|
+
response = self._get(url, params=query_params)
|
51
|
+
response.raise_for_status()
|
52
|
+
return response.json()
|
53
|
+
|
54
|
+
def list_all_users(self, ) -> dict[str, Any]:
|
55
|
+
"""
|
56
|
+
Fetches a list of all users from the API endpoint and returns the data as a dictionary.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
None: This method does not take any parameters.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
A dictionary containing the list of users retrieved from the API. The dictionary keys are strings, and the values can be of any type.
|
63
|
+
"""
|
64
|
+
url = f"{self.base_url}/v1/users"
|
65
|
+
query_params = {}
|
66
|
+
response = self._get(url, params=query_params)
|
67
|
+
response.raise_for_status()
|
68
|
+
return response.json()
|
69
|
+
|
70
|
+
def retrieve_your_token_sbot_user(self, ) -> dict[str, Any]:
|
71
|
+
"""
|
72
|
+
Retrieves the authentication token for the current user from the SBOT service.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
self: Instance of the class containing configuration such as 'base_url' and the '_get' method.
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
A dictionary containing the JSON response of the current user's token information.
|
79
|
+
"""
|
80
|
+
url = f"{self.base_url}/v1/users/me"
|
81
|
+
query_params = {}
|
82
|
+
response = self._get(url, params=query_params)
|
83
|
+
response.raise_for_status()
|
84
|
+
return response.json()
|
85
|
+
|
86
|
+
def retrieve_a_database(self, id) -> dict[str, Any]:
|
87
|
+
"""
|
88
|
+
Retrieves database details from a specified endpoint using the provided database ID.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
id: A unique identifier for the database to be retrieved. Must be a non-null value.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
A dictionary containing the details of the requested database.
|
95
|
+
"""
|
96
|
+
if id is None:
|
97
|
+
raise ValueError("Missing required parameter 'id'")
|
98
|
+
url = f"{self.base_url}/v1/databases/{id}"
|
99
|
+
query_params = {}
|
100
|
+
response = self._get(url, params=query_params)
|
101
|
+
response.raise_for_status()
|
102
|
+
return response.json()
|
103
|
+
|
104
|
+
def update_a_database(self, id, request_body=None) -> dict[str, Any]:
|
105
|
+
"""
|
106
|
+
Updates a database entry with the given ID using a PATCH request.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
self: The instance of the class to which the method belongs.
|
110
|
+
id: The unique identifier of the database entry to be updated.
|
111
|
+
request_body: An optional dictionary containing the data to update the database entry with. Defaults to None.
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
A dictionary representing the JSON response from the server after the update operation.
|
115
|
+
"""
|
116
|
+
if id is None:
|
117
|
+
raise ValueError("Missing required parameter 'id'")
|
118
|
+
url = f"{self.base_url}/v1/databases/{id}"
|
119
|
+
query_params = {}
|
120
|
+
response = self._patch(url, data=request_body, params=query_params)
|
121
|
+
response.raise_for_status()
|
122
|
+
return response.json()
|
123
|
+
|
124
|
+
def query_a_database(self, id, request_body=None) -> dict[str, Any]:
|
125
|
+
"""
|
126
|
+
Executes a query on a specified database using an identifier and an optional request body.
|
127
|
+
|
128
|
+
Args:
|
129
|
+
id: The unique identifier of the database to query.
|
130
|
+
request_body: Optional JSON-compatible dictionary representing the body of the query request; if None, no additional query data is sent.
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
A dictionary containing the response data from the database query as parsed from JSON.
|
134
|
+
"""
|
135
|
+
if id is None:
|
136
|
+
raise ValueError("Missing required parameter 'id'")
|
137
|
+
url = f"{self.base_url}/v1/databases/{id}/query"
|
138
|
+
query_params = {}
|
139
|
+
response = self._post(url, data=request_body, params=query_params)
|
140
|
+
response.raise_for_status()
|
141
|
+
return response.json()
|
142
|
+
|
143
|
+
def create_a_database(self, request_body=None) -> dict[str, Any]:
|
144
|
+
"""
|
145
|
+
Creates a new database on the server using the specified request body.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
request_body: A dictionary containing the data to be sent in the request body. Defaults to None if not provided.
|
149
|
+
|
150
|
+
Returns:
|
151
|
+
A dictionary containing the server's JSON response from the database creation request.
|
152
|
+
"""
|
153
|
+
url = f"{self.base_url}/v1/databases/"
|
154
|
+
query_params = {}
|
155
|
+
response = self._post(url, data=request_body, params=query_params)
|
156
|
+
response.raise_for_status()
|
157
|
+
return response.json()
|
158
|
+
|
159
|
+
def create_a_page(self, request_body=None) -> dict[str, Any]:
|
160
|
+
"""
|
161
|
+
Creates a new page by sending a POST request to the specified endpoint.
|
162
|
+
|
163
|
+
Args:
|
164
|
+
request_body: Optional; A dictionary containing the data to be sent in the body of the POST request. Defaults to None.
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
A dictionary representing the JSON response from the server, containing the details of the newly created page.
|
168
|
+
"""
|
169
|
+
url = f"{self.base_url}/v1/pages/"
|
170
|
+
query_params = {}
|
171
|
+
response = self._post(url, data=request_body, params=query_params)
|
172
|
+
response.raise_for_status()
|
173
|
+
return response.json()
|
174
|
+
|
175
|
+
def retrieve_a_page(self, id) -> dict[str, Any]:
|
176
|
+
"""
|
177
|
+
Retrieves a page by its unique identifier from a remote server.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
id: The unique identifier of the page to retrieve.
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
A dictionary containing the JSON response from the server, which represents the page data.
|
184
|
+
"""
|
185
|
+
if id is None:
|
186
|
+
raise ValueError("Missing required parameter 'id'")
|
187
|
+
url = f"{self.base_url}/v1/pages/{id}"
|
188
|
+
query_params = {}
|
189
|
+
response = self._get(url, params=query_params)
|
190
|
+
response.raise_for_status()
|
191
|
+
return response.json()
|
192
|
+
|
193
|
+
def update_page_properties(self, id, request_body=None) -> dict[str, Any]:
|
194
|
+
"""
|
195
|
+
Updates the properties of a page identified by its ID using the provided request body.
|
196
|
+
|
197
|
+
Args:
|
198
|
+
id: The unique identifier of the page whose properties are to be updated. Must not be None.
|
199
|
+
request_body: An optional dictionary representing the request payload containing the properties to be updated. Defaults to None.
|
200
|
+
|
201
|
+
Returns:
|
202
|
+
A dictionary containing the updated page properties as returned by the server after the update.
|
203
|
+
"""
|
204
|
+
if id is None:
|
205
|
+
raise ValueError("Missing required parameter 'id'")
|
206
|
+
url = f"{self.base_url}/v1/pages/{id}"
|
207
|
+
query_params = {}
|
208
|
+
response = self._patch(url, data=request_body, params=query_params)
|
209
|
+
response.raise_for_status()
|
210
|
+
return response.json()
|
211
|
+
|
212
|
+
def retrieve_a_page_property_item(self, page_id, property_id) -> dict[str, Any]:
|
213
|
+
"""
|
214
|
+
Retrieves the property item of a page using specified page and property identifiers.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
page_id: The unique identifier of the page from which the property item is to be retrieved.
|
218
|
+
property_id: The unique identifier of the property associated with the specified page.
|
219
|
+
|
220
|
+
Returns:
|
221
|
+
A dictionary representing the JSON response with details of the property item.
|
222
|
+
"""
|
223
|
+
if page_id is None:
|
224
|
+
raise ValueError("Missing required parameter 'page_id'")
|
225
|
+
if property_id is None:
|
226
|
+
raise ValueError("Missing required parameter 'property_id'")
|
227
|
+
url = f"{self.base_url}/v1/pages/{page_id}/properties/{property_id}"
|
228
|
+
query_params = {}
|
229
|
+
response = self._get(url, params=query_params)
|
230
|
+
response.raise_for_status()
|
231
|
+
return response.json()
|
232
|
+
|
233
|
+
def retrieve_block_children(self, id, page_size=None) -> dict[str, Any]:
|
234
|
+
"""
|
235
|
+
Retrieves the children of a specified block using its unique identifier.
|
236
|
+
|
237
|
+
Args:
|
238
|
+
id: The unique identifier of the block whose children are to be retrieved.
|
239
|
+
page_size: Optional; The maximum number of children to return per page in the response, if specified.
|
240
|
+
|
241
|
+
Returns:
|
242
|
+
A dictionary containing the data representing the children of the specified block.
|
243
|
+
"""
|
244
|
+
if id is None:
|
245
|
+
raise ValueError("Missing required parameter 'id'")
|
246
|
+
url = f"{self.base_url}/v1/blocks/{id}/children"
|
247
|
+
query_params = {k: v for k, v in [('page_size', page_size)] if v is not None}
|
248
|
+
response = self._get(url, params=query_params)
|
249
|
+
response.raise_for_status()
|
250
|
+
return response.json()
|
251
|
+
|
252
|
+
def append_block_children(self, id, request_body=None) -> dict[str, Any]:
|
253
|
+
"""
|
254
|
+
Appends child elements to a block identified by its ID and returns the updated block data.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
self: Instance of the class containing this method.
|
258
|
+
id: The identifier of the block to which children will be appended. It must not be None.
|
259
|
+
request_body: Optional dictionary containing the data of the child elements to be appended to the block.
|
260
|
+
|
261
|
+
Returns:
|
262
|
+
A dictionary representing the updated block data after appending the child elements.
|
263
|
+
"""
|
264
|
+
if id is None:
|
265
|
+
raise ValueError("Missing required parameter 'id'")
|
266
|
+
url = f"{self.base_url}/v1/blocks/{id}/children"
|
267
|
+
query_params = {}
|
268
|
+
response = self._patch(url, data=request_body, params=query_params)
|
269
|
+
response.raise_for_status()
|
270
|
+
return response.json()
|
271
|
+
|
272
|
+
def retrieve_a_block(self, id) -> dict[str, Any]:
|
273
|
+
"""
|
274
|
+
Retrieves a block of data from a given API endpoint using the specified block ID.
|
275
|
+
|
276
|
+
Args:
|
277
|
+
id: The unique identifier for the block to be retrieved. It must be a non-None value, otherwise a ValueError will be raised.
|
278
|
+
|
279
|
+
Returns:
|
280
|
+
A dictionary containing the JSON response from the API, representing the block data corresponding to the provided ID.
|
281
|
+
"""
|
282
|
+
if id is None:
|
283
|
+
raise ValueError("Missing required parameter 'id'")
|
284
|
+
url = f"{self.base_url}/v1/blocks/{id}"
|
285
|
+
query_params = {}
|
286
|
+
response = self._get(url, params=query_params)
|
287
|
+
response.raise_for_status()
|
288
|
+
return response.json()
|
289
|
+
|
290
|
+
def delete_a_block(self, id) -> dict[str, Any]:
|
291
|
+
"""
|
292
|
+
Deletes a block by its unique identifier and returns the server's response.
|
293
|
+
|
294
|
+
Args:
|
295
|
+
id: The unique identifier of the block to be deleted. Must not be None.
|
296
|
+
|
297
|
+
Returns:
|
298
|
+
A dictionary containing the response data from the server after attempting to delete the block.
|
299
|
+
"""
|
300
|
+
if id is None:
|
301
|
+
raise ValueError("Missing required parameter 'id'")
|
302
|
+
url = f"{self.base_url}/v1/blocks/{id}"
|
303
|
+
query_params = {}
|
304
|
+
response = self._delete(url, params=query_params)
|
305
|
+
response.raise_for_status()
|
306
|
+
return response.json()
|
307
|
+
|
308
|
+
def update_a_block(self, id, request_body=None) -> dict[str, Any]:
|
309
|
+
"""
|
310
|
+
Updates a block by sending a PATCH request to the specified endpoint.
|
311
|
+
|
312
|
+
Args:
|
313
|
+
id: The unique identifier for the block that is to be updated.
|
314
|
+
request_body: Optional; A dictionary containing the data to update the block with. Defaults to None.
|
315
|
+
|
316
|
+
Returns:
|
317
|
+
A dictionary containing the JSON response from the server after updating the block.
|
318
|
+
"""
|
319
|
+
if id is None:
|
320
|
+
raise ValueError("Missing required parameter 'id'")
|
321
|
+
url = f"{self.base_url}/v1/blocks/{id}"
|
322
|
+
query_params = {}
|
323
|
+
response = self._patch(url, data=request_body, params=query_params)
|
324
|
+
response.raise_for_status()
|
325
|
+
return response.json()
|
326
|
+
|
327
|
+
def search(self, request_body=None) -> dict[str, Any]:
|
328
|
+
"""
|
329
|
+
Executes a search query using the specified request body and returns the results.
|
330
|
+
|
331
|
+
Args:
|
332
|
+
request_body: An optional dictionary containing the data to be sent in the search request.
|
333
|
+
|
334
|
+
Returns:
|
335
|
+
A dictionary containing the JSON-decoded response from the search operation.
|
336
|
+
"""
|
337
|
+
url = f"{self.base_url}/v1/search"
|
338
|
+
query_params = {}
|
339
|
+
response = self._post(url, data=request_body, params=query_params)
|
340
|
+
response.raise_for_status()
|
341
|
+
return response.json()
|
342
|
+
|
343
|
+
def retrieve_comments(self, block_id=None, page_size=None, request_body=None) -> dict[str, Any]:
|
344
|
+
"""
|
345
|
+
Fetches comments from a remote server for a specified block, with optional pagination.
|
346
|
+
|
347
|
+
Args:
|
348
|
+
block_id: Optional; Identifies the block whose comments should be retrieved. If None, retrieves comments without filtering by block.
|
349
|
+
page_size: Optional; Specifies the number of comments to retrieve per page for pagination. If None, the server's default page size is used.
|
350
|
+
request_body: Unused placeholder for future extensibility; this function currently does not utilize this parameter.
|
351
|
+
|
352
|
+
Returns:
|
353
|
+
A dictionary containing the response data from the server, parsed from JSON, with keys and values representing comment-related information.
|
354
|
+
"""
|
355
|
+
url = f"{self.base_url}/v1/comments"
|
356
|
+
query_params = {k: v for k, v in [('block_id', block_id), ('page_size', page_size)] if v is not None}
|
357
|
+
response = self._get(url, params=query_params)
|
358
|
+
response.raise_for_status()
|
359
|
+
return response.json()
|
360
|
+
|
361
|
+
def add_comment_to_page(self, request_body=None) -> dict[str, Any]:
|
362
|
+
"""
|
363
|
+
Adds a comment to a page by sending a POST request with the provided request body.
|
364
|
+
|
365
|
+
Args:
|
366
|
+
request_body: Optional; A dictionary containing the data for the comment to be added. Defaults to None.
|
367
|
+
|
368
|
+
Returns:
|
369
|
+
A dictionary representing the JSON response from the server, which includes details of the newly added comment.
|
370
|
+
"""
|
371
|
+
url = f"{self.base_url}/v1/comments"
|
372
|
+
query_params = {}
|
373
|
+
response = self._post(url, data=request_body, params=query_params)
|
374
|
+
response.raise_for_status()
|
375
|
+
return response.json()
|
376
|
+
|
377
|
+
def list_tools(self):
|
378
|
+
"""
|
379
|
+
Returns a list of functions related to user and database operations.
|
380
|
+
|
381
|
+
Args:
|
382
|
+
self: The instance of the class to which the function belongs.
|
383
|
+
|
384
|
+
Returns:
|
385
|
+
A list of method references that pertain to various operations such as user retrieval, database operations, and page/block management.
|
386
|
+
"""
|
387
|
+
return [
|
388
|
+
self.retrieve_a_user,
|
389
|
+
self.list_all_users,
|
390
|
+
self.retrieve_your_token_sbot_user,
|
391
|
+
self.retrieve_a_database,
|
392
|
+
self.update_a_database,
|
393
|
+
self.query_a_database,
|
394
|
+
self.create_a_database,
|
395
|
+
self.create_a_page,
|
396
|
+
self.retrieve_a_page,
|
397
|
+
self.update_page_properties,
|
398
|
+
self.retrieve_a_page_property_item,
|
399
|
+
self.retrieve_block_children,
|
400
|
+
self.append_block_children,
|
401
|
+
self.retrieve_a_block,
|
402
|
+
self.delete_a_block,
|
403
|
+
self.update_a_block,
|
404
|
+
self.search,
|
405
|
+
self.retrieve_comments,
|
406
|
+
self.add_comment_to_page
|
407
|
+
]
|
@@ -0,0 +1,79 @@
|
|
1
|
+
from typing import Any, Literal
|
2
|
+
|
3
|
+
from universal_mcp.applications.application import APIApplication
|
4
|
+
from universal_mcp.integrations import Integration
|
5
|
+
|
6
|
+
|
7
|
+
class PerplexityApp(APIApplication):
|
8
|
+
def __init__(self, integration: Integration | None = None) -> None:
|
9
|
+
super().__init__(name="perplexity", integration=integration)
|
10
|
+
self.api_key: str | None = None
|
11
|
+
self.base_url = "https://api.perplexity.ai"
|
12
|
+
|
13
|
+
def _set_api_key(self):
|
14
|
+
if self.api_key:
|
15
|
+
return
|
16
|
+
|
17
|
+
if not self.integration:
|
18
|
+
raise ValueError("Integration is None. Cannot retrieve Perplexity API Key.")
|
19
|
+
|
20
|
+
credentials = self.integration.get_credentials()
|
21
|
+
if not credentials:
|
22
|
+
raise ValueError(
|
23
|
+
f"Failed to retrieve Perplexity API Key using integration '{self.integration.name}'. "
|
24
|
+
)
|
25
|
+
|
26
|
+
if not isinstance(credentials, str) or not credentials.strip():
|
27
|
+
raise ValueError(
|
28
|
+
f"Invalid credential format received for Perplexity API Key via integration '{self.integration.name}'. "
|
29
|
+
)
|
30
|
+
self.api_key = credentials
|
31
|
+
|
32
|
+
def _get_headers(self) -> dict[str, str]:
|
33
|
+
self._set_api_key()
|
34
|
+
return {
|
35
|
+
"Authorization": f"Bearer {self.api_key}",
|
36
|
+
"Content-Type": "application/json",
|
37
|
+
"Accept": "application/json",
|
38
|
+
}
|
39
|
+
|
40
|
+
def chat(self, query: str, model: Literal["r1-1776","sonar","sonar-pro","sonar-reasoning","sonar-reasoning-pro", "sonar-deep-research"] = "sonar" , temperature: float = 1, system_prompt: str = "Be precise and concise.") -> dict[str, Any] | str:
|
41
|
+
"""
|
42
|
+
Sends a query to a Perplexity Sonar online model and returns the response.
|
43
|
+
|
44
|
+
This uses the chat completions endpoint, suitable for conversational queries
|
45
|
+
and leveraging Perplexity's online capabilities.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
query: The user's query or message.
|
49
|
+
model: The specific Perplexity model to use (e.g., "r1-1776","sonar","sonar-pro","sonar-reasoning","sonar-reasoning-pro", "sonar-deep-research").Defaults to 'sonar'.
|
50
|
+
temperature: Sampling temperature for the response generation (e.g., 0.7).
|
51
|
+
system_prompt: An optional system message to guide the model's behavior.
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
A dictionary containing 'content' (str) and 'citations' (list) on success, or a string containing an error message on failure.
|
55
|
+
"""
|
56
|
+
endpoint = f"{self.base_url}/chat/completions"
|
57
|
+
|
58
|
+
messages = []
|
59
|
+
if system_prompt:
|
60
|
+
messages.append({"role": "system", "content": system_prompt})
|
61
|
+
messages.append({"role": "user", "content": query})
|
62
|
+
|
63
|
+
payload = {
|
64
|
+
"model": model,
|
65
|
+
"messages": messages,
|
66
|
+
"temperature": temperature,
|
67
|
+
# "max_tokens": 512,
|
68
|
+
}
|
69
|
+
|
70
|
+
data = self._post(endpoint, data=payload)
|
71
|
+
response = data.json()
|
72
|
+
content = response['choices'][0]['message']['content']
|
73
|
+
citations = response.get('citations', [])
|
74
|
+
return {"content": content, "citations": citations}
|
75
|
+
|
76
|
+
def list_tools(self):
|
77
|
+
return [
|
78
|
+
self.chat,
|
79
|
+
]
|
universal_mcp/cli.py
CHANGED
@@ -3,6 +3,8 @@ import os
|
|
3
3
|
from pathlib import Path
|
4
4
|
|
5
5
|
import typer
|
6
|
+
from rich import print as rprint
|
7
|
+
from rich.panel import Panel
|
6
8
|
|
7
9
|
from universal_mcp.utils.installation import (
|
8
10
|
get_supported_apps,
|
@@ -29,7 +31,7 @@ def generate(
|
|
29
31
|
"""Generate API client from OpenAPI schema with optional docstring generation.
|
30
32
|
|
31
33
|
The output filename should match the name of the API in the schema (e.g., 'twitter.py' for Twitter API).
|
32
|
-
This name will be used for the folder in applications
|
34
|
+
This name will be used for the folder in applications/.
|
33
35
|
"""
|
34
36
|
# Import here to avoid circular imports
|
35
37
|
from universal_mcp.utils.api_generator import generate_api_from_schema
|
@@ -66,7 +68,7 @@ def generate(
|
|
66
68
|
def docgen(
|
67
69
|
file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
|
68
70
|
model: str = typer.Option(
|
69
|
-
"anthropic/claude-3-sonnet-
|
71
|
+
"anthropic/claude-3-5-sonnet-20241022",
|
70
72
|
"--model",
|
71
73
|
"-m",
|
72
74
|
help="Model to use for generating docstrings",
|
@@ -97,9 +99,6 @@ def docgen(
|
|
97
99
|
typer.echo(f"Successfully processed {processed} functions")
|
98
100
|
except Exception as e:
|
99
101
|
typer.echo(f"Error: {e}", err=True)
|
100
|
-
import traceback
|
101
|
-
|
102
|
-
traceback.print_exc()
|
103
102
|
raise typer.Exit(1) from e
|
104
103
|
|
105
104
|
|
@@ -111,8 +110,11 @@ def run(
|
|
111
110
|
):
|
112
111
|
"""Run the MCP server"""
|
113
112
|
from universal_mcp.config import ServerConfig
|
113
|
+
from universal_mcp.logger import setup_logger
|
114
114
|
from universal_mcp.servers import server_from_config
|
115
115
|
|
116
|
+
setup_logger()
|
117
|
+
|
116
118
|
if config_path:
|
117
119
|
config = ServerConfig.model_validate_json(config_path.read_text())
|
118
120
|
else:
|
@@ -135,18 +137,23 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
|
|
135
137
|
raise typer.Exit(1)
|
136
138
|
|
137
139
|
# Print instructions before asking for API key
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
140
|
+
|
141
|
+
rprint(
|
142
|
+
Panel(
|
143
|
+
"API key is required. Visit [link]https://agentr.dev[/link] to create an API key.",
|
144
|
+
title="Instruction",
|
145
|
+
border_style="blue",
|
146
|
+
padding=(1, 2),
|
147
|
+
)
|
146
148
|
)
|
147
149
|
|
148
150
|
# Prompt for API key
|
149
|
-
api_key = typer.prompt(
|
151
|
+
api_key = typer.prompt(
|
152
|
+
"Enter your AgentR API key",
|
153
|
+
hide_input=False,
|
154
|
+
show_default=False,
|
155
|
+
type=str,
|
156
|
+
)
|
150
157
|
try:
|
151
158
|
if app_name == "claude":
|
152
159
|
typer.echo(f"Installing mcp server for: {app_name}")
|
@@ -7,6 +7,13 @@ from universal_mcp.exceptions import NotAuthorizedError
|
|
7
7
|
from universal_mcp.stores.store import Store
|
8
8
|
|
9
9
|
|
10
|
+
def sanitize_api_key_name(name: str) -> str:
|
11
|
+
suffix = "_API_KEY"
|
12
|
+
if name.endswith(suffix) or name.endswith(suffix.lower()):
|
13
|
+
return name.upper()
|
14
|
+
else:
|
15
|
+
return f"{name.upper()}{suffix}"
|
16
|
+
|
10
17
|
class Integration(ABC):
|
11
18
|
"""Abstract base class for handling application integrations and authentication.
|
12
19
|
|
@@ -75,9 +82,8 @@ class ApiKeyIntegration(Integration):
|
|
75
82
|
"""
|
76
83
|
|
77
84
|
def __init__(self, name: str, store: Store = None, **kwargs):
|
78
|
-
|
79
|
-
|
80
|
-
self.name = f"{name}_api_key"
|
85
|
+
sanitized_name = sanitize_api_key_name(name)
|
86
|
+
super().__init__(sanitized_name, store, **kwargs)
|
81
87
|
logger.info(f"Initializing API Key Integration: {name} with store: {store}")
|
82
88
|
|
83
89
|
def get_credentials(self):
|
universal_mcp/logger.py
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
import uuid
|
4
|
+
from functools import lru_cache
|
5
|
+
|
6
|
+
from loguru import logger
|
7
|
+
|
8
|
+
|
9
|
+
@lru_cache(maxsize=1)
|
10
|
+
def get_version():
|
11
|
+
"""
|
12
|
+
Get the version of the Universal MCP
|
13
|
+
"""
|
14
|
+
try:
|
15
|
+
from importlib.metadata import version
|
16
|
+
|
17
|
+
print(version("universal_mcp"))
|
18
|
+
return version("universal_mcp")
|
19
|
+
except ImportError:
|
20
|
+
return "unknown"
|
21
|
+
|
22
|
+
|
23
|
+
def get_user_id():
|
24
|
+
"""
|
25
|
+
Generate a unique user ID for the current session
|
26
|
+
"""
|
27
|
+
return "universal_" + str(uuid.uuid4())[:8]
|
28
|
+
|
29
|
+
|
30
|
+
def posthog_sink(message, user_id=get_user_id()):
|
31
|
+
"""
|
32
|
+
Custom sink for sending logs to PostHog
|
33
|
+
"""
|
34
|
+
try:
|
35
|
+
import posthog
|
36
|
+
|
37
|
+
posthog.host = "https://us.i.posthog.com"
|
38
|
+
posthog.api_key = "phc_6HXMDi8CjfIW0l04l34L7IDkpCDeOVz9cOz1KLAHXh8"
|
39
|
+
|
40
|
+
record = message.record
|
41
|
+
properties = {
|
42
|
+
"level": record["level"].name,
|
43
|
+
"module": record["name"],
|
44
|
+
"function": record["function"],
|
45
|
+
"line": record["line"],
|
46
|
+
"message": record["message"],
|
47
|
+
"version": get_version(),
|
48
|
+
}
|
49
|
+
posthog.capture(user_id, "universal_mcp", properties)
|
50
|
+
except Exception:
|
51
|
+
# Silently fail if PostHog capture fails - don't want logging to break the app
|
52
|
+
pass
|
53
|
+
|
54
|
+
|
55
|
+
def setup_logger():
|
56
|
+
logger.remove()
|
57
|
+
logger.add(
|
58
|
+
sink=sys.stdout,
|
59
|
+
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
60
|
+
level="INFO",
|
61
|
+
colorize=True,
|
62
|
+
)
|
63
|
+
logger.add(
|
64
|
+
sink=sys.stderr,
|
65
|
+
format="<red>{time:YYYY-MM-DD HH:mm:ss}</red> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
66
|
+
level="ERROR",
|
67
|
+
colorize=True,
|
68
|
+
)
|
69
|
+
telemetry_enabled = os.getenv("TELEMETRY_ENABLED", "true").lower() == "true"
|
70
|
+
if telemetry_enabled:
|
71
|
+
logger.add(posthog_sink, level="INFO") # PostHog telemetry
|
72
|
+
|
73
|
+
|
74
|
+
setup_logger()
|